10 Commits
Author SHA1 Message Date
Elias Bachaalany 2d76b9964a README: load the plugin with L, and correct three stale lines
- r2 -i <plugin> fails on Windows with radare2 6.2.2, which reads a .dll
  given to -i as a script ('Invalid l subcommand'). The examples now load
  it with -c 'L <path>', which works on every platform
- libxsql is pinned to a release tag (v1.0.21) in all three builds, no
  longer 'a current libxsql'
- the core library links radare2's r_util since r_name_check, so it is no
  longer backend-neutral
- 'may lag; see below' pointed at nothing
2026-10-02 10:18:24 -07:00
Elias Bachaalany d92f9d962d Pin libxsql to v1.0.21 instead of tracking main
This series needs libxsql 1.0.21 (exact_int64 / exact_text /
Int64KeyRange). All three build paths fetched libxsql main, so a later
libxsql change could break a build of this exact source. CMake's
GIT_TAG, the meson wrap's revision and the Makefile's clone now name the
release tag; bump them together.
2026-10-02 17:25:55 +02:00
Elias Bachaalany 2618735bd4 Describe measurements instead of pointing at research outside this repo
Comments pointed at research notes, named by directory and file, that a
reader of this repository cannot open. The measurements are kept; only
the pointers go.
2026-10-02 17:25:55 +02:00
Elias Bachaalany c46ccb42b6 Compare constraint and INSERT values exactly, as native columns would
SQLite hands a virtual table's constraint values over with no column
affinity, and every plan here sets omit=1, so it never re-checks a row.
Reading them with as_int64()/as_c_str() answered wrongly: `addr = 4096.5`
looked up 4096, `pattern = NULL` searched for "", a blob was read as its
bytes. The factories of byte_search, read_cstr, assemble, search_asm,
rop_gadgets, demangle and the instructions start_addr/count window now use
libxsql 1.0.21's exact_int64 / exact_text / Int64KeyRange: a value nothing
can equal yields no rows and issues no command, while '4096' and 4096.0
still mean 4096.

byte_search's window used -1 for "unset", so an address at or above 2^63
(a kernel image; negative in the int64 addr column) silently disabled the
bound. It is now an Int64KeyRange in the column's own signed order.

INSERT values get no affinity either, so `INSERT INTO comments(addr, ...)
VALUES ('main', ...)` wrote at address 0. Integer INSERT arguments of
comments, flags, bookmarks, funcs, patches and io_maps, and the bytes
UPDATE value, are now taken exactly or refused.
2026-10-02 17:25:55 +02:00
Elias Bachaalany 956d60a11a funcs: answer WHERE addr = X with one lookup, compute cost only when read
On radare2 6.2 a join or correlated subquery onto funcs timed out: every
probe rebuilt the whole table, and r_anal_function_cost (which decodes
every instruction) was ~70% of each build.

- funcs: filter_eq on addr -> func_at (libr: r_anal_get_function_at;
  r2pipe: afij @ X, refusing an address inside a function), rowid = addr,
  row_lookup, and key-based mutation snapshots so a multi-row
  UPDATE/DELETE stays correct with the filter in place
- libr: funcs_scan and func_at share fill_func_row, and the pipe path's
  aflj and afij rows share func_row_from_json, so a looked-up row cannot
  drift from a scanned one
- cost is computed only when the query reads that column

Pairs with libxsql 1.0.21, which builds a query-scoped table once per
cursor instead of once per xFilter.
2026-10-02 17:25:55 +02:00
Elias Bachaalany 58cd6ebde4 Fix three radare2 6.2 regressions
- libr data-dir probe: an explicit prefix is probed as given. radare2
  6.2's r_sys_prefix(arg) returns the executable-derived prefix instead
  of arg, so the probe checked the wrong directory and types, syscalls
  and calling conventions came back nearly empty
- relocs: io->cachemode is a bool, so 'cachemode & R_PERM_W' was always
  0 and io.cache was always switched on; test the bool
