When to use
Pick this skill any time the task is a mutation: adding/editing/
removing a comment, renaming a function, deleting a flag, persisting
session state. For read-only annotation lookups, use data or
disassembly.
Required: write mode + project
Mutations only land in r2's session memory. To persist them
across reopens you need both:
-w (or --write) when opening the session.
--project NAME so r2xsql can call Ps NAME on shutdown.
r2xsql -w --project demo -s ./malware.exe -q "
UPDATE comments SET text = 'crypto init' WHERE addr = 0x401000;
UPDATE flags SET name = 'aes_init' WHERE addr = 0x401000;
"
# reopen — the comment + rename are still there
r2xsql --project demo -s ./malware.exe -q "
SELECT text FROM comments WHERE addr = 0x401000;
"
Without --project, mutations are still visible in the current
session but vanish on exit.
Writable tables
| table |
INSERT → r2 cmd |
UPDATE → r2 cmd |
DELETE → r2 cmd |
funcs |
af @ <addr> (+afn) |
afn <name> @ <addr> (rename) / afs <sig> @ <addr> (set prototype) / afc <cc> @ <addr> (set calling convention) |
af- <addr> |
locals |
— |
afvn <new> <old> @ <fn> (rename) / afvt <name> <type> @ <fn> (retype) |
— |
comments |
CCu base64:<b64> @ <addr> |
CCu base64:<b64> @ <addr> |
CC- @ <addr> |
flags |
f <name> @ <addr> |
fr <old> <new> (rename, any flagspace) |
f- @ <addr> |
bookmarks |
f <name> @ <addr> |
fr <old> <new> |
f- @ <addr> |
io_maps |
om $d <vaddr> <size> <paddr> <perm> [name] |
— |
om-<map_id> |
types |
td "<kind> <name> {};" (empty struct/union/enum shell only; use r2xsql_type_define for a full declaration) |
— |
t- <name> |
projects |
— (use r2xsql_project_save) |
— |
P- <name> |
- Rename functions:
UPDATE funcs SET name='…' WHERE addr=… (issues afn).
flags also renames (UPDATE flags SET name='…' WHERE addr=…), always via
fr <old> <new> — radare2's generic in-place rename, regardless of which
flagspace the flag lives in. (This does NOT also drive afn: that command
only renames a flag already parked in the functions flagspace, and
otherwise creates a second, duplicate flag instead — to also rename a
function's own analysis-tracked name, use the funcs table.)
- Set a function prototype:
UPDATE funcs SET prototype='int parse(hdr_t *h, int len)' WHERE addr=… applies a C signature via afs (set-signature). A trailing ; is
stripped (r2's command separator); an empty/NULL prototype is rejected (r2 has no
clear-signature primitive). The read comes from aflj's signature field.
- Set a function's calling convention:
UPDATE funcs SET calltype='ms' WHERE addr=… issues afc <cc> (set-calling-convention). cc must be one of
the names SELECT name FROM calling_conventions reports for this session's
(arch,bits) — checked before the command runs, because afc itself gives no
usable signal on a rejected name: its textual output is empty on both a
successful set and an unknown convention. The read comes from aflj's
calltype field; the write is confirmed by reading afij back afterward.
- Every
funcs write (rename, set-signature, set-calltype, undefine, define) is
address-scoped, so
on the in-process (libr) backend each one also uses the same structural
command-dispatch path comments writes use — never evaluating r2's own command
separators at all, as a second, independent layer beneath the name/signature
validation above. The pipe backend is unaffected: it always uses the plain command
form.
flags' INSERT (f) and DELETE (f-) use the same structural dispatch (address-
scoped, like funcs/comments). Its rename does not issue fr at all on the
in-process (r2xsql-full) flavor — it calls radare2's rename function directly
(no address needed for that call either), measured substantially faster on a
bulk-rename workload with identical results. The pipe-only flavor still issues
fr, seeked to the flag's own address (a harmless anchor fr never consults).
bookmarks' INSERT (f) and DELETE (f-) use the same structural dispatch
(address-scoped) inside the pre-existing flagspace save/select/restore below.
Its rename (fr) is addr-less in the same way flags' is, and routes through
the dispatch the same way — seeked to the bookmark's own address, which fr
never consults, needing no new primitive. The pipe backend is unaffected on all
three writes.
types' DELETE (t-) and INSERT (td, including the general
r2xsql_type_define('<C decl>')) also use the structural dispatch, but
neither command has ANY address in its own grammar at all (a type
definition isn't tied to any address) — unlike every table above, there is
no row address to seek to, so both are seeked to a fixed anchor address (0)
instead, which neither command ever consults. The pipe backend is
unaffected on both.
- Rename / retype locals & args:
UPDATE locals SET name='…' or
SET type='…', keyed on func_addr + the current name. Locals
are analysis-derived — no INSERT/DELETE. The type is passed verbatim (r2 takes
a bare int / char *; do NOT quote it), validated to [A-Za-z0-9_.* []].
The pipe-only flavor issues afvn/afvt; r2xsql-full calls radare2's
rename/retype functions directly instead — same results, measured
substantially faster on a bulk rename/retype workload. On the in-process
(libr) backend, whenever that direct call is unavailable, the command-path
fallback (afvn/afvt, address-scoped like funcs's writes) also uses
the same structural command-dispatch path — never evaluating r2's own
command separators at all, as a second, independent layer beneath the
name/type validation above. The pipe backend is unaffected: it always uses
the plain command form.
- Comments are passed as
base64: so spaces, quotes, and @ are stored
verbatim and can't inject extra r2 commands, on the pipe-only flavor.
r2xsql-full sets/updates a comment by calling radare2's own comment-write
function directly instead of building that command — no encoding needed for
that call — while reproducing one existing behavior byte-for-byte: an
UPDATE whose new text is already contained inside the EXISTING comment is a
silent no-op on both flavors (this is radare2's own long-standing
dedup-on-write behavior, not something either flavor invented). On the
in-process (libr) backend, comment writes that DO fall back to the command
form also use a structural command-dispatch path that never evaluates r2's
own command separators at all, as a second, independent layer beneath the
encoding above.
- Reading
comments on the in-process (libr) backend reads radare2's
meta store directly instead of parsing CCj's JSON — same rows, faster,
unrelated to the write-path note above. comments.type always reads back
the literal string CCu (radare2's own comment-type tag, not a description
of the comment); comments.text reads back with a newline/tab/quote/
backslash or non-ASCII byte escaped (e.g. a real newline as the two
characters \n) — this is radare2's own command-path behavior on both
backends, not something either producer invents.
- Names must be
[A-Za-z0-9._$] (no spaces / r2 metacharacters) — enforced.
bookmarks writes are scoped to the bookmarks flagspace (saved/selected/
restored around each write); id is synthetic and ignored.
Persistence functions
SELECT r2xsql_project_save('triage1'); -- Ps: save the session as a project
SELECT name FROM projects; -- Plj: list saved projects
SELECT r2xsql_project_open('triage1'); -- P: load a project mid-session
SELECT r2xsql_type_define('struct hdr { int magic; int size; }'); -- td
Common operations
-- rename a function (the dummy fcn.<addr> names)
UPDATE funcs SET name = 'aes_decrypt' WHERE addr = 0x401000;
-- rename / retype a local variable or argument (keyed on func_addr + name)
UPDATE locals SET name = 'key_len' WHERE func_addr = 0x401000 AND name = 'var_20h';
UPDATE locals SET type = 'int' WHERE func_addr = 0x401000 AND name = 'key_len';
-- add / edit / remove a comment
INSERT INTO comments(addr, text) VALUES (0x401000, 'crypto init');
UPDATE comments SET text = 'crypto init v2' WHERE addr = 0x401000;
DELETE FROM comments WHERE addr = 0x401000;
-- add a flag / remove one
INSERT INTO flags(addr, name) VALUES (0x401000, 'aes_sbox');
DELETE FROM flags WHERE addr = 0x401000;
-- bookmarks (bookmarks flagspace)
INSERT INTO bookmarks (addr, name) VALUES (0x401000, 'loop_start');
UPDATE bookmarks SET name = 'loop_head' WHERE addr = 0x401000;
DELETE FROM bookmarks WHERE addr = 0x401000;
Bulk operations
UPDATE / DELETE happily take any SQL predicate, so bulk operations
just work:
-- mark every function whose name contains 'init' as analyzed
UPDATE comments SET text = 'TODO: review init logic'
WHERE addr IN (SELECT addr FROM funcs WHERE name LIKE '%init%');
-- delete every comment in a specific code section
DELETE FROM comments
WHERE addr IN (
SELECT addr FROM funcs
WHERE addr BETWEEN 0x401000 AND 0x402000
);
Caveats
comments, flags, and bookmarks support INSERT. Use INSERT
when adding a new address/name pair; UPDATE only affects rows that
already match the predicate.
- Changes are sequenced — r2xsql issues one r2 command per affected
row. For thousands of rows this is slow; chunk the predicate.
1---2name: r2xsql-annotations3description: Edit radare2 annotations — comments, flag/function names, local variables, bookmarks — via r2xsql. Use when asked to add comments, rename functions/flags, rename or retype locals, or persist analysis state.4---56## When to use78Pick this skill any time the task is a **mutation**: adding/editing/9removing a comment, renaming a function, deleting a flag, persisting10session state. For read-only annotation lookups, use `data` or11`disassembly`.1213## Required: write mode + project1415Mutations only land in r2's session memory. To **persist** them16across reopens you need both:17181. `-w` (or `--write`) when opening the session.192. `--project NAME` so r2xsql can call `Ps NAME` on shutdown.2021```bash22r2xsql -w --project demo -s ./malware.exe -q "23 UPDATE comments SET text = 'crypto init' WHERE addr = 0x401000;24 UPDATE flags SET name = 'aes_init' WHERE addr = 0x401000;25"26# reopen — the comment + rename are still there27r2xsql --project demo -s ./malware.exe -q "28 SELECT text FROM comments WHERE addr = 0x401000;29"30```3132Without `--project`, mutations are still visible in the current33session but vanish on exit.3435## Writable tables3637| table | INSERT → r2 cmd | UPDATE → r2 cmd | DELETE → r2 cmd |38|-------------|----------------------------|------------------------------------------|------------------|39| `funcs` | `af @ <addr>` (+`afn`) | `afn <name> @ <addr>` (**rename**) / `afs <sig> @ <addr>` (**set prototype**) / `afc <cc> @ <addr>` (**set calling convention**) | `af- <addr>` |40| `locals` | — | `afvn <new> <old> @ <fn>` (**rename**) / `afvt <name> <type> @ <fn>` (**retype**) | — |41| `comments` | `CCu base64:<b64> @ <addr>`| `CCu base64:<b64> @ <addr>` | `CC- @ <addr>` |42| `flags` | `f <name> @ <addr>` | `fr <old> <new>` (**rename**, any flagspace) | `f- @ <addr>` |43| `bookmarks` | `f <name> @ <addr>` | `fr <old> <new>` | `f- @ <addr>` |44| `io_maps` | `om $d <vaddr> <size> <paddr> <perm> [name]` | — | `om-<map_id>` |45| `types` | `td "<kind> <name> {};"` (empty struct/union/enum shell only; use `r2xsql_type_define` for a full declaration) | — | `t- <name>` |46| `projects` | — (use `r2xsql_project_save`)| — | `P- <name>` |4748- **Rename functions**: `UPDATE funcs SET name='…' WHERE addr=…` (issues `afn`).49 `flags` also renames (`UPDATE flags SET name='…' WHERE addr=…`), always via50 `fr <old> <new>` — radare2's generic in-place rename, regardless of which51 flagspace the flag lives in. (This does NOT also drive `afn`: that command52 only renames a flag already parked in the `functions` flagspace, and53 otherwise creates a second, duplicate flag instead — to also rename a54 function's own analysis-tracked name, use the `funcs` table.)55- **Set a function prototype**: `UPDATE funcs SET prototype='int parse(hdr_t *h, int len)'56 WHERE addr=…` applies a C signature via `afs` (set-signature). A trailing `;` is57 stripped (r2's command separator); an empty/NULL prototype is rejected (r2 has no58 clear-signature primitive). The read comes from `aflj`'s `signature` field.59- **Set a function's calling convention**: `UPDATE funcs SET calltype='ms'60 WHERE addr=…` issues `afc <cc>` (set-calling-convention). `cc` must be one of61 the names `SELECT name FROM calling_conventions` reports for this session's62 `(arch,bits)` — checked before the command runs, because `afc` itself gives no63 usable signal on a rejected name: its textual output is empty on both a64 successful set and an unknown convention. The read comes from `aflj`'s65 `calltype` field; the write is confirmed by reading `afij` back afterward.66- Every `funcs` write (rename, set-signature, set-calltype, undefine, define) is67 address-scoped, so68 on the in-process (libr) backend each one also uses the same structural69 command-dispatch path `comments` writes use — never evaluating r2's own command70 separators at all, as a second, independent layer beneath the name/signature71 validation above. The pipe backend is unaffected: it always uses the plain command72 form.73- `flags`' INSERT (`f`) and DELETE (`f-`) use the same structural dispatch (address-74 scoped, like `funcs`/`comments`). Its rename does not issue `fr` at all on the75 in-process (`r2xsql-full`) flavor — it calls radare2's rename function directly76 (no address needed for that call either), measured substantially faster on a77 bulk-rename workload with identical results. The pipe-only flavor still issues78 `fr`, seeked to the flag's own address (a harmless anchor `fr` never consults).79- `bookmarks`' INSERT (`f`) and DELETE (`f-`) use the same structural dispatch80 (address-scoped) inside the pre-existing flagspace save/select/restore below.81 Its rename (`fr`) is addr-less in the same way `flags`' is, and routes through82 the dispatch the same way — seeked to the bookmark's own address, which `fr`83 never consults, needing no new primitive. The pipe backend is unaffected on all84 three writes.85- `types`' DELETE (`t-`) and INSERT (`td`, including the general86 `r2xsql_type_define('<C decl>')`) also use the structural dispatch, but87 neither command has ANY address in its own grammar at all (a type88 definition isn't tied to any address) — unlike every table above, there is89 no row address to seek to, so both are seeked to a fixed anchor address (0)90 instead, which neither command ever consults. The pipe backend is91 unaffected on both.92- **Rename / retype locals & args**: `UPDATE locals SET name='…'` or93 `SET type='…'`, keyed on `func_addr` + the current `name`. Locals94 are analysis-derived — no INSERT/DELETE. The type is passed verbatim (r2 takes95 a bare `int` / `char *`; do NOT quote it), validated to `[A-Za-z0-9_.* []]`.96 The pipe-only flavor issues `afvn`/`afvt`; `r2xsql-full` calls radare2's97 rename/retype functions directly instead — same results, measured98 substantially faster on a bulk rename/retype workload. On the in-process99 (libr) backend, whenever that direct call is unavailable, the command-path100 fallback (`afvn`/`afvt`, address-scoped like `funcs`'s writes) also uses101 the same structural command-dispatch path — never evaluating r2's own102 command separators at all, as a second, independent layer beneath the103 name/type validation above. The pipe backend is unaffected: it always uses104 the plain command form.105- **Comments** are passed as `base64:` so spaces, quotes, and `@` are stored106 verbatim and can't inject extra r2 commands, on the pipe-only flavor.107 `r2xsql-full` sets/updates a comment by calling radare2's own comment-write108 function directly instead of building that command — no encoding needed for109 that call — while reproducing one existing behavior byte-for-byte: an110 UPDATE whose new text is already contained inside the EXISTING comment is a111 silent no-op on both flavors (this is radare2's own long-standing112 dedup-on-write behavior, not something either flavor invented). On the113 in-process (libr) backend, comment writes that DO fall back to the command114 form also use a structural command-dispatch path that never evaluates r2's115 own command separators at all, as a second, independent layer beneath the116 encoding above.117- **Reading `comments`** on the in-process (libr) backend reads radare2's118 meta store directly instead of parsing `CCj`'s JSON — same rows, faster,119 unrelated to the write-path note above. `comments.type` always reads back120 the literal string `CCu` (radare2's own comment-type tag, not a description121 of the comment); `comments.text` reads back with a newline/tab/quote/122 backslash or non-ASCII byte escaped (e.g. a real newline as the two123 characters `\n`) — this is radare2's own command-path behavior on both124 backends, not something either producer invents.125- **Names must be `[A-Za-z0-9._$]`** (no spaces / r2 metacharacters) — enforced.126- `bookmarks` writes are scoped to the `bookmarks` flagspace (saved/selected/127 restored around each write); `id` is synthetic and ignored.128129## Persistence functions130131```sql132SELECT r2xsql_project_save('triage1'); -- Ps: save the session as a project133SELECT name FROM projects; -- Plj: list saved projects134SELECT r2xsql_project_open('triage1'); -- P: load a project mid-session135SELECT r2xsql_type_define('struct hdr { int magic; int size; }'); -- td136```137138## Common operations139140```sql141-- rename a function (the dummy fcn.<addr> names)142UPDATE funcs SET name = 'aes_decrypt' WHERE addr = 0x401000;143144-- rename / retype a local variable or argument (keyed on func_addr + name)145UPDATE locals SET name = 'key_len' WHERE func_addr = 0x401000 AND name = 'var_20h';146UPDATE locals SET type = 'int' WHERE func_addr = 0x401000 AND name = 'key_len';147148-- add / edit / remove a comment149INSERT INTO comments(addr, text) VALUES (0x401000, 'crypto init');150UPDATE comments SET text = 'crypto init v2' WHERE addr = 0x401000;151DELETE FROM comments WHERE addr = 0x401000;152153-- add a flag / remove one154INSERT INTO flags(addr, name) VALUES (0x401000, 'aes_sbox');155DELETE FROM flags WHERE addr = 0x401000;156157-- bookmarks (bookmarks flagspace)158INSERT INTO bookmarks (addr, name) VALUES (0x401000, 'loop_start');159UPDATE bookmarks SET name = 'loop_head' WHERE addr = 0x401000;160DELETE FROM bookmarks WHERE addr = 0x401000;161```162163## Bulk operations164165`UPDATE` / `DELETE` happily take any SQL predicate, so bulk operations166just work:167168```sql169-- mark every function whose name contains 'init' as analyzed170UPDATE comments SET text = 'TODO: review init logic'171WHERE addr IN (SELECT addr FROM funcs WHERE name LIKE '%init%');172173-- delete every comment in a specific code section174DELETE FROM comments175WHERE addr IN (176 SELECT addr FROM funcs177 WHERE addr BETWEEN 0x401000 AND 0x402000178);179```180181## Caveats182183- `comments`, `flags`, and `bookmarks` support `INSERT`. Use `INSERT`184 when adding a new address/name pair; `UPDATE` only affects rows that185 already match the predicate.186- Changes are sequenced — r2xsql issues one r2 command per affected187 row. For thousands of rows this is slow; chunk the predicate.