When to use
Pick this skill when SQL alone can't answer the question:
- r2 commands not surfaced as tables (e.g. ESIL emulation,
aae*,agCcall-graph dump,p8raw-byte read at an address,wxto patch bytes, …) - multi-step r2 scripts where each step depends on r2 state, not just table data
- custom analyses written in r2js (
#!js) — typed JS that runs inside r2 with full SDK access - pivoting from SQL results back into r2 (e.g. drop temporary flags so the next SQL query sees them)
How r2xsql exposes raw r2 access
r2xsql holds a Backend (either LibrBackend or R2PipeBackend) and
forwards arbitrary commands through Session::raw_cmd(std::string).
From the CLI, raw r2 commands are reachable via:
| transport | syntax |
|---|---|
r2xsql -q/REPL |
a line starting with . runs a raw r2 command (the leading . is stripped), e.g. r2xsql -s a.bin -q ".pdf @ entry0" |
r2xsql --http |
POST /query body .r2cmd <command> → {"success":true,"output":"…"} |
| in-r2 plugin | the host RCore * is the r2 shell already |
| r2js inside r2 | r2cmd("aflj") returns JSON; round-trip via SQL |
| temp flags bridge | r2js writes flags, SQL reads them via flags table |
Recommended pattern: do the analysis pass in r2js, persist its results as r2 flags / comments, then query those from SQL.
Recipe — emulate a function and capture register state
// my_emulate.r2.js
const target = 0x401000;
r2cmd(`s ${target}`); // seek
r2cmd("aei; aeim; aeip"); // init ESIL, mem, ip
for (let i = 0; i < 50; i++) r2cmd("aes"); // step 50 instructions
const regs = r2cmd("arj"); // dump registers as JSON
const rax = JSON.parse(regs).rax;
r2cmd(`CC ESIL: rax=${rax.toString(16)} after 50 steps @ ${target}`);
Then SQL-side:
SELECT text FROM comments WHERE text LIKE 'ESIL:%';
Recipe — bridge query → r2js → query
SQL: find candidate addresses.
SELECT addr FROM funcs WHERE cc > 25;r2js: feed each address into a custom pass that doesn't fit SQL:
const addrs = [0x401000, 0x402000]; // from step 1 addrs.forEach(a => { r2cmd(`s ${a}`); const summary = r2cmd("agCd"); // some non-tabular output r2cmd(`f sym.complex_${a.toString(16)} 1 ${a}`); // tag it });SQL: re-query and the tags are visible:
SELECT name, addr FROM flags WHERE name LIKE 'sym.complex_%';
Raw hex byte patching → use the patches table, not r2js
r2xsql has a SQL write surface for this: INSERT INTO patches(addr, patched_bytes) VALUES(...) stages a raw hex patch, UPDATE patches SET committed = 1 WHERE addr = ... commits it (see the data skill's
patches section for the full column set and the commit/undo scoping
caveats — commit is table-wide, undo is stack-top-only). Reach for r2js
only when patches genuinely can't express what you need: an
assembled-instruction patch (wa) or a string/zero-terminated write (wz)
instead of raw hex, which patches doesn't support in its current version.
r2cmd("wa nop @ 0x401234"); // assembled patch -- not a `patches` INSERT
r2cmd("Ps"); // persist via project
Caveats
Session::raw_cmdbypasses the table layer entirely — there's no parsing, no schema, no pushdown. You're responsible for parsing whatever r2 prints.- r2js scripts run inside r2 (not inside r2xsql) — they don't see the SQL database directly. Bridge via flags/comments as above.
- Raw passthrough is available on every transport:
-q "."/REPL from the CLI,.r2cmd <command>over HTTP, and the host shell in the in-r2 plugin. The raw path bypasses the table layer (no schema, no pushdown).