- bininfo.file is SQL NULL when radare2 reports no file, not ''
2026-10-02 17:25:55 +02:00
Elias Bachaalany eca186fa6e Build with CMake, and against radare2 6.2.2 as well as master
- CMake: the core library's table sources now include <r_util.h> for
  r_name_check, but the r2xsql target had no radare2 include path or
  library, so eight files failed with 'r_util.h: No such file'. Link it
  to Radare2::r_util (the Makefile and meson builds already pass
  radare2's flags to every source)
- r_io_desc_info replaced r_io_desc_is_dbg in radare2 38e23d0018 (ABI
  144); 6.2.2, which CI pins, is ABI 142. Pick by R2_ABIVERSION
- restore json_utils::r2_quote, which the demangle commit removed but
  the types table's td fallback still calls
- the name-rule error messages describe r_name_check's rule
2026-10-02 17:25:55 +02:00
pancake 96f2f8a3c7 Use the r_bin_demangle native api instead of iD command 2026-10-02 17:25:55 +02:00
pancake 64b5e218e3 Fix build 2026-09-30 17:44:11 +02:00
pancake b5316d5e9d Use r_name_check instead of json_utils::valid_flag_name 2026-09-30 06:54:26 +02:00
32 changed files with 535 additions and 361 deletions

No files matched your search

+1 -1
View File
@@ -105,7 +105,7 @@ else()
message(STATUS "r2xsql: fetching libxsql from GitHub")
FetchContent_Declare(libxsql
GIT_REPOSITORY https://github.com/0xeb/libxsql.git
GIT_TAG main
GIT_TAG v1.0.21 # needs 1.0.21: exact_int64 / exact_text / Int64KeyRange
GIT_SHALLOW TRUE)
FetchContent_MakeAvailable(libxsql)
endif()
+4 -3
View File
@@ -35,6 +35,7 @@ BUILD=build-make
# --- libxsql: sibling checkout by default, else fetched into build-make/ ------
LIBXSQL_DIR?=../libxsql
LIBXSQL_URL?=https://github.com/0xeb/libxsql.git
LIBXSQL_REF?=v1.0.21
ifeq ($(wildcard $(LIBXSQL_DIR)/CMakeLists.txt),)
LIBXSQL_DIR:=$(BUILD)/libxsql
endif
@@ -52,8 +53,8 @@ R2XSQL_INC=-I$(CWD)/src/lib/include -I$(CWD)/src/lib/src -I$(CWD)/src
# --- defines ------------------------------------------------------------------
# R2XSQL_HAVE_LIBR=1 -> compile the in-process libr backend (real, not stub).
# XSQL_VERSION_STRING -> libxsql's --version string. Derived from libxsql's own
# CMakeLists at build time so it always follows whatever libxsql main is at —
# no pinned value here (r2xsql tracks libxsql main). Override with
# CMakeLists at build time, so it follows the libxsql checkout in use (the
# pinned LIBXSQL_REF when fetched). Override with
# `make XSQL_VERSION=x` only if you must.
# (pattern has no '(' on purpose: an unbalanced paren inside $(shell ...) makes
# GNU make read it as an unterminated call.)
@@ -91,7 +92,7 @@ all: $(PLUGIN)
# libxsql fetch (only if the sibling is absent and we redirected into build-make)
$(LIBXSQL_DIR)/CMakeLists.txt:
@echo "libxsql not found at ../libxsql -- cloning $(LIBXSQL_URL)"
git clone --depth 1 $(LIBXSQL_URL) $(LIBXSQL_DIR)
git clone --depth 1 --branch $(LIBXSQL_REF) $(LIBXSQL_URL) $(LIBXSQL_DIR)
$(BUILD)/libxsql.mk: $(LIBXSQL_DIR)/CMakeLists.txt
@mkdir -p $(dir $@)
+14 -11
View File
@@ -52,7 +52,7 @@ create empty struct/union/enum types through the corresponding tables.
# any one of these
r2pm -ci radare2 # radare2's own package manager
git clone https://github.com/radareorg/radare2 && radare2/sys/install.sh
sudo apt install radare2 # distro package (may lag; see below)
sudo apt install radare2 # distro package (may lag behind releases)
brew install radare2 # macOS
```
@@ -64,11 +64,13 @@ the in-process plugin depends on radare2's ABI.
Load the built module and analyze a target:
```sh
r2 -i /absolute/path/core_r2xsql.so -A ./target
r2 -A -c 'L /absolute/path/core_r2xsql.so' ./target
```
Use the actual `.so`, `.dylib`, or `.dll` path; omit `-i` once the plugin is
installed in radare2's plugin directory. At the radare2 prompt:
Use the actual `.so`, `.dylib`, or `.dll` path; omit the `L` command once the
plugin is installed in radare2's plugin directory. `-i <plugin>` also works on
Linux and macOS, but radare2 6.2.2 on Windows reads a `.dll` given to `-i` as a
script and fails; `L` works everywhere. At the radare2 prompt:
```text
sql SELECT name, size FROM funcs ORDER BY size DESC LIMIT 10
@@ -79,7 +81,7 @@ q
For a one-shot query:
```sh
r2 -q -A -i /absolute/path/core_r2xsql.so \
r2 -q -A -c 'L /absolute/path/core_r2xsql.so' \
-c '"sql SELECT name, size FROM funcs WHERE size > 100 LIMIT 5"' ./target
```
@@ -123,9 +125,9 @@ default; a missing radare2 installation is a configure error. The plugin is in
`build/bin/` (`build/bin/Release/` for the MSVC Release configuration).
`R2XSQL_BUILD_PLUGIN=OFF` disables the plugin target and
`R2XSQL_BUILD_EXAMPLES=OFF` disables examples. The backend-neutral static
library and r2pipe backend remain available to C++ consumers. The project
also supports being added as a CMake subdirectory.
`R2XSQL_BUILD_EXAMPLES=OFF` disables examples. The static core library (which
links radare2's `r_util`) and the r2pipe backend remain available to C++
consumers. The project also supports being added as a CMake subdirectory.
### Building the in-r2 plugin the radare2 way (Makefile / meson)
@@ -155,7 +157,7 @@ meson setup build-meson -Dr2_user_plugins=true # ... or the per-user dir
Both build the same plugin: the `sql` command. Both
consume [`libxsql`](https://github.com/0xeb/libxsql): the Makefile uses a
sibling `../libxsql` checkout (else clones it), and meson pulls it as a
subproject. A current libxsql is required (same as CMake's `FetchContent`).
subproject. All three builds pin the same libxsql release tag (`v1.0.21`).
### JSON results
@@ -175,12 +177,13 @@ The same command works through r2pipe and radare2's HTTP `/cmd/` endpoint.
Load the plugin in a persistent radare2 session and start r2's web server:
```sh
r2 -N -q -i /absolute/path/core_r2xsql.so \
r2 -N -q -c 'L /absolute/path/core_r2xsql.so' \
-e http.bind=127.0.0.1 -e http.port=9090 -c aa -c '=h' ./target
```
Use the plugin's actual `.so`, `.dylib`, or `.dll` path. If it is installed,
omit `-i`. Send a quoted SQL command through r2's `POST /cmd/` endpoint:
omit the `L` command. Send a quoted SQL command through r2's `POST /cmd/`
endpoint:
```sh
curl --data-binary '"sql SELECT name, size FROM funcs WHERE size > 100 LIMIT 5"' \
+5 -10
View File
@@ -1245,7 +1245,7 @@ its own, the identical trade-off `rop_gadgets` already made for its own
libr).
#### `demangle` (read-only)
One row per query: demangles an ARBITRARY mangled name via `iD`/`iDj`.
One row per query: demangles an ARBITRARY mangled name via `r_bin_demangle`.
Distinct from `funcs.name`/`imports.name`, which already demangle
automatically (gated on `bin.demangle`) for symbols radare2 already knows
from the loaded binary — `demangle` answers the same question for a string
@@ -1269,15 +1269,10 @@ what `lang` says — verified live that an unrecognized `lang` string does not
abort demangling, it just falls through to auto-detection. `mangled` is NOT
charset-restricted the way `assemble`/`search_asm`'s `asm` is: MSVC manglings
use `?`/`@`/`$` throughout, which a disassembly-text allowlist would reject.
Instead this table dispatches through `Backend::cmd_noeval`, seeked to a
fixed anchor (0, unobserved by `iD`'s handler either way — same shape as
`types`' addr-less `td`/`t-` dispatch), falling back to the
`json_utils::r2_quote`-wrapped plain command on every backend that reports
cmd_noeval unsupported. Verified live that the injection risk is real: `iDj
cxx foo; ?e INJECTED` spliced through a plain `Backend::cmd()` call DOES
execute the injected `?e` as a second command, while `cmd_noeval` reaches
`iD`'s argument parser with the whole string, `;` included, treated as
opaque data.
The in-process backend passes the mangled name directly to `r_bin_demangle`,
with the current `RBinFile` and `bin.lang` default. This preserves arbitrary
input without command parsing. A backend without an in-process RBin API
reports an error for this table.
#### Point-lookup recipes: `name_at` / `section_at` / `string_at`
+6 -6
View File
@@ -4,10 +4,9 @@
# This file is licensed under the Human-Origin Source License v1.0.
# See LICENSE.
# libr2xsql — static core library exposing radare2 state as SQLite virtual
# tables. Backend-neutral: it carries the mock + r2pipe backends only and links
# NO radare2, so consumers can link it without importing any r_*.dll. The libr
# backend lives in the r2xsql_libr OBJECT library below, used by
# in-process consumers and the in-r2 plugin.
# tables. It carries the mock + r2pipe backends and uses r_util (name checks
# go through r_name_check); the libr backend lives in the r2xsql_libr OBJECT
# library below, used by in-process consumers and the in-r2 plugin.
add_library(r2xsql STATIC
src/version.cpp
@@ -69,7 +68,8 @@ target_include_directories(r2xsql
${CMAKE_CURRENT_SOURCE_DIR}/src
)
target_link_libraries(r2xsql PUBLIC xsql::xsql)
# r_util for r_name_check, which the table sources call through <r_util.h>.
target_link_libraries(r2xsql PUBLIC xsql::xsql Radare2::r_util)
target_compile_features(r2xsql PUBLIC cxx_std_20)
if(WIN32)
@@ -86,7 +86,7 @@ set_target_properties(r2xsql PROPERTIES POSITION_INDEPENDENT_CODE ON)
# direct_libr.cpp is the C-API side of the src/direct.hpp seam: the row
# producers a ported table uses instead of running a command and parsing its
# JSON. It belongs here rather than in a tables_*.cpp because those are
# compiled into the radare2-free core library above.
# compiled into the core library above, which only uses r_util.
add_library(r2xsql_libr OBJECT
src/backend_libr.cpp
src/direct_libr.cpp
+5 -1
View File
@@ -33,7 +33,11 @@ namespace r2xsql {
// radare2 expects its data files.
LibrDataDirStatus libr_data_dir_status_at(const char* prefix) {
LibrDataDirStatus st;
char* pfx = r_sys_prefix(prefix);
// An explicit prefix is probed as given. Do NOT route it through
// r_sys_prefix(): up to radare2 6.1 that returned its argument, but from
// 6.2 it returns the executable-derived prefix (and exports R2_PREFIX=arg
// as a side effect), so the probe silently checked the wrong directory.
char* pfx = prefix ? strdup(prefix) : r_sys_prefix(nullptr);
if (pfx) {
char* dir = r_file_new(pfx, R2_SDB_FCNSIGN, nullptr); // <prefix>/share/fcnsign
if (dir) {
+27 -13
View File
@@ -26,6 +26,8 @@
// That TU boundary is what forces the row struct out of its table's anonymous
// namespace and into this header: a row struct lives beside its table until the
// table is ported, and moves here when a second TU has to build it.
// The demangle method also uses this boundary so arbitrary symbol text reaches
// RBin directly, without passing through the r2 command parser.
#pragma once
@@ -183,8 +185,8 @@ struct BlockData {
// sliced by `asmop.size` (a SEPARATE ASM-side decode, `r_asm_disassemble`).
// This producer calls only `r_anal_op` (never `r_asm_disassemble` -- that
// call exists purely to build the mnemonic), reading `bytes` from its own
// per-block/per-range read buffer sliced by `op.size` -- `instructions-api-
// lab/FINDINGS.md` measured the two decoders never disagreeing on length or
// per-block/per-range read buffer sliced by `op.size` -- measured, the two decoders never
// disagreed on length or
// on which addresses fail to decode, across five real/generated fixtures,
// but this is a live risk to keep in mind if a future radare2 arch plugin
// ever changes that.
@@ -282,7 +284,7 @@ struct FlagRow {
// `RBinString` at all -- it is resolved per row via `r_bin_get_section_at`;
// `vaddr` is rebased through `r_bin_get_vaddr`, not read off the raw member;
// `type` is rendered through `r_bin_string_type`, not the raw char code.
// Measured in a private lab at 26-86x across a real PE, a large generated
// Measured at 26-86x across a real PE, a large generated
// fixture, and a real large DLL, with exact field parity.
struct StringRow {
int64_t addr = 0;
@@ -302,7 +304,7 @@ struct StringRow {
// (`r_str_srwx_i`), not the raw permission bitmask, and `addr` silently
// downgrades to NO rebase for a section lacking the READ permission bit --
// a real per-row branch on the section's own `perm`, not a fixed `va` mode
// for the whole table. Measured in a private lab at 19-58x (`sections`) and
// for the whole table. Measured at 19-58x (`sections`) and
// 58-195x (`segments`, vacuous on every PE fixture tested -- no PE section
// sets `is_segment`, so this rides `sections`' proven-live shared producer)
// with exact field parity.
@@ -324,7 +326,7 @@ struct SectionRow {
// `RBinInfo::has_lit`. All of this goes through PUBLIC `R_API` calls, never
// a private/static helper -- the producer calls the same functions the
// emitter calls, it does not reimplement demangling or address resolution.
// Measured in a private lab at 10-13x across a real PE, a large generated
// Measured at 10-13x across a real PE, a large generated
// fixture, and a real 1986-import DLL, with exact field parity (the
// genuine C++-demangling branch itself is unexercised by any available
// fixture -- radare2 is a C codebase, so every fixture's imports are
@@ -340,9 +342,8 @@ struct ImportRow {
// One row of `comments` -- the command path's `CCj`, a single full-scan
// command (not an N+1-per-function shape, unlike `funcs`/`xrefs`/`locals`/the
// blocks cluster -- the same "one command already" shape `binary-meta-lab`
// found for `strings`/`sections`/`imports`/`entries`). Measured in a private
// lab at 5-70x with exact field parity, including two quirks found by
// blocks cluster -- the same "one command already" shape measured for
// `strings`/`sections`/`imports`/`entries`). Measured at 5-70x with exact field parity, including two quirks found by
// reading `r_meta_print` (`libr/anal/meta.c`) rather than assumed from the
// struct:
//
@@ -374,7 +375,7 @@ struct CommentRow {
// (exports), merged by address (this project's own `merge_row`, preserved
// exactly on the API leg). `iej`'s JSON never emits `name`/`size`/`bind`
// (confirmed by reading `bin_entry`'s JSON block field-by-field) but DOES
// emit `type` via `r_bin_entry_type_string` -- an early lab probe omitted
// emit `type` via `r_bin_entry_type_string` -- an early probe omitted
// this and showed a real parity failure before the producer was corrected.
// `entries.name` is NOT the demangled name despite living one call away from
// a full demangling pipeline: reading `bin_symbols`' JSON emit block found
@@ -382,7 +383,7 @@ struct CommentRow {
// name), UTF8-escaped -- the demangled form goes to a separate `"demname"`
// key this table never reads. `its_an_export` (`!is_imported && bind ==
// "GLOBAL"`) is a private/static helper in radare2 but trivial to reproduce
// without calling it. Measured in a private lab at 9-78x across a real PE,
// without calling it. Measured at 9-78x across a real PE,
// a large generated fixture, and a real 755-export DLL, with exact
// field parity.
struct EntryRow {
@@ -508,6 +509,11 @@ class Api {
virtual void binary_info(std::vector<BinaryRow>& out) = 0;
virtual std::string file_path() = 0;
virtual std::string engine_version() = 0;
// Demangle an arbitrary symbol using the current RBinFile and bin.lang
// setting. Empty result means radare2 could not demangle the name; error
// receives the same diagnostic as iDj in that case.
virtual std::string demangle(const std::string& mangled,
const std::string& lang, std::string& error) = 0;
virtual bool entry_address(int64_t& out) = 0;
virtual void relocs_scan(std::vector<RelocRow>& out) = 0;
virtual void type_members_scan(std::vector<MemberRow>& out) = 0;
@@ -557,7 +563,15 @@ class Api {
// eight fields are: radare2 builds it per function from the type database,
// and it dominates the cost of this scan. The caller passes false whenever
// the query does not read that column.
virtual void funcs_scan(bool with_prototype, std::vector<FuncRow>& out) = 0;
// `with_cost` gates r_anal_function_cost, which decodes every instruction
// and is most of a prototype-less scan (it left cost 0 when false).
virtual void funcs_scan(bool with_prototype, bool with_cost,
std::vector<FuncRow>& out) = 0;
// `funcs` equality lookup -- the command path's `afij @ <addr>`, restricted
// to a function STARTING at `addr`. False when none does.
virtual bool func_at(int64_t addr, bool with_prototype, bool with_cost,
FuncRow& out) = 0;
// `blocks`/`cfg_edges`/`switch_tables`/`dominators`/`post_dominators`/
// `loops` — ONE function's basic-block list, the command path's
@@ -610,7 +624,7 @@ class Api {
// `fi->{addr,size,name,realname,space->name}` directly -- no `fs`
// selection, no per-space re-listing, no session-state save/restore at
// all, since `r_flag_foreach` is not scoped by the currently-selected
// flagspace the way the `fj` COMMAND is. Measured in a private lab at
// flagspace the way the `fj` COMMAND is. Measured at
// 17-47x on a full scan across a real binary and generated fixtures, plus
// a real large DLL, with exact field parity -- including the pointer-
// equality `realname` quirk -- before being ported.
@@ -652,7 +666,7 @@ class Api {
//
// Unlike every READ producer above (an N+1-per-row scan, where command
// dispatch is paid once per row), a single write is one command either
// way -- there is no N+1 shape to collapse. A private standalone lab
// way -- there is no N+1 shape to collapse. A benchmark
// measured a BULK workload instead (N repeated writes in a loop,
// simulating a bulk-rename/bulk-annotate script) and found each of the
// four operations examined pays a real, per-call fixed cost for
+100 -49
View File
@@ -121,6 +121,29 @@ class LibrApi : public Api {
return 0;
}
std::string demangle(const std::string& mangled,
const std::string& lang, std::string& error) override {
std::lock_guard lock(mu_);
error = "Cannot demangle string";
if (!core_ || !core_->bin) return {};
const char* selected_lang = lang.empty()
? r_config_get(core_->config, "bin.lang") : lang.c_str();
if ((!selected_lang || !*selected_lang) && lang.empty()) {
error = "Set the language in `e bin.lang` or pass a second argument";
}
RBinFile temporary_file = {};
RBinFile* file = core_->bin->cur;
if (!file) {
temporary_file.rbin = core_->bin;
file = &temporary_file;
}
char* result = r_bin_demangle(file, selected_lang, mangled.c_str(), 0, false);
if (!result) return {};
std::string demangled(result);
free(result);
return demangled;
}
bool function_is_start(int64_t addr) override {
std::lock_guard lock(mu_);
return core_ && core_->anal &&
@@ -434,7 +457,11 @@ class LibrApi : public Api {
std::lock_guard lock(mu_);
if (!core_ || !core_->bin) return;
RIODesc* desc = r_io_desc_get(core_->io, r_io_fd_get_current(core_->io));
#if R2_ABIVERSION >= 144
if (desc && r_io_desc_info(desc).isdbg) return;
#else
if (desc && r_io_desc_is_dbg(desc)) return;
#endif
RVecRBinReloc* relocs = r_bin_get_relocs(core_->bin);
// `irj` applies pending relocations when requested, including the IO
// cache/overlay routing used for relocatable object files.
@@ -442,7 +469,9 @@ class LibrApi : public Api {
const bool cache = r_config_get_b(core_->config, "bin.cache");
if ((apply || cache) && core_->bin->cur) {
if (cache) {
if (!(core_->io->cachemode & R_PERM_W)) r_config_set_b(core_->config, "io.cache", true);
// `cachemode` is a bool, so radare2's own `cachemode & R_PERM_W` in
// cbin.c is always 0 and always sets io.cache; test the bool itself.
if (!core_->io->cachemode) r_config_set_b(core_->config, "io.cache", true);
core_->bin->iob.overlay_write_at = apply ? r_io_cache_write_at : r_io_write_at;
}
relocs = r_bin_patch_relocs(core_->bin->cur);
@@ -721,61 +750,83 @@ class LibrApi : public Api {
// what `aflj` prints.
//
// A field-by-field parity test against the command path guards all nine.
void funcs_scan(bool with_prototype, std::vector<FuncRow>& out) override {
void funcs_scan(bool with_prototype, bool with_cost,
std::vector<FuncRow>& out) override {
std::lock_guard lock(mu_);
if (!core_ || !core_->anal) return;
for_each_rlist<RAnalFunction>(core_->anal->fcns, [&](RAnalFunction* fcn) {
FuncRow r;
r.addr = static_cast<int64_t>(fcn->addr);
if (char* name = r_core_anal_fcn_name(core_, fcn)) {
r.name = name;
free(name);
}
r.size = static_cast<int64_t>(r_anal_function_linear_size(fcn));
r.nbbs = static_cast<int64_t>(r_list_length(fcn->bbs));
int ebbs = 0;
r.edges = r_anal_function_count_edges(fcn, &ebbs);
r.cc = r_anal_function_complexity(fcn);
if (const char* t = r_anal_functiontype_tostring(fcn->type)) r.type = t;
// `aflj` omits `calltype` entirely when radare2 has none, which reads
// back as the empty string — so an absent convention must stay empty
// rather than becoming some default.
if (const char* cc = r_anal_function_cc(fcn)) r.calltype = cc;
// The fields `aflj` emits and the table used to drop. Same calls
// `fcn_print_json` makes, in the same order.
r.end_addr = static_cast<int64_t>(r_anal_function_max_addr(fcn));
r.ninstrs = r_anal_function_instrcount(fcn);
r.stackframe = fcn->maxstack;
r.cost = r_anal_function_cost(fcn);
r.tracecov = r_anal_function_coverage(fcn);
r.is_noreturn = fcn->is_noreturn;
r.is_pure = r_anal_function_purity(fcn);
// `recursive` has no single call behind it: radare2 computes it inline
// (a static helper) as "does this function call itself". Same test here.
if (RVecAnalRef* calls = r_core_anal_fcn_get_calls(core_, fcn)) {
RAnalRef* callref;
R_VEC_FOREACH (calls, callref) {
if (callref->addr == fcn->addr) { r.is_recursive = true; break; }
}
RVecAnalRef_free(calls);
}
if (with_prototype) {
// `aflj` fills `signature` by running `afcf`, which is
// r_anal_function_format_sig with the function's own name (the NULL
// default below), then trimming. Note this is a DIFFERENT function
// from r_anal_function_get_signature_string, which formats its own way.
if (char* sig = r_anal_function_format_sig(core_->anal, fcn, nullptr,
nullptr, nullptr, nullptr)) {
r_str_trim(sig);
r.prototype = sig;
free(sig);
}
}
fill_func_row(fcn, with_prototype, with_cost, r);
out.push_back(std::move(r));
});
}
// One function, by its exact START address: the `WHERE addr = X` lookup a
// JOIN or correlated subquery onto `funcs` issues per outer row. An address
// inside a function (not its entry) answers false, as `addr = X` requires.
bool func_at(int64_t addr, bool with_prototype, bool with_cost,
FuncRow& out) override {
std::lock_guard lock(mu_);
if (!core_ || !core_->anal) return false;
RAnalFunction* fcn =
r_anal_get_function_at(core_->anal, static_cast<ut64>(addr));
if (!fcn) return false;
fill_func_row(fcn, with_prototype, with_cost, out);
return true;
}
// Reads one function into a FuncRow -- shared by `funcs_scan` and `func_at`
// so a filtered row and a scanned row can never drift. Caller holds `mu_`.
void fill_func_row(RAnalFunction* fcn, bool with_prototype, bool with_cost,
FuncRow& r) {
r.addr = static_cast<int64_t>(fcn->addr);
if (char* name = r_core_anal_fcn_name(core_, fcn)) {
r.name = name;
free(name);
}
r.size = static_cast<int64_t>(r_anal_function_linear_size(fcn));
r.nbbs = static_cast<int64_t>(r_list_length(fcn->bbs));
int ebbs = 0;
r.edges = r_anal_function_count_edges(fcn, &ebbs);
r.cc = r_anal_function_complexity(fcn);
if (const char* t = r_anal_functiontype_tostring(fcn->type)) r.type = t;
// `aflj` omits `calltype` entirely when radare2 has none, which reads
// back as the empty string — so an absent convention must stay empty
// rather than becoming some default.
if (const char* cc = r_anal_function_cc(fcn)) r.calltype = cc;
// The fields `aflj` emits and the table used to drop. Same calls
// `fcn_print_json` makes, in the same order.
r.end_addr = static_cast<int64_t>(r_anal_function_max_addr(fcn));
r.ninstrs = r_anal_function_instrcount(fcn);
r.stackframe = fcn->maxstack;
if (with_cost) r.cost = r_anal_function_cost(fcn);
r.tracecov = r_anal_function_coverage(fcn);
r.is_noreturn = fcn->is_noreturn;
r.is_pure = r_anal_function_purity(fcn);
// `recursive` has no single call behind it: radare2 computes it inline
// (a static helper) as "does this function call itself". Same test here.
if (RVecAnalRef* calls = r_core_anal_fcn_get_calls(core_, fcn)) {
RAnalRef* callref;
R_VEC_FOREACH (calls, callref) {
if (callref->addr == fcn->addr) { r.is_recursive = true; break; }
}
RVecAnalRef_free(calls);
}
if (with_prototype) {
// `aflj` fills `signature` by running `afcf`, which is
// r_anal_function_format_sig with the function's own name (the NULL
// default below), then trimming. Note this is a DIFFERENT function
// from r_anal_function_get_signature_string, which formats its own way.
if (char* sig = r_anal_function_format_sig(core_->anal, fcn, nullptr,
nullptr, nullptr, nullptr)) {
r_str_trim(sig);
r.prototype = sig;
free(sig);
}
}
}
// Reads one RAnalBlock into BlockData — shared by both `blocks_in_func` and
// `blocks_scan` below so the two can never drift on what a block carries.
static void fill_block(RAnalBlock* b, BlockData& d) {
@@ -1034,7 +1085,7 @@ class LibrApi : public Api {
// `func_addr` stays 0 on every row here). Refills its read buffer from IO
// whenever fewer than 32 bytes remain -- the same low-water threshold the
// command path's own `pdj` loop uses (`disasm.c`, `i >= nb_bytes - 32`,
// confirmed by `pd-range-lab`), since the longest x86 instruction is under
// confirmed by measurement), since the longest x86 instruction is under
// 16 bytes and every other arch this project targets is shorter still.
void instructions_range(int64_t start_addr, int64_t count,
std::vector<InsnRow>& out) override {
+4 -10
View File
@@ -4,6 +4,7 @@
// This file is licensed under the Human-Origin Source License v1.0.
// See LICENSE.
#include "json_utils.hpp"
#include <r_util.h>
#include <cctype>
#include <cstdio>
@@ -56,6 +57,9 @@ bool boolean(const nlohmann::json& obj, std::string_view key, bool dflt) {
return dflt;
}
std::string r2_quote(std::string_view s) {
std::string out;
out.reserve(s.size() + 2);
@@ -73,16 +77,6 @@ std::string r2_quote(std::string_view s) {
return out;
}
bool valid_flag_name(std::string_view name) {
if (name.empty()) return false;
for (unsigned char c : name) {
const bool ok = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
(c >= '0' && c <= '9') || c == '.' || c == '_' || c == '$';
if (!ok) return false;
}
return true;
}
bool valid_type_text(std::string_view type) {
if (type.empty()) return false;
for (unsigned char c : type) {
+6 -7
View File
@@ -31,14 +31,13 @@ int64_t i64_addr(const nlohmann::json& obj, int64_t dflt = 0);
// Quote a string for use as a shell-ish r2 command argument. r2 itself is
// not a shell, but spaces, quotes and backslashes need escaping in CC args.
std::string r2_quote(std::string_view s);
// True iff `name` is safe to splice into an r2 flag command unquoted: a
// non-empty string of [A-Za-z0-9._$] only. This is roughly r2's own flag-name
// charset; rejecting anything else prevents a name with spaces or command
// separators (`;`, `@`, `|`, backtick, …) from breaking parsing or injecting
// extra r2 commands. Use to gate INSERT/UPDATE on the flags/bookmarks tables.
bool valid_flag_name(std::string_view name);
// Quote a string for use as a shell-ish r2 command argument. r2 itself is
// not a shell, but spaces, quotes and backslashes need escaping in CC args.
std::string r2_quote(std::string_view s);
// True iff `type` is a safe C type string to splice into an r2 `afvt` command
// unquoted: a non-empty string of type-syntax characters only —
+18
View File
@@ -15,6 +15,8 @@
#include <string>
#include <string_view>
#include <xsql/vtable.hpp>
#include "direct.hpp"
#include "json_utils.hpp"
@@ -57,6 +59,22 @@ inline std::string command_error_text(const std::string& out) {
return {};
}
// An integer INSERT argument (an address, a size), taken only when it is
// exactly an integer (xsql::exact_int64: 4096, 4096.0 and '4096' all are).
// SQLite applies no column affinity to a virtual table's INSERT values, so
// as_int64() would write 'main' at address 0 and 4096.5 at 4096; this refuses
// both with a vtab error naming `what` instead.
inline bool exact_write_int64(const xsql::FunctionArg& v, std::string_view what,
int64_t& out) {
const auto exact = xsql::exact_int64(v);
if (!exact) {
xsql::set_vtab_error(std::string(what) + " must be an integer");
return false;
}
out = *exact;
return true;
}
inline bool resolve_func_owner(Backend* be, int64_t addr, int64_t& out) {
if (auto* api = be->direct()) return api->function_owner(addr, out);
auto info = be->cmd_json("afij @ " + json_utils::hex_addr(addr));
+1 -1
View File
@@ -59,7 +59,7 @@ void register_class_methods_table(Session& s);
void register_registers_table(Session& s); // static per-arch register profile (arpj)
void register_relocs_table(Session& s); // relocation table (irj)
void register_libs_table(Session& s); // linked library list (ilj)
void register_demangle_table(Session& s); // one-row demangler (iD/iDj)
void register_demangle_table(Session& s); // one-row RBin demangler
void register_assemble_table(Session& s); // one-row assembler (pa)
void register_search_asm_table(Session& s); // assemble-then-search (/a)
void register_read_cstr_table(Session& s); // arbitrary-address cstring read (pszj)
+30 -4
View File
@@ -233,11 +233,24 @@ void register_assemble_table(Session& s) {
-> std::unique_ptr<xsql::Generator<AssembleRow>> {
std::string asm_text;
int64_t addr = 0;
// omit=1: SQLite does not re-check these values, so each is compared the
// way a native column would (xsql::exact_*): a value nothing can equal
// (10.5 for an integer, NULL, a blob) yields no rows.
for (const auto& a : args) {
if (a.op != xsql::ConstraintOp::Eq) continue;
switch (a.column_index) {
case kAAsm: { const char* v = a.value.as_c_str(); if (v) asm_text = v; break; }
case kAAddr: addr = a.value.as_int64(); break;
case kAAsm: {
auto v = xsql::exact_text(a.value);
if (!v) return nullptr;
asm_text = std::move(*v);
break;
}
case kAAddr: {
const auto v = xsql::exact_int64(a.value);
if (!v) return nullptr;
addr = *v;
break;
}
default: break;
}
}
@@ -273,11 +286,24 @@ void register_search_asm_table(Session& s) {
-> std::unique_ptr<xsql::Generator<SearchAsmRow>> {
std::string asm_text;
int64_t max_results = 0;
// omit=1: SQLite does not re-check these values, so each is compared the
// way a native column would (xsql::exact_*): a value nothing can equal
// (10.5 for an integer, NULL, a blob) yields no rows.
for (const auto& a : args) {
if (a.op != xsql::ConstraintOp::Eq) continue;
switch (a.column_index) {
case kSAsm: { const char* v = a.value.as_c_str(); if (v) asm_text = v; break; }
case kSMaxResults: max_results = a.value.as_int64(); break;
case kSAsm: {
auto v = xsql::exact_text(a.value);
if (!v) return nullptr;
asm_text = std::move(*v);
break;
}
case kSMaxResults: {
const auto v = xsql::exact_int64(a.value);
if (!v) return nullptr;
max_results = *v;
break;
}
default: break;
}
}
+3 -1
View File
@@ -86,7 +86,9 @@ void register_bininfo_table(Session& s) {
r.has_pi = opt_bool01(info, "pic");
r.default_cc = opt_str(info, "cc");
r.file = table_sources::file_path(be);
// No file is SQL NULL, not '' -- the same "unknown reads NULL" contract
// the havecode-gated columns keep.
if (auto path = table_sources::file_path(be); !path.empty()) r.file = std::move(path);
rows.push_back(std::move(r));
})
+7 -5
View File
@@ -39,6 +39,7 @@
#include <r2xsql/backend.hpp>
#include <xsql/vtable.hpp>
#include <r_util.h>
#include "tables.hpp"
#include "json_utils.hpp"
#include "table_utils.hpp"
@@ -126,9 +127,9 @@ void register_bookmarks_table(Session& s) {
[](const BookmarkRow& r) { return r.name; },
[be = s.backend_ptr()](BookmarkRow& r, const char* v) -> bool {
if (!v || !*v) return false;
if (!json_utils::valid_flag_name(v)) {
if (!r_name_check(v)) {
xsql::set_vtab_error(
"bookmarks: name must be [A-Za-z0-9._$] (no spaces or r2 metacharacters)");
"bookmarks: name must start with a letter, '_' or ':' and use only [A-Za-z0-9._:]");
return false;
}
if (r.name == v) return true;
@@ -174,15 +175,16 @@ void register_bookmarks_table(Session& s) {
xsql::set_vtab_error("bookmarks: INSERT requires addr and name");
return false;
}
const int64_t addr = argv[1].as_int64();
int64_t addr = 0;
if (!table_utils::exact_write_int64(argv[1], "bookmarks: addr", addr)) return false;
const char* name = argv[2].as_c_str();
if (!name || !*name) {
xsql::set_vtab_error("bookmarks: INSERT requires a non-empty name");
return false;
}
if (!json_utils::valid_flag_name(name)) {
if (!r_name_check(name)) {
xsql::set_vtab_error(
"bookmarks: name must be [A-Za-z0-9._$] (no spaces or r2 metacharacters)");
"bookmarks: name must start with a letter, '_' or ':' and use only [A-Za-z0-9._:]");
return false;
}
BookmarkSpaceScope scope(*be);
+43 -34
View File
@@ -10,7 +10,6 @@
// client-side so no global r2 search config is mutated.
#include <cstdint>
#include <limits>
#include <memory>
#include <string>
#include <vector>
@@ -158,14 +157,13 @@ class ByteSearchGen : public xsql::Generator<ByteSearchRow> {
public:
ByteSearchGen(Backend* be, const std::string& command_token,
size_t pattern_size,
int64_t start, int64_t end, int64_t max_results) {
const xsql::Int64KeyRange& window, int64_t max_results) {
if (command_token.empty() || pattern_size == 0) return;
auto hits = be->cmd_json("/xj " + command_token);
if (!hits.is_array()) return;
for (auto& h : hits) {
const int64_t addr = json_utils::i64(h, "addr");
if (start >= 0 && addr < start) continue;
if (end >= 0 && addr >= end) continue;
if (!window.contains(addr)) continue;
ByteSearchRow r;
r.addr = addr;
r.matched_bytes = full_match_bytes(be, h, addr, pattern_size);
@@ -189,48 +187,59 @@ void register_byte_search_table(Session& s) {
auto make_gen = [be = s.backend_ptr()](
const std::vector<xsql::GeneratorConstraintArg>& args)
-> std::unique_ptr<xsql::Generator<ByteSearchRow>> {
// omit=1 on every constraint below: SQLite does not re-check these
// values, so each is compared the way a native column would
// (xsql::exact_* / Int64KeyRange). A value nothing can equal (10.5 for an
// integer, NULL, a blob) yields no rows rather than a truncated search.
//
// The window is in the addr column's own order -- signed int64, the way
// SQLite sorts it -- so a ut64 address at or above 2^63 (reported
// negative) is bounded like any other, never mistaken for "unset".
// start_addr is inclusive, end_addr exclusive.
std::string pattern;
int64_t start = -1, end = -1, max_results = 0;
xsql::Int64KeyRange window;
int64_t max_results = 0;
for (const auto& a : args) {
if (a.op != xsql::ConstraintOp::Eq) {
// addr range predicates (Ge/Gt lower, Lt/Le upper) tighten bounds.
if (a.column_index == kAddr) {
const int64_t v = a.value.as_int64();
if (a.op == xsql::ConstraintOp::Ge) start = (start < 0) ? v : (v > start ? v : start);
else if (a.op == xsql::ConstraintOp::Gt) {
if (v == std::numeric_limits<int64_t>::max()) {
start = v;
end = v;
} else {
int64_t w = v + 1;
start = (start < 0) ? w : (w > start ? w : start);
}
}
else if (a.op == xsql::ConstraintOp::Lt) end = (end < 0) ? v : (v < end ? v : end);
else if (a.op == xsql::ConstraintOp::Le &&
v != std::numeric_limits<int64_t>::max()) {
int64_t w = v + 1;
end = (end < 0) ? w : (w < end ? w : end);
}
}
continue;
}
switch (a.column_index) {
case kPattern: { const char* p = a.value.as_c_str(); if (p) pattern = p; break; }
case kStart: start = a.value.as_int64(); break;
case kEnd: end = a.value.as_int64(); break;
case kMaxResults: max_results = a.value.as_int64(); break;
case kAddr:
window.constrain(a.op, a.value);
break;
case kPattern: {
auto v = xsql::exact_text(a.value);
if (!v) return nullptr;
pattern = std::move(*v);
break;
}
case kStart: {
const auto v = xsql::exact_int64(a.value);
if (!v) return nullptr;
window.constrain(xsql::ConstraintOp::Ge, *v);
break;
}
case kEnd: {
const auto v = xsql::exact_int64(a.value);
if (!v) return nullptr;
window.constrain(xsql::ConstraintOp::Lt, *v);
break;
}
case kMaxResults: {
const auto v = xsql::exact_int64(a.value);
if (!v) return nullptr;
max_results = *v;
break;
}
default: break;
}
}
if (window.empty()) return nullptr;
ParsedPattern parsed = parse_pattern(pattern);
if (!parsed.error.empty()) {
xsql::set_vtab_error(parsed.error);
return std::make_unique<ByteSearchGen>(
be, std::string{}, 0, 0, 0, 0);
be, std::string{}, 0, window, 0);
}
return std::make_unique<ByteSearchGen>(
be, parsed.command_token, parsed.byte_count, start, end, max_results);
be, parsed.command_token, parsed.byte_count, window, max_results);
};
auto def = xsql::generator_table<ByteSearchRow>("byte_search")
+8 -1
View File
@@ -395,7 +395,14 @@ void register_bytes_table(Session& s) {
xsql::set_vtab_error("bytes: UPDATE value cannot be set to NULL");
return false;
}
const int64_t iv = v.as_int64();
// Exact integers only: 65.5 is refused, never truncated to 65.
const auto exact = xsql::exact_int64(v);
if (!exact) {
xsql::set_vtab_error(
"bytes: UPDATE value must be an integer between 0 and 255");
return false;
}
const int64_t iv = *exact;
if (iv < 0 || iv > 255) {
xsql::set_vtab_error(
"bytes: UPDATE value must be an integer between 0 and 255 (got " +
+3 -2
View File
@@ -29,7 +29,7 @@
//
// The WRITE path (this table's `text` UPDATE and INSERT) is ALSO now
// C-API-backed on the libr flavor, a separate, later port from both of the
// above: a private standalone lab measured a bulk workload (N=500 repeated
// above: a benchmark measured a bulk workload (N=500 repeated
// sets, simulating a bulk-annotate script) at 30-40x over the CMD leg
// (`r_core_call_str_at("CCu base64:...")`, the SAME dispatch this table
// already issues), with exact field/behavior parity once the API leg
@@ -145,7 +145,8 @@ void register_comments_table(Session& s) {
xsql::set_vtab_error("comments: INSERT requires addr and text");
return false;
}
const int64_t addr = argv[0].as_int64();
int64_t addr = 0;
if (!table_utils::exact_write_int64(argv[0], "comments: addr", addr)) return false;
const char* text = argv[2].as_c_str();
const std::string body = text ? text : "";
if (auto* api = be->direct()) {
+12 -108
View File
@@ -4,93 +4,25 @@
// This file is licensed under the Human-Origin Source License v1.0.
// See LICENSE.
//
// demangle -- one-row-per-query demangler over radare2's `iD`/`iDj` (verified
// against the CURRENT radare2 source, `libr/core/cmd_info.inc.c`'s
// `cmd_info_demangle` + `libr/bin/demangle.c`'s `r_bin_demangle`/
// `r_bin_demangle_type`, not assumed from a citation). This closes a REAL
// gap: `funcs.name` and `imports.name` already demangle automatically (via
// `r_core_anal_fcn_name`/`r_bin_demangle` respectively, gated on the
// `bin.demangle` config) for symbols radare2 already knows about from the
// loaded binary, but there was no SQL surface to demangle an ARBITRARY
// string that isn't necessarily any table's row.
//
// The `mangled` hidden input is NOT charset-restricted the way
// `assemble`/`search_asm`'s `asm` input is (see tables_assemble.cpp):
// real-world manglings routinely use characters a disassembly-text
// allowlist would reject -- MSVC uses `?`/`@`/`$` throughout
// (`?foo@@YAHXZ`), Rust and Itanium C++ use `$`-escaped substitutions
// heavily. So this table uses the SAME structural no-eval dispatch
// (Backend::cmd_noeval) `types`' `td` already pilots
// for an addr-less command with a genuinely unrestrictable free-text
// argument -- verified live (not assumed) that the injection risk is real:
// `iDj cxx foo; ?e INJECTED` through a plain `Backend::cmd()` splice DOES
// execute the injected `?e` as a second command (radare2's own `;`
// chaining), while `cmd_noeval` reaches `iD`'s single argument parser with
// the whole string, `;` included, treated as opaque data.
//
// `iD`/`iDj` carries no address in its own grammar at all (same shape as
// `td`/`t-`), so this dispatches seeked to a fixed anchor (0, same constant
// choice as `table_utils::kTypeSystemAnchorAddr` -- kept as a small local
// copy rather than reusing that name directly, since this table has nothing
// to do with the type system; the anchor value is arbitrary and unobserved
// by `iD`'s handler either way, verified against `cmd_info_demangle`, which
// reads only its own `lang`/`text` arguments, never `core->addr`).
//
// `lang` is passed through UNVALIDATED against radare2's own known-language
// list: verified live that `r_bin_demangle_type()` returning
// `R_BIN_LANG_NONE` for an unrecognized string does NOT abort the call --
// `cmd_info_demangle` still invokes `r_bin_demangle(bf, lang, text, ...)`,
// which runs its OWN auto-detection from the mangled text's prefix
// (`_Z`/`__Z`/`?`/...) regardless of what `lang` said. So a caller-supplied
// `lang` is always advisory, never validated client-side to match; this
// table simply forwards it and lets radare2 do what it already does.
//
// `iDj`'s own JSON always echoes `lang` back VERBATIM (the input string,
// not radare2's actually-auto-detected type) -- confirmed live with
// `iDj bogus _ZN3Foo3barEv`, which still demangled successfully via
// auto-detection while echoing `"lang":"bogus"`. This table does not
// surface that echo at all (mangled/lang are pure hidden inputs, never
// duplicated as output columns, matching byte_search's own
// pattern-is-hidden-only convention) precisely to avoid presenting that
// potentially-misleading echo as if it were the resolved demangler.
//
// No JSON mode exists for the empty-`text` or unparseable-argument cases
// either -- `iDj` still emits a well-formed `{"lang":...,"mangled":...,
// "error":"..."}` object in every failure mode exercised (empty text,
// non-mangled text, unrecognized lang), so `error` is always populated
// exactly when `demangled` is not, with no separate "malformed command"
// case to handle.
//
// Command-path-only, no C-API producer: a single demangle call has no N+1
// or full-scan cost shape to collapse (same "one full-scan-equivalent
// command, no per-row cost" reasoning `bininfo`/`rop_gadgets` already
// documented for themselves), so both backends share the identical
// command-based implementation and cannot disagree.
// demangle -- one row for an arbitrary mangled name. Demangling always uses
// r_bin_demangle through the in-process direct::Api.
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
#include <nlohmann/json.hpp>
#include <r2xsql/session.hpp>
#include <r2xsql/backend.hpp>
#include <xsql/vtable.hpp>
#include "tables.hpp"
#include "json_utils.hpp"
#include "table_utils.hpp"
#include "direct.hpp"
namespace r2xsql {
namespace {
// Arbitrary, unobserved by `iD`'s handler (see file header) -- kept as a
// small local constant rather than reusing table_utils::kTypeSystemAnchorAddr
// so this table reads as self-contained, not borrowed from the type system.
constexpr int64_t kDemangleAnchorAddr = 0;
struct DemangleRow {
std::string demangled; // empty iff demangling failed (never a legitimate
// empty symbol name)
@@ -99,18 +31,6 @@ struct DemangleRow {
enum Col { kDemangled = 0, kError = 1, kMangled = 2, kLang = 3 };
// Seeked-to-a-fixed-anchor no-eval dispatch for `iDj`, mirroring
// table_utils::dispatch_type_write's shape exactly (try cmd_noeval first,
// fall back to the r2_quote-wrapped plain command on every backend that
// reports it unsupported) -- a local copy, not a shared call, since this
// table's anchor and command shape are its own (see file header).
std::string dispatch_demangle(Backend* be, const std::string& noeval_command,
const std::string& fallback_command) {
std::string out;
if (be->cmd_noeval(kDemangleAnchorAddr, noeval_command, &out)) return out;
return be->cmd(fallback_command);
}
DemangleRow run_demangle(Backend* be, const std::string& mangled,
const std::string& lang) {
DemangleRow row;
@@ -118,31 +38,13 @@ DemangleRow run_demangle(Backend* be, const std::string& mangled,
row.error = "demangle: mangled must not be empty";
return row;
}
// Two-arg form when lang is given ("iDj <lang> <mangled>"), single-arg
// form otherwise ("iDj <mangled>", radare2 defaults lang from its own
// bin.lang config) -- matches iD's own grammar (see file header); this
// table never synthesizes a placeholder lang token to keep the shapes
// separate, since args="" lang would misparse a mangled name containing
// its own leading space.
const std::string plain =
lang.empty() ? ("iDj " + mangled) : ("iDj " + lang + " " + mangled);
const std::string out = dispatch_demangle(be, plain, json_utils::r2_quote(plain));
nlohmann::json j = nlohmann::json::parse(out, nullptr, false);
if (j.is_discarded() || !j.is_object()) {
row.error = "demangle: radare2 returned no result (iDj produced no "
"parseable output)";
auto* api = be->direct();
if (!api) {
row.error = "demangle requires the in-process libr backend";
return row;
}
if (j.contains("error") && j["error"].is_string()) {
row.error = j["error"].get<std::string>();
return row;
}
row.demangled = json_utils::str(j, "demangled");
if (row.demangled.empty()) {
row.error = "demangle: radare2 produced neither a demangled name nor an "
"error (unexpected iDj shape)";
}
row.demangled = api->demangle(mangled, lang, row.error);
if (!row.demangled.empty()) row.error.clear();
return row;
}
@@ -170,8 +72,10 @@ void register_demangle_table(Session& s) {
for (const auto& a : args) {
if (a.op != xsql::ConstraintOp::Eq) continue;
switch (a.column_index) {
case kMangled: { const char* v = a.value.as_c_str(); if (v) mangled = v; break; }
case kLang: { const char* v = a.value.as_c_str(); if (v) lang = v; break; }
// omit=1: compared like a native TEXT column (xsql::exact_text), so a
// NULL or a blob yields no rows rather than an empty-string lookup.
case kMangled: { auto v = xsql::exact_text(a.value); if (!v) return nullptr; mangled = std::move(*v); break; }
case kLang: { auto v = xsql::exact_text(a.value); if (!v) return nullptr; lang = std::move(*v); break; }
default: break;
}
}
+10 -8
View File
@@ -21,7 +21,7 @@
// A full scan is that same enumeration: one `fj` (full dump) + one
// `fs <name>; fj; fs *` PER FLAGSPACE to attribute the real space back onto
// each flag by name. On the libr backend this reads `r_flag_foreach` directly
// instead — measured in a private lab at 17-47x on a full scan across a real
// instead — measured at 17-47x on a full scan across a real
// binary, this project's own scale fixtures, and a real large DLL, with exact
// field parity (including a pointer-equality quirk in radare2's own
// `realname` field), before being ported (see `direct.hpp` for why the second
@@ -61,7 +61,7 @@
// unbuilt and is not needed here.
//
// The rename is ALSO now C-API-backed on the libr flavor, a separate, later
// port from the cmd_noeval rollout above: a private standalone lab
// port from the cmd_noeval rollout above: a benchmark
// measured a bulk workload (N up to 500 repeated renames, simulating a
// bulk-rename script) at 48-62x over the CMD leg (the SAME
// `r_core_call_str_at("fr ...")` dispatch this table already issues), with
@@ -77,6 +77,7 @@
#include <r2xsql/backend.hpp>
#include <xsql/vtable.hpp>
#include <r_util.h>
#include "tables.hpp"
#include "direct.hpp"
#include "json_utils.hpp"
@@ -127,7 +128,7 @@ void build_flags(Backend* be, std::vector<FlagRow>& rows) {
const std::string name = json_utils::str(sp, "name");
// A space name is spliced into a command, so it gets the same charset gate
// as every other name this project sends to radare2.
if (name.empty() || !json_utils::valid_flag_name(name)) continue;
if (name.empty() || !r_name_check(name.c_str())) continue;
if (json_utils::i64(sp, "count") == 0) continue; // nothing to attribute
// One command, not three: selecting a flagspace is session state, so the
@@ -163,9 +164,9 @@ void register_flags_table(Session& s) {
[](const FlagRow& r) { return r.name; },
[be = s.backend_ptr()](FlagRow& r, const char* v) -> bool {
if (!v || !*v) return false;
if (!json_utils::valid_flag_name(v)) {
if (!r_name_check(v)) {
xsql::set_vtab_error(
"flags: name must be [A-Za-z0-9._$] (no spaces or r2 metacharacters)");
"flags: name must start with a letter, '_' or ':' and use only [A-Za-z0-9._:]");
return false;
}
if (r.name == v) return true;
@@ -254,15 +255,16 @@ void register_flags_table(Session& s) {
xsql::set_vtab_error("flags: INSERT requires addr and name");
return false;
}
const int64_t addr = argv[0].as_int64();
int64_t addr = 0;
if (!table_utils::exact_write_int64(argv[0], "flags: addr", addr)) return false;
const char* name = argv[2].as_c_str();
if (!name || !*name) {
xsql::set_vtab_error("flags: INSERT requires a non-empty name");
return false;
}
if (!json_utils::valid_flag_name(name)) {
if (!r_name_check(name)) {
xsql::set_vtab_error(
"flags: name must be [A-Za-z0-9._$] (no spaces or r2 metacharacters)");
"flags: name must start with a letter, '_' or ':' and use only [A-Za-z0-9._:]");
return false;
}
std::string out = dispatch_write(be, addr, std::string("f ") + name);
+136 -42
View File
@@ -12,7 +12,7 @@
// radare2's own command handler, seeked to the target address, called
// directly without evaluating its command-chaining grammar (`;`/`@`/`|`/
// backtick) at all. This is an ADDITIONAL layer alongside this table's
// existing grammar/value validation (`valid_flag_name`, `valid_c_signature`,
// existing grammar/value validation (`r_name_check`, `valid_c_signature`,
// the `calling_convention_exists` check on `calltype`) — none replaces the
// others. On every other backend (r2pipe, mock) cmd_noeval reports
// unsupported and the exact same `cmd()` + spliced `@ <addr>` string this
@@ -20,8 +20,7 @@
// pilot of this dispatch.
//
// `name`'s rename is explicitly REFUSED a C-API port, unlike the sibling
// rename/set writes on `comments`/`flags`/`locals` (measured together in a
// private standalone lab). Not a speed question — `afn`'s bulk-rename
// rename/set writes on `comments`/`flags`/`locals` (measured together). Not a speed question — `afn`'s bulk-rename
// cost clears the same 3x bar the others did. The refusal is PARITY: reading
// `afn`'s actual handler (`__setFunctionName`, `cmd_anal.inc.c`) found it
// does substantially more than `r_anal_function_rename` alone —
@@ -32,12 +31,12 @@
// reads back through `afcf`;
// * it fires the `on_fcn_rename` callback.
// `r_anal_function_rename` touches ONLY the function's own name field and
// hashtable entries — none of the above. The write-path lab proved this
// live, not just from the source read: after an equal-sized bulk rename on
// hashtable entries — none of the above. Measured live, not just
// read from the source: after an equal-sized bulk rename on
// two independent cores, the CMD leg's flag was renamed on 100% of rows and
// the API leg's flag was renamed on 0% — a real, silent desync between
// `funcs.name` and its own backing flag, the exact bug class already fixed
// once this session for `flags.name` (`afn` creating a duplicate flag
// for `flags.name` (`afn` creating a duplicate flag
// instead of renaming in place). Porting this write would require
// reimplementing `__setFunctionName` in full, which is not a "call the C
// API instead" port at all — it is copying that problem in reverse. Stays
@@ -50,12 +49,14 @@
#include <cctype>
#include <string_view>
#include <r_util.h>
#include "tables.hpp"
#include "direct.hpp"
#include "json_utils.hpp"
#include "table_utils.hpp"
#include <cstdint>
#include <memory>
#include <vector>
namespace r2xsql {
@@ -70,45 +71,119 @@ using direct::FuncRow;
// when the query does not read it is worth roughly an order of magnitude.
constexpr uint64_t kColUsedAll = 1ull << 63;
constexpr uint64_t kPrototypeColBit = 1ull << 8;
// Same idea for `cost`: r_anal_function_cost decodes every instruction of
// every function. On radare2 6.2 it is ~70% of a prototype-less scan, and a
// join onto `funcs` (the `callers` view) rescans the table per outer row, so
// paying it there pushed a real binary past the query timeout.
constexpr uint64_t kCostColBit = 1ull << 12;
void build_funcs(Backend* be, std::vector<FuncRow>& rows, bool with_prototype) {
// One `aflj` / `afij` function object -> FuncRow. Both commands emit the same
// per-function object (fcn_print_json), so the scan and the single-function
// lookup share this and cannot drift.
FuncRow func_row_from_json(const nlohmann::json& j, bool with_prototype) {
FuncRow r;
r.addr = json_utils::i64_addr(j);
r.name = json_utils::str(j, "name");
r.size = json_utils::i64(j, "size");
r.nbbs = json_utils::i64(j, "nbbs");
r.edges = json_utils::i64(j, "edges");
r.cc = json_utils::i64(j, "cc");
r.type = json_utils::str(j, "type");
r.calltype = json_utils::str(j, "calltype");
// One `aflj` carries every field, so on this path the signature is already
// paid for whether or not the query wants it.
if (with_prototype) r.prototype = json_utils::str(j, "signature");
r.end_addr = json_utils::i64(j, "maxaddr");
r.ninstrs = json_utils::i64(j, "ninstrs");
r.stackframe = json_utils::i64(j, "stackframe");
r.cost = json_utils::i64(j, "cost");
r.tracecov = json_utils::i64(j, "tracecov");
r.is_noreturn = json_utils::boolean(j, "noreturn");
// `is-pure` is the odd one out twice over: the key is hyphenated, and
// radare2 prints it through r_str_bool, so it arrives as the STRING
// "true"/"false" rather than a JSON boolean. Reading it as a bool yields
// false for every function.
r.is_pure = json_utils::str(j, "is-pure") == "true";
r.is_recursive = json_utils::boolean(j, "recursive");
return r;
}
void build_funcs(Backend* be, std::vector<FuncRow>& rows, bool with_prototype,
bool with_cost) {
if (auto* api = be->direct()) {
api->funcs_scan(with_prototype, rows);
api->funcs_scan(with_prototype, with_cost, rows);
return;
}
auto arr = be->cmd_json("aflj");
if (!arr.is_array()) return;
rows.reserve(arr.size());
for (auto& j : arr) {
FuncRow r;
r.addr = json_utils::i64_addr(j);
r.name = json_utils::str(j, "name");
r.size = json_utils::i64(j, "size");
r.nbbs = json_utils::i64(j, "nbbs");
r.edges = json_utils::i64(j, "edges");
r.cc = json_utils::i64(j, "cc");
r.type = json_utils::str(j, "type");
r.calltype = json_utils::str(j, "calltype");
// One `aflj` carries every field, so on this path the signature is already
// paid for whether or not the query wants it.
if (with_prototype) r.prototype = json_utils::str(j, "signature");
r.end_addr = json_utils::i64(j, "maxaddr");
r.ninstrs = json_utils::i64(j, "ninstrs");
r.stackframe = json_utils::i64(j, "stackframe");
r.cost = json_utils::i64(j, "cost");
r.tracecov = json_utils::i64(j, "tracecov");
r.is_noreturn = json_utils::boolean(j, "noreturn");
// `is-pure` is the odd one out twice over: the key is hyphenated, and
// radare2 prints it through r_str_bool, so it arrives as the STRING
// "true"/"false" rather than a JSON boolean. Reading it as a bool yields
// false for every function.
r.is_pure = json_utils::str(j, "is-pure") == "true";
r.is_recursive = json_utils::boolean(j, "recursive");
rows.push_back(std::move(r));
}
for (auto& j : arr) rows.push_back(func_row_from_json(j, with_prototype));
}
// The function STARTING at `addr`, or false. `afij @ X` resolves the function
// CONTAINING X, so a mid-function X must be refused here: `addr = X` asks for
// an entry point, and SQLite does not re-check a pushed-down constraint.
bool func_at(Backend* be, int64_t addr, FuncRow& out) {
if (auto* api = be->direct()) {
return api->func_at(addr, /*with_prototype=*/true, /*with_cost=*/true, out);
}
auto info = be->cmd_json("afij @ " + json_utils::hex_addr(addr));
const nlohmann::json* func = nullptr;
if (info.is_array() && !info.empty() && info[0].is_object()) {
func = &info[0];
} else if (info.is_object()) {
func = &info;
}
if (!func || json_utils::i64_addr(*func) != addr) return false;
out = func_row_from_json(*func, /*with_prototype=*/true);
return true;
}
// Zero-or-one row for the `addr` equality pushdown. Column order MUST match
// the builder in register_funcs_table: a filtered row has to equal, column for
// column, the same row from a full scan.
class FuncAtIterator : public xsql::RowIterator {
FuncRow row_;
bool have_ = false;
bool started_ = false;
public:
FuncAtIterator(Backend* be, int64_t addr) { have_ = func_at(be, addr, row_); }
bool next() override {
if (started_) { have_ = false; return false; }
started_ = true;
return have_;
}
bool eof() const override { return started_ && !have_; }
int64_t rowid() const override { return row_.addr; }
void column(xsql::FunctionContext& ctx, int col) override {
const FuncRow& r = row_;
switch (col) {
case 0: ctx.result_int64(r.addr); break;
case 1: ctx.result_text(r.name); break;
case 2: ctx.result_int64(r.size); break;
case 3: ctx.result_int64(r.nbbs); break;
case 4: ctx.result_int64(r.edges); break;
case 5: ctx.result_int64(r.cc); break;
case 6: ctx.result_text(r.type); break;
case 7: ctx.result_text(r.calltype); break;
case 8: ctx.result_text(r.prototype); break;
case 9: ctx.result_int64(r.end_addr); break;
case 10: ctx.result_int64(r.ninstrs); break;
case 11: ctx.result_int64(r.stackframe); break;
case 12: ctx.result_int64(r.cost); break;
case 13: ctx.result_int64(r.tracecov); break;
case 14: ctx.result_int64(r.is_noreturn ? 1 : 0); break;
case 15: ctx.result_int64(r.is_pure ? 1 : 0); break;
case 16: ctx.result_int64(r.is_recursive ? 1 : 0); break;
default: ctx.result_null(); break;
}
}
};
std::string compact_signature(std::string_view signature) {
while (!signature.empty() &&
(std::isspace(static_cast<unsigned char>(signature.back())) ||
@@ -202,13 +277,23 @@ void register_funcs_table(Session& s) {
return table_utils::quick_count(be, direct::Collection::Functions, "afl~?", 1000);
})
.cache_builder([be = s.backend_ptr()](std::vector<FuncRow>& rows) {
build_funcs(be, rows, /*with_prototype=*/true);
build_funcs(be, rows, /*with_prototype=*/true, /*with_cost=*/true);
})
.projection_cache_builder([be = s.backend_ptr()](std::vector<FuncRow>& rows,
uint64_t colUsed) {
const bool with_prototype =
(colUsed & kColUsedAll) || (colUsed & kPrototypeColBit);
build_funcs(be, rows, with_prototype);
const bool with_cost =
(colUsed & kColUsedAll) || (colUsed & kCostColBit);
build_funcs(be, rows, with_prototype, with_cost);
})
// Identity is the entry address, not the cache position. That makes a
// multi-row UPDATE/DELETE resolve each row by its own address (through the
// key-based snapshot below) and lets `WHERE addr = X` be a real lookup.
.rowid([](const FuncRow& r) -> int64_t { return r.addr; })
.snapshot_mutations()
.row_lookup([be = s.backend_ptr()](FuncRow& r, int64_t addr) -> bool {
return func_at(be, addr, r);
})
.column_int64("addr", [](const FuncRow& r) { return r.addr; })
// Writable: UPDATE funcs SET name='x' WHERE addr=... renames the function
@@ -218,9 +303,9 @@ void register_funcs_table(Session& s) {
[](const FuncRow& r) { return r.name; },
[be = s.backend_ptr()](FuncRow& r, const char* v) -> bool {
if (!v || !*v) return false;
if (!json_utils::valid_flag_name(v)) {
if (!r_name_check(v)) {
xsql::set_vtab_error(
"funcs: name must be [A-Za-z0-9._$] (no spaces or r2 metacharacters)");
"funcs: name must start with a letter, '_' or ':' and use only [A-Za-z0-9._:]");
return false;
}
std::string out = dispatch_write(be, r.addr, std::string("afn ") + v);
@@ -337,6 +422,14 @@ void register_funcs_table(Session& s) {
.column_int64("is_noreturn", [](const FuncRow& r) -> int64_t { return r.is_noreturn ? 1 : 0; })
.column_int64("is_pure", [](const FuncRow& r) -> int64_t { return r.is_pure ? 1 : 0; })
.column_int64("is_recursive", [](const FuncRow& r) -> int64_t { return r.is_recursive ? 1 : 0; })
// `WHERE addr = X` -- a JOIN probe or a correlated subquery (`NOT EXISTS
// (SELECT 1 FROM funcs f WHERE f.addr = x.from_func)`) -- is one function
// lookup, not a scan. A correlated subquery opens a fresh cursor per
// evaluation, so no cursor-lived cache can help it; this is what does.
.filter_eq("addr",
[be = s.backend_ptr()](int64_t addr) -> std::unique_ptr<xsql::RowIterator> {
return std::make_unique<FuncAtIterator>(be, addr);
}, 1.0, 1.0)
.deletable([be = s.backend_ptr()](FuncRow& r) -> bool {
// Remove the function definition at this address (analysis only).
// `af-` takes the address as a plain trailing argument, not via `@`
@@ -362,7 +455,8 @@ void register_funcs_table(Session& s) {
xsql::set_vtab_error("funcs: INSERT requires addr");
return false;
}
const int64_t addr = argv[0].as_int64();
int64_t addr = 0;
if (!table_utils::exact_write_int64(argv[0], "funcs: addr", addr)) return false;
const std::string at = json_utils::hex_addr(addr);
if (std::string e = table_utils::command_error_text(dispatch_write(be, addr, "af")); !e.empty()) {
xsql::set_vtab_error("funcs: INSERT failed to define a function at " + at + ": " + e);
@@ -371,7 +465,7 @@ void register_funcs_table(Session& s) {
// Optional best-effort naming; a failed afn does not undo the af above.
if (argc > 1 && !argv[1].is_null()) {
const char* name = argv[1].as_c_str();
if (name && *name && json_utils::valid_flag_name(name)) {
if (name && *name && r_name_check(name)) {
(void)dispatch_write(be, addr, std::string("afn ") + name);
}
}
+1 -1
View File
@@ -69,7 +69,7 @@ void build_grep_rows(Backend* be, std::vector<GrepRow>& rows) {
parent, addr});
};
std::vector<direct::FuncRow> funcs;
api->funcs_scan(false, funcs);
api->funcs_scan(false, false, funcs);
for (const auto& r : funcs) add("func", r.addr, r.name);
std::vector<direct::FlagRow> flags;
api->flags_scan(flags, nullptr, true);
+7 -2
View File
@@ -373,8 +373,13 @@ void register_instructions_table(Session& s) {
.parametric_filter_projection({"start_addr", "count"},
[be = s.backend_ptr()](const std::vector<xsql::FunctionArg>& args, uint64_t col_used)
-> std::unique_ptr<xsql::Generator<InsnRow>> {
const int64_t start_addr = args[0].as_int64();
const int64_t count = args[1].as_int64();
// omit=1: exact integers only (xsql::exact_int64); 4096.5 or NULL is
// no rows, never a walk from a truncated address.
const auto start_opt = xsql::exact_int64(args[0]);
const auto count_opt = xsql::exact_int64(args[1]);
if (!start_opt || !count_opt) return nullptr;
const int64_t start_addr = *start_opt;
const int64_t count = *count_opt;
if (!needs_render(col_used)) {
if (auto* api = be->direct()) {
return std::make_unique<InsnsVecGen>(insns_range_api(api, start_addr, count));
+13 -5
View File
@@ -15,6 +15,7 @@
#include <r2xsql/backend.hpp>
#include <xsql/vtable.hpp>
#include <r_util.h>
#include "tables.hpp"
#include "json_utils.hpp"
#include "table_utils.hpp"
@@ -85,8 +86,11 @@ void register_io_maps_table(Session& s) {
xsql::set_vtab_error("io_maps: INSERT requires start_addr and end_addr");
return false;
}
const int64_t start = argv[0].as_int64();
const int64_t end = argv[1].as_int64();
int64_t start = 0, end = 0;
if (!table_utils::exact_write_int64(argv[0], "io_maps: start_addr", start) ||
!table_utils::exact_write_int64(argv[1], "io_maps: end_addr", end)) {
return false;
}
if (end <= start) {
xsql::set_vtab_error("io_maps: end_addr must be > start_addr");
return false;
@@ -99,12 +103,16 @@ void register_io_maps_table(Session& s) {
std::string name;
if (argc > 3 && !argv[3].is_null()) {
name = argv[3].as_c_str();
if (!name.empty() && !json_utils::valid_flag_name(name.c_str())) {
xsql::set_vtab_error("io_maps: name must be [A-Za-z0-9._$] (no spaces)");
if (!name.empty() && !r_name_check(name.c_str())) {
xsql::set_vtab_error("io_maps: name must start with a letter, '_' or ':' and use only [A-Za-z0-9._:]");
return false;
}
}
const int64_t paddr = (argc > 4 && !argv[4].is_null()) ? argv[4].as_int64() : 0;
int64_t paddr = 0;
if (argc > 4 && !argv[4].is_null() &&
!table_utils::exact_write_int64(argv[4], "io_maps: paddr", paddr)) {
return false;
}
std::string out = be->cmd(make_om_command(start, end, paddr, perm, name));
if (std::string e = table_utils::command_error_text(out); !e.empty()) {
xsql::set_vtab_error("io_maps: INSERT failed (check a file is open and "
+9 -8
View File
@@ -13,7 +13,7 @@
//
// A full scan enumerates functions (`aflj`) and pulls each one's vars
// (`afvj @ <addr>`). On the libr backend this reads `fcn->vars` directly
// instead — measured in a private lab at 50-780x on a full scan across a real
// instead — measured at 50-780x on a full scan across a real
// binary and generated fixtures, with exact field parity, before being ported
// (see `direct.hpp` for why the second producer lives in another TU). The
// command path remains the only implementation r2pipe can have, and it also
@@ -26,13 +26,13 @@
//
// The WRITE side (this table's `name` rename and `type` retype) is ALSO now
// C-API-backed on the libr flavor, a separate, later port from the read-side
// scan above: a private standalone lab measured a bulk workload (N
// scan above: a benchmark measured a bulk workload (N
// repeated renames/retypes, simulating a bulk-retype script) at 78-150x over
// the CMD leg. Reading `cmd_anal.inc.c`'s `afvn`/`afvt` handlers found each is
// nothing more than `r_anal_function_get_var_byname` + `r_anal_var_rename`/
// `r_anal_var_set_type` — the SAME calls the API leg makes directly, with no
// other side effect (unlike `funcs.name`'s `afn`, refused for exactly this
// reason on that table) — so parity is exact, proven live in the lab. The
// reason on that table) — so parity is exact, proven live. The
// single-function readback (`local_readback_matches`, below) stays
// command-based on BOTH legs, same as `funcs`'/`flags`' own per-write
// readbacks — this port only moves the WRITE call itself. On every non-libr
@@ -49,7 +49,7 @@
// `dispatch_write(be, addr, command)` helper every sibling write-capable
// table (`comments`, `funcs`, `flags`, `bookmarks`, `types`, `patches`)
// already uses. This is an ADDITIONAL layer alongside this table's existing
// grammar validation (`valid_flag_name`/`valid_type_text`), never a
// grammar validation (`r_name_check`/`valid_type_text`), never a
// replacement for it. On the r2pipe backend, `cmd_noeval` reports unsupported
// and the exact `afvn`/`afvt` command text this table always issued runs
// unchanged.
@@ -64,6 +64,7 @@
#include <r2xsql/backend.hpp>
#include <xsql/vtable.hpp>
#include <r_util.h>
#include "tables.hpp"
#include "direct.hpp"
#include "json_utils.hpp"
@@ -191,12 +192,12 @@ void register_locals_table(Session& s) {
[](const LocalRow& r) { return r.name; },
[be = s.backend_ptr()](LocalRow& r, const char* v) -> bool {
if (!v || !*v) return false;
if (!json_utils::valid_flag_name(v)) {
if (!r_name_check(v)) {
xsql::set_vtab_error(
"locals: name must be [A-Za-z0-9._$] (no spaces or r2 metacharacters)");
"locals: name must start with a letter, '_' or ':' and use only [A-Za-z0-9._:]");
return false;
}
if (!json_utils::valid_flag_name(r.name)) {
if (!r_name_check(r.name.c_str())) {
xsql::set_vtab_error(
"locals: existing name contains r2 command metacharacters");
return false;
@@ -245,7 +246,7 @@ void register_locals_table(Session& s) {
"locals: type contains an r2 metacharacter (@ ; | ` \" $ etc.)");
return false;
}
if (!json_utils::valid_flag_name(r.name)) {
if (!r_name_check(r.name.c_str())) {
xsql::set_vtab_error(
"locals: existing name contains r2 command metacharacters");
return false;
+2 -1
View File
@@ -294,7 +294,8 @@ void register_patches_table(Session& s) {
return false;
}
const int64_t addr = argv[0].as_int64();
int64_t addr = 0;
if (!table_utils::exact_write_int64(argv[0], "patches: addr", addr)) return false;
const bool have_hex = argc > 3 && !argv[3].is_null();
const bool have_asm = argc > 5 && !argv[5].is_null();
if (have_hex == have_asm) {
+2 -1
View File
@@ -11,6 +11,7 @@
#include <r2xsql/backend.hpp>
#include <xsql/vtable.hpp>
#include <r_util.h>
#include "tables.hpp"
#include "direct.hpp"
#include "json_utils.hpp"
@@ -42,7 +43,7 @@ void register_projects_table(Session& s) {
})
.column_text("name", [](const ProjectRow& r) { return r.name; })
.deletable([be = s.backend_ptr()](ProjectRow& r) -> bool {
if (r.name.empty() || !json_utils::valid_flag_name(r.name)) {
if (r.name.empty() || !r_name_check(r.name.c_str())) {
xsql::set_vtab_error("projects: refusing to delete an unsafe/empty project name");
return false;
}
+16 -2
View File
@@ -164,11 +164,25 @@ void register_read_cstr_table(Session& s) {
bool have_addr = false;
int64_t addr = 0;
int64_t max_len = kDefaultMaxLen;
// omit=1: SQLite does not re-check these values, so each is compared the
// way a native column would (xsql::exact_*): a value nothing can equal
// (10.5 for an integer, NULL, a blob) yields no rows.
for (const auto& a : args) {
if (a.op != xsql::ConstraintOp::Eq) continue;
switch (a.column_index) {
case kAddr: addr = a.value.as_int64(); have_addr = true; break;
case kMaxLen: max_len = a.value.as_int64(); break;
case kAddr: {
const auto v = xsql::exact_int64(a.value);
if (!v) return nullptr;
addr = *v;
have_addr = true;
break;
}
case kMaxLen: {
const auto v = xsql::exact_int64(a.value);
if (!v) return nullptr;
max_len = *v;
break;
}
default: break;
}
}
+34 -16
View File
@@ -401,26 +401,35 @@ void register_rop_gadgets_table(Session& s) {
const std::vector<xsql::GeneratorConstraintArg>& args)
-> std::unique_ptr<xsql::Generator<GadgetRow>> {
SearchParams p;
// omit=1: SQLite does not re-check these values, so each is compared the
// way a native column would (xsql::exact_*): a value nothing can equal
// (10.5 for an integer, NULL, a blob) yields no rows.
for (const auto& a : args) {
switch (a.column_index) {
case kGPattern: {
const char* v = a.value.as_c_str();
if (v) p.pattern = v;
auto v = xsql::exact_text(a.value);
if (!v) return nullptr;
p.pattern = std::move(*v);
break;
}
case kGTerminator: {
const char* v = a.value.as_c_str();
if (v && *v) p.terminator = v;
auto v = xsql::exact_text(a.value);
if (!v) return nullptr;
if (!v->empty()) p.terminator = std::move(*v);
break;
}
case kGKlass: {
const char* v = a.value.as_c_str();
if (v) p.klass = v;
auto v = xsql::exact_text(a.value);
if (!v) return nullptr;
p.klass = std::move(*v);
break;
}
case kGMaxResults:
p.max_results = a.value.as_int64();
case kGMaxResults: {
const auto v = xsql::exact_int64(a.value);
if (!v) return nullptr;
p.max_results = *v;
break;
}
default:
break;
}
@@ -455,26 +464,35 @@ void register_rop_gadget_instructions_table(Session& s) {
const std::vector<xsql::GeneratorConstraintArg>& args)
-> std::unique_ptr<xsql::Generator<GadgetInstrRow>> {
SearchParams p;
// omit=1: SQLite does not re-check these values, so each is compared the
// way a native column would (xsql::exact_*): a value nothing can equal
// (10.5 for an integer, NULL, a blob) yields no rows.
for (const auto& a : args) {
switch (a.column_index) {
case kGiPattern: {
const char* v = a.value.as_c_str();
if (v) p.pattern = v;
auto v = xsql::exact_text(a.value);
if (!v) return nullptr;
p.pattern = std::move(*v);
break;
}
case kGiTerminator: {
const char* v = a.value.as_c_str();
if (v && *v) p.terminator = v;
auto v = xsql::exact_text(a.value);
if (!v) return nullptr;
if (!v->empty()) p.terminator = std::move(*v);
break;
}
case kGiKlass: {
const char* v = a.value.as_c_str();
if (v) p.klass = v;
auto v = xsql::exact_text(a.value);
if (!v) return nullptr;
p.klass = std::move(*v);
break;
}
case kGiMaxResults:
p.max_results = a.value.as_int64();
case kGiMaxResults: {
const auto v = xsql::exact_int64(a.value);
if (!v) return nullptr;
p.max_results = *v;
break;
}
default:
break;
}
+3 -2
View File
@@ -55,6 +55,7 @@
#include <r2xsql/backend.hpp>
#include <xsql/vtable.hpp>
#include <r_util.h>
#include "tables.hpp"
#include "direct.hpp"
#include "json_utils.hpp"
@@ -148,7 +149,7 @@ void register_types_table(Session& s) {
// SQL creates empty struct/union/enum shells and deletes types by name.
// Full declarations use radare2's native td command.
.deletable([be = s.backend_ptr()](TypeRow& r) -> bool {
if (r.name.empty() || !json_utils::valid_flag_name(r.name)) {
if (r.name.empty() || !r_name_check(r.name.c_str())) {
xsql::set_vtab_error("types: refusing to delete an unsafe/empty type name");
return false;
}
@@ -177,7 +178,7 @@ void register_types_table(Session& s) {
const char* kind_c = argv[2].as_c_str();
const std::string name = name_c ? name_c : "";
const std::string kind = kind_c ? kind_c : "";
if (name.empty() || !json_utils::valid_flag_name(name)) {
if (name.empty() || !r_name_check(name.c_str())) {
xsql::set_vtab_error("types: refusing to create an unsafe/empty type name");
return false;
}
+4 -5
View File
@@ -2,13 +2,12 @@
# sqlite3/nlohmann). It ships no meson.build of its own (CMake-native),
# so the packagefiles/ overlay below supplies one for the meson build path.
#
# In the monorepo, prefer the editable sibling checkout instead of this fetch by
# pointing meson at it: meson setup build-meson -Dlibxsql:... (or drop a
# subprojects/libxsql/ checkout in place; meson uses a present directory over the
# wrap). For a standalone/public build this fetches the same source CMake does.
# When developing against a local libxsql, prefer that checkout over this fetch:
# drop it in as subprojects/libxsql/ and meson will use the present directory in
# preference to the wrap. Otherwise this fetches the same source CMake does.
[wrap-git]
url = https://github.com/0xeb/libxsql.git
revision = main
revision = v1.0.21
depth = 1
patch_directory = libxsql
+1 -1
View File
@@ -8,7 +8,7 @@ project('libxsql', ['c', 'cpp'],
default_options: ['cpp_std=c++17'])
# Derive libxsql's version from its own CMakeLists so the --version banner
# always follows whatever libxsql main is at — no pinned value here.
# follows the libxsql checkout in use (the wrap's pinned revision when fetched).
fs = import('fs')
libxsql_ver = 'unknown'
foreach line : fs.read('CMakeLists.txt').split('\n')