When to use
Pick this skill for non-code facts about the binary:
- printable strings the binary embeds (
strings)
- mapped bytes and initialization state, or overwriting one byte directly (
bytes)
- defined data symbols/strings (
data_items)
- byte-pattern matches (
byte_search)
- encoding an assembly mnemonic to bytes (
assemble), or finding where an
assembled instruction occurs in the binary (search_asm)
- demangling a C++/Rust/Swift/MSVC symbol name (
demangle)
- reading a null-terminated string at an ARBITRARY address, not just an
address
strings' own detection heuristics flagged (read_cstr)
- staging, committing, or undoing a revertible raw hex byte patch, either
from raw hex or an assembly mnemonic (
patches)
- functions the binary imports from other libraries (
imports)
- loader entry points and functions/symbols the binary exports (
entries)
- the binary's relocation table (
relocs)
- the binary's linked/imported library list (
libs)
- top-level binary metadata: arch, bits, bintype, OS, … (
binary)
- type definitions and members loaded by r2 (
types, types_members —
see the connect schema-catalog reference for the full column list)
For code structure (functions/blocks/instructions) use disassembly.
For cross-references between code and data, use xrefs.
Tables
| table |
source |
columns |
bytes |
omj + p8 + iSj |
addr, value, is_initialized — writable (value only) |
data_items |
isj + izj |
addr, name, data_type, size, value_repr, segment_name, is_string, is_initialized |
byte_search |
/xj + p8 |
addr, matched_hex, matched_bytes, size; hidden pattern/bounds |
assemble |
pa |
hex, bytes, size, error; hidden asm (required), addr |
search_asm |
/a |
addr, matched_hex; hidden asm (required), max_results |
demangle |
iD/iDj |
demangled, error; hidden mangled (required), lang (optional) |
read_cstr |
pszj |
content, length, section, type, truncated, error; hidden addr (required), max_len (optional, default 128) |
patches |
wcj (write-cache) |
addr, size, original_bytes, patched_bytes, asm_text, committed — writable |
strings |
izj |
addr, length, section, type, content, paddr |
imports |
iij |
addr, ordinal, bind, type, name, module |
entries |
iej + iEj |
addr, size, type, bind, name |
relocs |
irj |
addr, paddr, type, ntype, name, demname, sym_vaddr, is_ifunc |
libs |
ilj |
name |
binary |
iIj + quick-counts |
key, value (key-value pairs) |
types |
tk* + tks |
ordinal (read-only), name, kind, size, format |
types_members |
tk* + tkj |
type/member names, kinds, layout, sizes, values |
Common queries
-- find every string containing "password" (case-insensitive)
SELECT addr, content FROM strings WHERE content LIKE '%password%';
-- ...and where to patch it on disk: addr is the VIRTUAL address, paddr the
-- file offset. They differ by the image base, so do not use one for the other.
SELECT printf('0x%x', addr) AS vaddr, paddr, content
FROM strings WHERE content LIKE '%password%';
-- binary type / arch / bits at a glance
SELECT key, value FROM binary
WHERE key IN ('bintype','arch','bits','os','class','endian');
-- imports grouped by library
SELECT module, COUNT(*) AS n
FROM imports
GROUP BY module ORDER BY n DESC;
-- crypto API surface
SELECT name, module FROM imports
WHERE name LIKE 'Crypt%' OR name LIKE '%AES%'
OR name LIKE '%RSA%' OR name LIKE 'BCrypt%';
-- network / IO surface
SELECT name, module FROM imports
WHERE module IN ('WS2_32.dll','WINHTTP.dll','WININET.dll','URLMON.dll');
-- entry points / exports (`entries`)
SELECT name, addr, type FROM entries ORDER BY name;
-- linked libraries
SELECT name FROM libs ORDER BY name;
-- relocations against a named import
SELECT printf('0x%x', addr) AS at, name, type FROM relocs WHERE name IS NOT NULL;
-- readable initialized bytes in a range
SELECT addr, value FROM bytes
WHERE addr >= 0x401000 AND addr < 0x401100 AND is_initialized = 1;
-- overwrite one byte directly and permanently (NOT staged -- see caveats)
UPDATE bytes SET value = 0x90 WHERE addr = 0x401000;
-- byte pattern (hex and `.` wildcard nibbles; optional `:mask`)
SELECT addr, matched_hex FROM byte_search
WHERE pattern = '4889' AND max_results = 20;
-- search_first(pat) recipe: first match only. max_results = 1 (not just
-- LIMIT 1) skips byte_search's own N+1 full-match re-read for every hit
-- after the first -- LIMIT 1 alone would still pay that cost for every hit,
-- since the generator collects the whole result set before any row is
-- truncated (see the caveat below).
SELECT addr, matched_hex, size FROM byte_search
WHERE pattern = '4889' AND max_results = 1;
-- assemble a mnemonic to bytes (encoded relative to addr, for pc-relative
-- instructions like short branches / rip-relative loads -- omit addr for a
-- context-free encoding, which defaults to address 0)
SELECT hex, size FROM assemble WHERE asm = 'mov eax, 1';
SELECT hex FROM assemble WHERE asm = 'jmp 0x401010' AND addr = 0x401000;
-- assemble-then-search: every site where this exact instruction occurs
SELECT printf('0x%x', addr) AS at, matched_hex FROM search_asm
WHERE asm = 'push rbp' AND max_results = 20;
-- demangle a symbol name (lang is optional -- radare2 auto-detects from the
-- mangled text's own prefix regardless of what lang says, see caveats)
SELECT demangled FROM demangle WHERE mangled = '_ZN3Foo3barEv';
SELECT demangled, error FROM demangle WHERE mangled = '?foo@@YAHXZ' AND lang = 'msvc';
-- read_cstr(addr): a null-terminated string at an ARBITRARY address, not
-- just wherever `strings`' own detection heuristics already flagged one --
-- e.g. mid-string, or an address the heuristics never picked up.
SELECT content, length FROM read_cstr WHERE addr = 0x1400173f6;
SELECT content, truncated FROM read_cstr WHERE addr = 0x401000 AND max_len = 512;
-- name_at(addr) / section_at(addr) / string_at(addr): all three resolve to
-- a plain WHERE against an existing table -- no dedicated table needed.
SELECT name, namespace FROM names WHERE addr = 0x140001d70; -- name_at
SELECT name FROM sections -- section_at
WHERE start_addr <= 0x140001d70 AND end_addr > 0x140001d70;
SELECT content FROM strings WHERE addr = 0x1400173f0; -- string_at
-- biggest strings
SELECT addr, length, type, content
FROM strings ORDER BY length DESC LIMIT 20;
-- stage a 4-byte NOP patch, then commit it
INSERT INTO patches(addr, patched_bytes) VALUES (0x401234, '90909090');
UPDATE patches SET committed = 1 WHERE addr = 0x401234;
-- see everything still staged but not yet on disk
SELECT addr, patched_bytes FROM patches WHERE committed = 0;
-- patch_asm(addr, asm) recipe: assemble-then-write in one INSERT (asm_text
-- is assembled AT addr, so pc-relative encodings land correctly) -- exactly
-- one of patched_bytes/asm_text is required, never both
INSERT INTO patches(addr, asm_text) VALUES (0x401234, 'nop');
UPDATE patches SET committed = 1 WHERE addr = 0x401234;
Bootstrap recipe
SELECT key, value FROM binary ORDER BY key;
binary includes the raw iIj keys plus the func_count,
string_count, import_count, section_count quick-counts r2xsql
computes at session start, plus radare2_version (the LIVE running engine's
own version from ?Vj — distinct from r2xsql_version/tool_version,
which are r2xsql's own build-time identity). A db_info-style key/value
metadata summary needs no dedicated table: binary plus
bininfo already carry every field it would have, and every *_count is just
SELECT COUNT(*) FROM <table>.
Performance
strings, imports, and entries all read straight from radare2's
in-process binary state on the in-process build (r_bin_get_strings,
RBinImport, RBinAddr/RBinSymbol respectively) instead of running
izj/iij/iej+iEj and parsing the JSON — identical rows either way,
including imports.name's demangling and entries.name staying the
ORIGINAL (never demangled) name on both build flavors. None of these three
needs an analysis pass: they read the loaded binary's own metadata.
relocs/libs are each already ONE full-scan command (irj/ilj) with no
per-function/per-address loop to collapse, so both stay command-path-only on
every build flavor — no C-API producer exists or is needed. Neither needs an
analysis pass either: both read the loaded binary's own bin-info directly.
Caveats
strings.type distinguishes ascii, utf16le, utf8, … — filter
on it if you only want printable ASCII.
imports.addr is the IAT slot, not the resolved external. Use the
xrefs skill to find code that calls through that slot.
entries combines loader entry points and exports; executables can therefore
have rows even when they export no symbols.
bytes.value is NULL for uninitialized or unreadable mapped bytes.
UPDATE bytes SET value = X WHERE addr = Y is a direct, immediate,
permanent one-byte overwrite (wx) — this is NOT the same mechanism as
patches. It bypasses io.cache entirely (disabling it for the write,
then restoring whatever it was before, even mid-flight around an unrelated
patches row) so it can never be staged, undone, or interfere with a
patches entry. There is no undo; use patches instead if you want a
revertible change. value must be 0-255 (rejected otherwise before any
command runs), and only a currently-initialized address can be written — a
BSS/unbacked address is refused up front. No INSERT/DELETE; addr/
is_initialized stay read-only.
relocs has no addend column: the underlying C struct carries one, but
radare2's own irj never emits it as a JSON field, so there is nothing
for this table to read it from.
relocs.name/demname/sym_vaddr are each independently nullable —
sym_vaddr is populated only when the relocation resolves through a
genuine symbol rather than an import.
libs is a single-column table (name) — ilj carries no ordinal,
load-order index, or resolved path to surface.
patches accepts EITHER raw hex (patched_bytes) OR an assembly mnemonic
(asm_text, assembled via the same mechanism as the assemble table) on
INSERT — exactly one of the two, never both, never neither. asm_text
itself is write-only: it is never stored or read back, so SELECT asm_text FROM patches always reads empty, even for the row it just created.
assemble/search_asm/patches.asm_text all charset-validate the
mnemonic text (letters, digits, space, and ,.+-*/[]():%#_$) before
splicing it into a command — real assembly text never needs ;/@/|/
backtick/quotes, so this is enough to make the value injection-safe without
quoting. assemble and patches.asm_text are also address-scoped: the
encoding depends on which address the instruction lands at (pc-relative
branches, rip-relative loads), so pass addr (or the patch's own target
address) rather than relying on whatever the session happens to be seeked
to.
pa (backing assemble) has no JSON output mode and prints nothing to
stdout on a bad mnemonic (radare2's own diagnostic is stderr-only) — a
failed assemble reads back as an empty hex with a populated error, not
as a SQL error.
/a (backing search_asm) similarly has no working JSON mode:
appending j right after /a is NOT this command's JSON flag — it
misroutes to the unrelated /at (search-by-instruction-type) family
instead (verified live; do not try .r2cmd /aj ... expecting a JSON
assemble-search). search_asm parses the plain-text hit lines itself.
demangle's lang is always advisory: radare2 auto-detects the actual
demangler from the mangled text's own prefix (_Z/__Z/?/…) regardless
of what lang says, and an unrecognized lang string does not abort the
call — it just falls through to auto-detection. demangled is empty (with
error populated) whenever radare2 can't demangle the text — including a
plain, unmangled string like main.
read_cstr's max_len = 0 is rejected before any command is issued
(radare2's pszj prints nothing at all for an explicit 0, verified
live). truncated = 1 means only "no NUL found in the scanned window" —
it does NOT by itself distinguish a genuinely longer string from an
unmapped/meaningless address: an out-of-range read still comes back as a
full max_len-byte string of radare2's own unmapped-fill byte, with
section/type both "unknown". Cross-check bytes for a stronger
mapped/unmapped guarantee. name_at/section_at/string_at above are
recipes, not tables — name_at's names lookup can return MORE THAN ONE
row per address (radare2's flags are not single-valued per address); add
AND namespace = '...' to disambiguate.
patches commit is table-wide: UPDATE patches SET committed = 1 WHERE addr = X commits every pending patch in the session, not just the row
at X (radare2's write cache has no selective/ranged commit — its ranged
form is unsafe). SET committed = 0 is rejected; there is no verified
"uncommit" primitive.
DELETE FROM patches WHERE addr = X undoes an uncommitted patch only
when it is the most-recently-staged one — radare2's undo has no
address targeting, it always undoes whatever was staged last. Delete
newer pending patches first if you need to undo an older one.
- INSERT's stage (
wx) is address-scoped, so on the in-process backend it
also uses the same structural command-dispatch path other writable tables'
address-scoped writes use — a second, independent layer beneath the hex
validation above. The commit (wci) and undo (wcu) primitives are fixed
commands with no user input in them at all, so there is nothing there for
that dispatch to add.
1---2name: r2xsql-data3description: Query radare2 data and memory — bytes, defined data, byte patterns, assembling/demangling a mnemonic or symbol name, strings, imports, entry points, relocations, linked libraries, and binary metadata — via r2xsql.4---56## When to use78Pick this skill for **non-code** facts about the binary:910- printable strings the binary embeds (`strings`)11- mapped bytes and initialization state, or overwriting one byte directly (`bytes`)12- defined data symbols/strings (`data_items`)13- byte-pattern matches (`byte_search`)14- encoding an assembly mnemonic to bytes (`assemble`), or finding where an15 assembled instruction occurs in the binary (`search_asm`)16- demangling a C++/Rust/Swift/MSVC symbol name (`demangle`)17- reading a null-terminated string at an ARBITRARY address, not just an18 address `strings`' own detection heuristics flagged (`read_cstr`)19- staging, committing, or undoing a **revertible** raw hex byte patch, either20 from raw hex or an assembly mnemonic (`patches`)21- functions the binary imports from other libraries (`imports`)22- loader entry points and functions/symbols the binary exports (`entries`)23- the binary's relocation table (`relocs`)24- the binary's linked/imported library list (`libs`)25- top-level binary metadata: arch, bits, bintype, OS, … (`binary`)26- type definitions and members loaded by r2 (`types`, `types_members` —27 see the `connect` schema-catalog reference for the full column list)2829For code structure (functions/blocks/instructions) use `disassembly`.30For cross-references between code and data, use `xrefs`.3132## Tables3334| table | source | columns |35|---------------|--------|----------------------------------------------------|36| `bytes` | `omj` + `p8` + `iSj` | `addr`, `value`, `is_initialized` — **writable (`value` only)** |37| `data_items` | `isj` + `izj` | `addr`, `name`, `data_type`, `size`, `value_repr`, `segment_name`, `is_string`, `is_initialized` |38| `byte_search` | `/xj` + `p8` | `addr`, `matched_hex`, `matched_bytes`, `size`; hidden pattern/bounds |39| `assemble` | `pa` | `hex`, `bytes`, `size`, `error`; hidden `asm` (required), `addr` |40| `search_asm` | `/a` | `addr`, `matched_hex`; hidden `asm` (required), `max_results` |41| `demangle` | `iD`/`iDj` | `demangled`, `error`; hidden `mangled` (required), `lang` (optional) |42| `read_cstr` | `pszj` | `content`, `length`, `section`, `type`, `truncated`, `error`; hidden `addr` (required), `max_len` (optional, default 128) |43| `patches` | `wcj` (write-cache) | `addr`, `size`, `original_bytes`, `patched_bytes`, `asm_text`, `committed` — **writable** |44| `strings` | `izj` | `addr`, `length`, `section`, `type`, `content`, `paddr` |45| `imports` | `iij` | `addr`, `ordinal`, `bind`, `type`, `name`, `module` |46| `entries` | `iej` + `iEj` | `addr`, `size`, `type`, `bind`, `name` |47| `relocs` | `irj` | `addr`, `paddr`, `type`, `ntype`, `name`, `demname`, `sym_vaddr`, `is_ifunc` |48| `libs` | `ilj` | `name` |49| `binary` | `iIj` + quick-counts | `key`, `value` (key-value pairs) |50| `types` | `tk*` + `tks` | `ordinal` (read-only), `name`, `kind`, `size`, `format` |51| `types_members` | `tk*` + `tkj` | type/member names, kinds, layout, sizes, values |5253## Common queries5455```sql56-- find every string containing "password" (case-insensitive)57SELECT addr, content FROM strings WHERE content LIKE '%password%';5859-- ...and where to patch it on disk: addr is the VIRTUAL address, paddr the60-- file offset. They differ by the image base, so do not use one for the other.61SELECT printf('0x%x', addr) AS vaddr, paddr, content62FROM strings WHERE content LIKE '%password%';6364-- binary type / arch / bits at a glance65SELECT key, value FROM binary66WHERE key IN ('bintype','arch','bits','os','class','endian');6768-- imports grouped by library69SELECT module, COUNT(*) AS n70FROM imports71GROUP BY module ORDER BY n DESC;7273-- crypto API surface74SELECT name, module FROM imports75WHERE name LIKE 'Crypt%' OR name LIKE '%AES%'76 OR name LIKE '%RSA%' OR name LIKE 'BCrypt%';7778-- network / IO surface79SELECT name, module FROM imports80WHERE module IN ('WS2_32.dll','WINHTTP.dll','WININET.dll','URLMON.dll');8182-- entry points / exports (`entries`)83SELECT name, addr, type FROM entries ORDER BY name;8485-- linked libraries86SELECT name FROM libs ORDER BY name;8788-- relocations against a named import89SELECT printf('0x%x', addr) AS at, name, type FROM relocs WHERE name IS NOT NULL;9091-- readable initialized bytes in a range92SELECT addr, value FROM bytes93WHERE addr >= 0x401000 AND addr < 0x401100 AND is_initialized = 1;9495-- overwrite one byte directly and permanently (NOT staged -- see caveats)96UPDATE bytes SET value = 0x90 WHERE addr = 0x401000;9798-- byte pattern (hex and `.` wildcard nibbles; optional `:mask`)99SELECT addr, matched_hex FROM byte_search100WHERE pattern = '4889' AND max_results = 20;101102-- search_first(pat) recipe: first match only. max_results = 1 (not just103-- LIMIT 1) skips byte_search's own N+1 full-match re-read for every hit104-- after the first -- LIMIT 1 alone would still pay that cost for every hit,105-- since the generator collects the whole result set before any row is106-- truncated (see the caveat below).107SELECT addr, matched_hex, size FROM byte_search108WHERE pattern = '4889' AND max_results = 1;109110-- assemble a mnemonic to bytes (encoded relative to addr, for pc-relative111-- instructions like short branches / rip-relative loads -- omit addr for a112-- context-free encoding, which defaults to address 0)113SELECT hex, size FROM assemble WHERE asm = 'mov eax, 1';114SELECT hex FROM assemble WHERE asm = 'jmp 0x401010' AND addr = 0x401000;115116-- assemble-then-search: every site where this exact instruction occurs117SELECT printf('0x%x', addr) AS at, matched_hex FROM search_asm118WHERE asm = 'push rbp' AND max_results = 20;119120-- demangle a symbol name (lang is optional -- radare2 auto-detects from the121-- mangled text's own prefix regardless of what lang says, see caveats)122SELECT demangled FROM demangle WHERE mangled = '_ZN3Foo3barEv';123SELECT demangled, error FROM demangle WHERE mangled = '?foo@@YAHXZ' AND lang = 'msvc';124125-- read_cstr(addr): a null-terminated string at an ARBITRARY address, not126-- just wherever `strings`' own detection heuristics already flagged one --127-- e.g. mid-string, or an address the heuristics never picked up.128SELECT content, length FROM read_cstr WHERE addr = 0x1400173f6;129SELECT content, truncated FROM read_cstr WHERE addr = 0x401000 AND max_len = 512;130131-- name_at(addr) / section_at(addr) / string_at(addr): all three resolve to132-- a plain WHERE against an existing table -- no dedicated table needed.133SELECT name, namespace FROM names WHERE addr = 0x140001d70; -- name_at134SELECT name FROM sections -- section_at135WHERE start_addr <= 0x140001d70 AND end_addr > 0x140001d70;136SELECT content FROM strings WHERE addr = 0x1400173f0; -- string_at137138-- biggest strings139SELECT addr, length, type, content140FROM strings ORDER BY length DESC LIMIT 20;141142-- stage a 4-byte NOP patch, then commit it143INSERT INTO patches(addr, patched_bytes) VALUES (0x401234, '90909090');144UPDATE patches SET committed = 1 WHERE addr = 0x401234;145146-- see everything still staged but not yet on disk147SELECT addr, patched_bytes FROM patches WHERE committed = 0;148149-- patch_asm(addr, asm) recipe: assemble-then-write in one INSERT (asm_text150-- is assembled AT addr, so pc-relative encodings land correctly) -- exactly151-- one of patched_bytes/asm_text is required, never both152INSERT INTO patches(addr, asm_text) VALUES (0x401234, 'nop');153UPDATE patches SET committed = 1 WHERE addr = 0x401234;154```155156## Bootstrap recipe157158```sql159SELECT key, value FROM binary ORDER BY key;160```161162`binary` includes the raw `iIj` keys plus the `func_count`,163`string_count`, `import_count`, `section_count` quick-counts r2xsql164computes at session start, plus `radare2_version` (the LIVE running engine's165own version from `?Vj` — distinct from `r2xsql_version`/`tool_version`,166which are r2xsql's own build-time identity). A `db_info`-style key/value167metadata summary needs no dedicated table: `binary` plus168`bininfo` already carry every field it would have, and every `*_count` is just169`SELECT COUNT(*) FROM <table>`.170171## Performance172173`strings`, `imports`, and `entries` all read straight from radare2's174in-process binary state on the in-process build (`r_bin_get_strings`,175`RBinImport`, `RBinAddr`/`RBinSymbol` respectively) instead of running176`izj`/`iij`/`iej`+`iEj` and parsing the JSON — identical rows either way,177including `imports.name`'s demangling and `entries.name` staying the178ORIGINAL (never demangled) name on both build flavors. None of these three179needs an analysis pass: they read the loaded binary's own metadata.180181`relocs`/`libs` are each already ONE full-scan command (`irj`/`ilj`) with no182per-function/per-address loop to collapse, so both stay command-path-only on183every build flavor — no C-API producer exists or is needed. Neither needs an184analysis pass either: both read the loaded binary's own bin-info directly.185186## Caveats187188- `strings.type` distinguishes `ascii`, `utf16le`, `utf8`, … — filter189 on it if you only want printable ASCII.190- `imports.addr` is the IAT slot, not the resolved external. Use the191 `xrefs` skill to find code that calls through that slot.192- `entries` combines loader entry points and exports; executables can therefore193 have rows even when they export no symbols.194- `bytes.value` is NULL for uninitialized or unreadable mapped bytes.195- `UPDATE bytes SET value = X WHERE addr = Y` is a **direct, immediate,196 permanent** one-byte overwrite (`wx`) — this is NOT the same mechanism as197 `patches`. It bypasses `io.cache` entirely (disabling it for the write,198 then restoring whatever it was before, even mid-flight around an unrelated199 `patches` row) so it can never be staged, undone, or interfere with a200 `patches` entry. There is no undo; use `patches` instead if you want a201 revertible change. `value` must be 0-255 (rejected otherwise before any202 command runs), and only a currently-initialized address can be written — a203 BSS/unbacked address is refused up front. No INSERT/DELETE; `addr`/204 `is_initialized` stay read-only.205- `relocs` has no `addend` column: the underlying C struct carries one, but206 radare2's own `irj` never emits it as a JSON field, so there is nothing207 for this table to read it from.208- `relocs.name`/`demname`/`sym_vaddr` are each independently nullable —209 `sym_vaddr` is populated only when the relocation resolves through a210 genuine symbol rather than an import.211- `libs` is a single-column table (`name`) — `ilj` carries no ordinal,212 load-order index, or resolved path to surface.213- `patches` accepts EITHER raw hex (`patched_bytes`) OR an assembly mnemonic214 (`asm_text`, assembled via the same mechanism as the `assemble` table) on215 INSERT — exactly one of the two, never both, never neither. `asm_text`216 itself is write-only: it is never stored or read back, so `SELECT asm_text217 FROM patches` always reads empty, even for the row it just created.218- `assemble`/`search_asm`/`patches.asm_text` all charset-validate the219 mnemonic text (letters, digits, space, and `,.+-*/[]():%#_$`) before220 splicing it into a command — real assembly text never needs `;`/`@`/`|`/221 backtick/quotes, so this is enough to make the value injection-safe without222 quoting. `assemble` and `patches.asm_text` are also address-scoped: the223 encoding depends on which address the instruction lands at (pc-relative224 branches, rip-relative loads), so pass `addr` (or the patch's own target225 address) rather than relying on whatever the session happens to be seeked226 to.227- `pa` (backing `assemble`) has **no JSON output mode** and prints nothing to228 stdout on a bad mnemonic (radare2's own diagnostic is stderr-only) — a229 failed assemble reads back as an empty `hex` with a populated `error`, not230 as a SQL error.231- `/a` (backing `search_asm`) similarly has **no working JSON mode**:232 appending `j` right after `/a` is NOT this command's JSON flag — it233 misroutes to the unrelated `/at` (search-by-instruction-**type**) family234 instead (verified live; do not try `.r2cmd /aj ...` expecting a JSON235 assemble-search). `search_asm` parses the plain-text hit lines itself.236- `demangle`'s `lang` is always advisory: radare2 auto-detects the actual237 demangler from the mangled text's own prefix (`_Z`/`__Z`/`?`/…) regardless238 of what `lang` says, and an unrecognized `lang` string does not abort the239 call — it just falls through to auto-detection. `demangled` is empty (with240 `error` populated) whenever radare2 can't demangle the text — including a241 plain, unmangled string like `main`.242- `read_cstr`'s `max_len = 0` is rejected before any command is issued243 (radare2's `pszj` prints nothing at all for an explicit `0`, verified244 live). `truncated = 1` means only "no NUL found in the scanned window" —245 it does NOT by itself distinguish a genuinely longer string from an246 unmapped/meaningless address: an out-of-range read still comes back as a247 full `max_len`-byte string of radare2's own unmapped-fill byte, with248 `section`/`type` both `"unknown"`. Cross-check `bytes` for a stronger249 mapped/unmapped guarantee. `name_at`/`section_at`/`string_at` above are250 recipes, not tables — `name_at`'s `names` lookup can return MORE THAN ONE251 row per address (radare2's flags are not single-valued per address); add252 `AND namespace = '...'` to disambiguate.253- `patches` commit is table-wide: `UPDATE patches SET committed = 1 WHERE254 addr = X` commits **every** pending patch in the session, not just the row255 at `X` (radare2's write cache has no selective/ranged commit — its ranged256 form is unsafe). `SET committed = 0` is rejected; there is no verified257 "uncommit" primitive.258- `DELETE FROM patches WHERE addr = X` undoes an **uncommitted** patch only259 when it is the **most-recently-staged** one — radare2's undo has no260 address targeting, it always undoes whatever was staged last. Delete261 newer pending patches first if you need to undo an older one.262- INSERT's stage (`wx`) is address-scoped, so on the in-process backend it263 also uses the same structural command-dispatch path other writable tables'264 address-scoped writes use — a second, independent layer beneath the hex265 validation above. The commit (`wci`) and undo (`wcu`) primitives are fixed266 commands with no user input in them at all, so there is nothing there for267 that dispatch to add.