23 Commits
Author SHA1 Message Date
pancake 5417ccc164 Add source user installs and verify released r2pm binaries 2026-11-06 04:21:27 +01:00
pancake 9df012ee10 Fix release builds and SQL session cleanup 2026-11-06 04:00:53 +01:00
pancake 104599f12a Build fixes for r2-6.2.6 2026-11-06 03:28:33 +01:00
pancake c4b6114464 Check for stale libxsql in the Makefile 2026-10-15 11:06:55 +02:00
Elias Bachaalany 248bcdc83f Document agent workflows and fix Windows runtime DLL placement 2026-10-14 07:52:38 +02:00
Elias Bachaalany 16da52e29f projects: DELETE does not stop on radare2's deletion prompt
With scr.interactive on, P- asks "Confirm project deletion? (Y/n)" on the
console (r_core_project_delete) and waits. A DELETE FROM projects therefore
blocked the statement, and over =h the request with it. The DELETE is the
confirmation: switch scr.interactive off around P- and restore it.
2026-10-11 01:07:35 +02:00
Elias Bachaalany 70184621db xrefs: the full scan reads every reference, like the pushdowns
The libr full scan walked each analysed function's own refs, so it missed
references whose source lies outside every function (a call past a
function's last analysed block). WHERE to_addr = X and WHERE from_addr = X
read radare2's global xref store, so the plans disagreed: on a real 64-bit
PE, to_addr = X returned 157 rows and the filtered scan 148.

Scan the same store (r_anal_refs_get(anal, UT64_MAX), what axj lists) and
resolve from_func per row with the same owner lookup the pushdowns use. A
source address inside two functions is now one row, not one per function.
2026-10-11 01:07:35 +02:00
pancake 46f00dd9a1 Use native metadata APIs and literal SQL command dispatch 2026-10-05 10:27:07 +02:00
pancake 69e959d898 Prevent command expansion in SQL assembly and gadget searches 2026-10-05 10:27:07 +02:00
pancake 7eaaedc12c Remove the obsolete r2pipe backend 2026-10-05 10:27:07 +02:00
pancake 5836f83068 Recommend r2pm and document manual plugin loading 2026-10-03 11:00:56 +02:00
Elias Bachaalany 2d32ad72dc 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-03 11:00:56 +02:00
pancake 25776ca418 Fix plugin installs and name release assets for r2pm 2026-10-03 09:09:12 +02:00
pancake 3f9ae4cfa0 Fix Linux CI libpath under the custom r2prefix 2026-10-03 08:10:06 +02: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
68 changed files with 1699 additions and 2343 deletions

No files matched your search

+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
# Test the public release through r2pm using radare2's official distribution.
set -euo pipefail
mkdir -p build-r2-runtime
base="https://github.com/radareorg/radare2/releases/download/${R2_REF}"
case "$RUNNER_OS" in
Linux)
curl -fsSL -o build-r2-runtime/radare2.deb "$base/radare2_${R2_REF}_amd64.deb"
sudo apt-get update -qq
sudo apt-get install -y ./build-r2-runtime/radare2.deb
;;
Windows)
curl -fsSL -o build-r2-runtime/radare2.zip "$base/radare2-${R2_REF}-w64.zip"
unzip -q build-r2-runtime/radare2.zip -d build-r2-runtime
r2=$(find "$PWD/build-r2-runtime" -name radare2.exe -type f -print -quit)
[[ -n "$r2" ]]
export PATH="$(dirname "$r2"):$PATH"
;;
*) echo "Unsupported runner: $RUNNER_OS" >&2; exit 1 ;;
esac
r2pm -U
r2pm -bi r2xsql
python3 tests/smoke.py "$(command -v radare2)"
r2pm -u r2xsql
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# Exercise the source installation paths used outside the release archives.
set -euo pipefail
export PATH="$PWD/r2prefix/bin:$PATH"
export LD_LIBRARY_PATH="$PWD/r2prefix/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
export DYLD_LIBRARY_PATH="$PWD/r2prefix/lib${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}"
export PKG_CONFIG_PATH="$PWD/r2prefix/lib/pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}"
if [[ "$RUNNER_OS" == Windows ]]; then
cmake --build build --config Release --target user-install
else
make -j2 user-install LIBXSQL_DIR="$PWD/build/_deps/libxsql-src"
fi
python3 tests/smoke.py ./r2prefix/bin/radare2
if [[ "$RUNNER_OS" == Windows ]]; then
cmake --build build --config Release --target user-uninstall
else
make user-uninstall
fi
+32 -75
View File
@@ -1,87 +1,47 @@
#!/usr/bin/env bash
# Stage r2xsql release trees from a completed build:
# dist/plugin/ — extract ON TOP OF a radare2 install prefix (libr drop-in):
# <r2-plugin-dir>/core_r2xsql.* (r2's own plugin search dir)
#
# The plugin's relative path is derived from the installed prefix layout so the
# plugin drops the plugin exactly where this r2 looks for it — Linux uses
# <libdir>/radare2/<ver>/, Windows uses lib/plugins.
# Stage the same plugins/ layout on every platform for r2pm -bi.
set -euo pipefail
PFX="${GITHUB_WORKSPACE}/r2prefix"
# On Windows runners GITHUB_WORKSPACE is a backslash path (D:\a\r2xsql\r2xsql),
# which find/grep/test choke on under Git-Bash. Normalize to a Unix-style path
# (cygpath exists only in Git-Bash; a no-op elsewhere).
if command -v cygpath >/dev/null 2>&1; then
PFX="$(cygpath -u "${GITHUB_WORKSPACE}")/r2prefix"
fi
# Match the r2pm OS/architecture/bits naming used by the package recipe.
case "${RUNNER_OS}-${RUNNER_ARCH}" in
Linux-X64) R2PM_TRIPLET=linux-x86-64; ext=so ;;
Windows-X64) R2PM_TRIPLET=windows-x86-64; ext=dll ;;
macOS-X64) R2PM_TRIPLET=darwin-x86-64; ext=dylib ;;
macOS-ARM64) R2PM_TRIPLET=darwin-arm-64; ext=dylib ;;
*) echo "Unsupported runner: ${RUNNER_OS}-${RUNNER_ARCH}" >&2; exit 1 ;;
esac
# radare2's system plugin dir, RELATIVE to the prefix, derived from the installed
# layout (we do NOT run radare2 — `radare2 -H` drops into r2pipe mode under CI
# pipes). The default meson build links the core plugins statically, so the
# plugin dir often does NOT exist on disk after install (the plugin creates it):
# * Windows (meson): <prefix>/lib/plugins — and there is no
# */radare2/<version> dir at all, so the scan below finds nothing.
# * Unix (meson): <prefix>/<libdir>/radare2/<version> (libdir = lib, lib64,
# or lib/<triplet>), reconstructed from the data-dir <version> and where
# libr_core landed.
# `|| true` keeps an empty find from tripping pipefail+errexit (which would kill
# the script before it prints anything — exactly the Windows symptom).
REL_PLUGIN=""
if [ -d "${PFX}/lib/plugins" ]; then
REL_PLUGIN="lib/plugins"
else
VER_DIR="$(find "${PFX}" -type d -path '*/radare2/*' 2>/dev/null \
| grep -E '/radare2/[0-9][^/]*$' | head -1 || true)"
if [ -n "${VER_DIR}" ]; then
R2_VER="$(basename "${VER_DIR}")"
# Match the import/shared lib (libr_core.* on Unix, r_core.lib on Windows),
# never the header r_core.h.
CORE_LIB="$(find "${PFX}" -maxdepth 4 \
\( -name 'libr_core.*' -o -name 'r_core.lib' -o -name 'r_core.dll.a' \) \
2>/dev/null | head -1 || true)"
if [ -n "${CORE_LIB}" ]; then
REL_LIBDIR="$(dirname "${CORE_LIB}")"
REL_LIBDIR="${REL_LIBDIR#"${PFX}/"}"
REL_PLUGIN="${REL_LIBDIR}/radare2/${R2_VER}"
fi
fi
fi
# Fallback (Windows static-plugin builds expose no versioned dir): radare2 looks
# for system plugins in <prefix>/lib/plugins.
if [ -z "${REL_PLUGIN}" ]; then
REL_PLUGIN="lib/plugins"
fi
echo "radare2 core plugin dir: ${REL_PLUGIN}"
# Helper: copy the first path that exists.
copy_first() {
local dest="$1"; shift
local src
for src in "$@"; do
if [ -f "$src" ]; then cp "$src" "$dest"; return 0; fi
done
echo "ERROR: none of these exist: $*" >&2
return 1
export LD_LIBRARY_PATH="$PWD/r2prefix/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
export DYLD_LIBRARY_PATH="$PWD/r2prefix/lib${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}"
actual_r2=$(./r2prefix/bin/radare2 -NN -H R2_VERSION)
[[ "$actual_r2" == "$R2_REF" ]] || {
echo "Expected radare2 $R2_REF, found $actual_r2" >&2; exit 1;
}
rm -rf dist
mkdir -p "dist/plugin/${REL_PLUGIN}"
mkdir -p dist/plugin/plugins dist/plugin/licenses
cp "build/bin/core_r2xsql.${ext}" dist/plugin/plugins/
cp LICENSE dist/plugin/
libxsql_dir=${LIBXSQL_DIR:-build/_deps/libxsql-src}
cp "$libxsql_dir/LICENSE" dist/plugin/licenses/libxsql.txt
cp "$libxsql_dir/external/nlohmann/LICENSE" dist/plugin/licenses/nlohmann-json.txt
copy_first "dist/plugin/${REL_PLUGIN}/" \
build/bin/core_r2xsql.so build/bin/core_r2xsql.dylib build/bin/core_r2xsql.dll \
build/bin/Release/core_r2xsql.dll build/src/plugin/core_r2xsql.so \
build/src/plugin/Release/core_r2xsql.dll
# Test the packaged binary, automatic discovery and both process exit paths.
R2_LIBR_PLUGINS="$PWD/dist/plugin/plugins" \
python3 tests/smoke.py ./r2prefix/bin/radare2
cat > dist/plugin/README.txt <<EOF
r2xsql — radare2 core plugin
Extract this archive ON TOP OF your matching radare2 install prefix (the
directory that contains bin/radare2[.exe]). It adds:
Built against radare2 ${R2_REF} for ${R2PM_TRIPLET}.
Install with a matching radare2 version and a recent r2pm:
${REL_PLUGIN}/core_r2xsql.* in-r2 plugin
r2pm -U
r2pm -bi r2xsql
Confirm the plugin loaded with Lcj inside radare2.
Or copy plugins/core_r2xsql.${ext} into the directory printed by
radare2 -NN -H R2_USER_PLUGINS (per-user) or R2_LIBR_PLUGINS (system-wide).
Remove that file to uninstall. Rebuild for a different radare2 ABI.
At the radare2 prompt:
aaa
@@ -95,15 +55,12 @@ with double quotes to preserve SQL operators and statement separators.
Analysis is explicit: run aaa or start radare2 with -A.
The plugin also works through radare2's own HTTP server:
radare2 -e http.bind=127.0.0.1 -e http.port=9090 -c aaa -c '=h' <file>
radare2 -e http.bind=127.0.0.1 -e http.sandbox=false -e http.port=9090 -c aaa -c '=h' <file>
curl --data-binary '"sqlj SELECT name FROM funcs LIMIT 5"' http://127.0.0.1:9090/cmd/
Read the command output for errors even when HTTP returns status 200.
Save analysis with Ps NAME before exiting; binary byte writes require -w.
Built against radare2 ${R2_REF}. Rebuild for a different radare2 ABI.
EOF
echo "=== staged tree ==="
printf 'name=r2xsql-%s-%s\n' "$R2_REF" "$R2PM_TRIPLET" >> "$GITHUB_OUTPUT"
find dist -type f | sort
+46 -15
View File
@@ -16,12 +16,9 @@ concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
# radare2 is not a submodule of r2xsql; CI builds it from source to an install
# prefix and points r2xsql at it with -DRadare2_ROOT. R2XSQL_BUILD_FULL=ON
# builds the libr backend and in-r2 plugin. The plugin artifact installs the
# plugin under r2's own plugin dir, on top of a matching radare2 install prefix.
# Build against this radare2 release without changing its version for r2pm.
env:
R2_REF: "6.2.2"
R2_REF: "6.2.6"
jobs:
# ============================================================================
@@ -29,7 +26,8 @@ jobs:
# ============================================================================
build-linux:
name: Build (Linux)
runs-on: ubuntu-latest
# Keep the release's glibc/libstdc++ baseline stable as ubuntu-latest moves.
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -50,7 +48,7 @@ jobs:
git -C r2src fetch --depth 1 origin "$R2_REF"
git -C r2src checkout --detach FETCH_HEAD
meson setup r2src/b r2src --prefix="$GITHUB_WORKSPACE/r2prefix" \
--buildtype=release -Dsdb_cgen=false
--buildtype=release --libdir=lib -Dsdb_cgen=false
ninja -C r2src/b
ninja -C r2src/b install
@@ -66,14 +64,19 @@ jobs:
export LD_LIBRARY_PATH="$GITHUB_WORKSPACE/r2prefix/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
make -C tests R2XSQL_PLUGIN="$GITHUB_WORKSPACE/build/bin/core_r2xsql.so"
- name: Check source user installation
shell: bash
run: bash .github/scripts/check_user_install.sh
- name: Stage artifacts
id: stage
shell: bash
run: bash .github/scripts/stage_artifacts.sh
- name: Upload plugin artifact
uses: actions/upload-artifact@v4
with:
name: r2xsql-linux-plugin
name: ${{ steps.stage.outputs.name }}
path: dist/plugin/
if-no-files-found: error
@@ -129,14 +132,19 @@ jobs:
-DRadare2_ROOT="$GITHUB_WORKSPACE/r2prefix"
cmake --build build --config Release
- name: Check source user installation
shell: bash
run: bash .github/scripts/check_user_install.sh
- name: Stage artifacts
id: stage
shell: bash
run: bash .github/scripts/stage_artifacts.sh
- name: Upload plugin artifact
uses: actions/upload-artifact@v4
with:
name: r2xsql-windows-plugin
name: ${{ steps.stage.outputs.name }}
path: dist/plugin/
if-no-files-found: error
@@ -172,20 +180,30 @@ jobs:
-DRadare2_ROOT="$GITHUB_WORKSPACE/r2prefix"
cmake --build build --config Release
- name: Run r2r regression tests
run: |
export PATH="$GITHUB_WORKSPACE/r2prefix/bin:$PATH"
export DYLD_LIBRARY_PATH="$GITHUB_WORKSPACE/r2prefix/lib${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}"
make -C tests R2XSQL_PLUGIN="$GITHUB_WORKSPACE/build/bin/core_r2xsql.dylib"
- name: Check source user installation
shell: bash
run: bash .github/scripts/check_user_install.sh
- name: Stage artifacts
id: stage
shell: bash
run: bash .github/scripts/stage_artifacts.sh
- name: Upload plugin artifact
uses: actions/upload-artifact@v4
with:
name: r2xsql-macos-plugin
name: ${{ steps.stage.outputs.name }}
path: dist/plugin/
if-no-files-found: error
# ============================================================================
# Release — zip each artifact tree (zip root = the deployable layout) and
# attach. Plugin zips extract on top of a radare2 install prefix.
# Release — zip each artifact with plugins/ at the archive root.
# ============================================================================
publish-release:
name: Publish Release
@@ -213,7 +231,20 @@ jobs:
uses: softprops/action-gh-release@v2
with:
files: |
r2xsql-linux-plugin.zip
r2xsql-windows-plugin.zip
r2xsql-macos-plugin.zip
r2xsql-*.zip
generate_release_notes: true
verify-release:
name: Install release (${{ matrix.os }})
if: startsWith(github.ref, 'refs/tags/')
needs: publish-release
strategy:
fail-fast: false
matrix:
os: [ubuntu-24.04, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Install and test the published r2pm binary
shell: bash
run: bash .github/scripts/check_binary_install.sh
+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()
+38 -14
View File
@@ -23,9 +23,9 @@
# with `make R2=radare2` if only the long name is on PATH.
R2?=r2
R2R?=r2r
R2_LIBEXT=$(shell $(R2) -H R2_LIBEXT)
R2_USER_PLUGINS=$(shell $(R2) -H R2_USER_PLUGINS)
R2_LIBR_PLUGINS=$(shell $(R2) -H R2_LIBR_PLUGINS)
R2_LIBEXT=$(shell $(R2) -NN -H R2_LIBEXT)
R2_USER_PLUGINS=$(shell $(R2) -NN -H R2_USER_PLUGINS)
R2_LIBR_PLUGINS=$(shell $(R2) -NN -H R2_LIBR_PLUGINS)
CXX?=g++
CC?=gcc
@@ -35,10 +35,31 @@ 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
# $(1) is the included makefile stamp, refreshed after a managed checkout.
define check_libxsql
@mkdir -p $(dir $(1))
@current=unknown; expected=unknown; \
if [ -e "$(LIBXSQL_DIR)/.git" ]; then \
current=$$(git -C "$(LIBXSQL_DIR)" rev-parse --verify HEAD 2>/dev/null || echo unknown); \
expected=$$(git -C "$(LIBXSQL_DIR)" rev-parse --verify "$(LIBXSQL_REF)^{commit}" 2>/dev/null); \
fi; \
if [ "$$current" != "$$expected" ]; then \
if [ "$(LIBXSQL_DIR)" = "$(BUILD)/libxsql" ]; then \
git -C "$(LIBXSQL_DIR)" fetch origin tag "$(LIBXSQL_REF)" && \
git -C "$(LIBXSQL_DIR)" checkout "$(LIBXSQL_REF)" && touch $(1) || exit $$?; \
elif [ "$$current" != unknown ]; then \
echo "ERROR: libxsql at $$current, expected $(LIBXSQL_REF). Update $(LIBXSQL_DIR) manually." >&2; \
exit 1; \
fi; \
fi
@test -f $(1) || touch $(1)
endef
# --- radare2 (in-process libr backend) ----------------------------------------
R2_CFLAGS=$(shell pkg-config --cflags r_core)
R2_LDFLAGS=$(shell pkg-config --libs r_core)
@@ -50,15 +71,14 @@ XSQL_INC=-I$(LIBXSQL_DIR)/include \
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.)
XSQL_VERSION = $(shell grep -m1 'libxsql VERSION' "$(LIBXSQL_DIR)/CMakeLists.txt" 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
DEFS=-DR2XSQL_HAVE_LIBR=1 -DXSQL_VERSION_STRING=\"$(XSQL_VERSION)\"
DEFS=-DXSQL_VERSION_STRING=\"$(XSQL_VERSION)\"
SQLITE_DEFS=-DSQLITE_ENABLE_FTS5 -DSQLITE_ENABLE_RTREE -DSQLITE_ENABLE_JSON1 \
-DSQLITE_ENABLE_COLUMN_METADATA
@@ -70,8 +90,7 @@ PLUGIN=$(BUILD)/core_r2xsql.$(R2_LIBEXT)
PLUGDIR?=$(R2_LIBR_PLUGINS)
# --- sources ------------------------------------------------------------------
# All of src/lib/src/*.cpp is the full (libr) plugin set: the 43 core adapters
# plus backend_libr.cpp + direct_libr.cpp (guarded by R2XSQL_HAVE_LIBR).
# Compile the SQL tables and native backend into the plugin.
R2XSQL_SRCS=$(wildcard src/lib/src/*.cpp)
R2XSQL_OBJS=$(patsubst src/lib/src/%.cpp,$(BUILD)/obj/r2xsql/%.o,$(R2XSQL_SRCS))
XSQL_SRCS=$(wildcard $(LIBXSQL_DIR)/src/*.cpp)
@@ -83,19 +102,23 @@ OBJS=$(SQLITE_OBJ) $(XSQL_OBJS) $(R2XSQL_OBJS) $(PLUGIN_OBJ)
all: $(PLUGIN)
-include $(BUILD)/libxsql.mk
# Removing files must not fetch or rebuild dependencies via included makefiles.
ifneq ($(filter-out clean mrproper uninstall user-uninstall,$(or $(MAKECMDGOALS),all)),)
include $(BUILD)/libxsql.mk
-include $(OBJS:.o=.d)
endif
.DELETE_ON_ERROR:
# 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 $@)
@touch $@
# Check before reading sources; a checkout restarts make with the new source list.
.PHONY: libxsql-check
$(BUILD)/libxsql.mk: $(LIBXSQL_DIR)/CMakeLists.txt libxsql-check
$(call check_libxsql,$@)
# Rebuild legacy objects without .d files when this Makefile is updated.
$(BUILD)/obj/sqlite/sqlite3.o: $(LIBXSQL_DIR)/external/sqlite/sqlite3.c Makefile | $(LIBXSQL_DIR)/CMakeLists.txt
@@ -121,6 +144,7 @@ ifneq (,$(wildcard /usr/bin/codesign))
endif
install: $(PLUGIN)
@test -n "$(PLUGDIR)" || { echo "Cannot determine radare2 plugin directory; set PLUGDIR." >&2; exit 1; }
mkdir -p "$(DESTDIR)$(PLUGDIR)"
rm -f "$(DESTDIR)$(PLUGDIR)/core_r2xsql.$(R2_LIBEXT)"
cp -f $(PLUGIN) "$(DESTDIR)$(PLUGDIR)/core_r2xsql.$(R2_LIBEXT)"
+64 -243
View File
@@ -3,276 +3,97 @@
[![ci status](https://github.com/radareorg/r2xsql/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/radareorg/r2xsql/actions/workflows/build.yml?query=branch%3Amain)
SQL interface for [radare2](https://github.com/radareorg/radare2), powered
by [`libxsql`](https://github.com/0xeb/libxsql).
```sql
SELECT name, size FROM funcs WHERE name LIKE 'main%';
SELECT content FROM strings WHERE content LIKE '%password%';
UPDATE comments SET text = 'reviewed' WHERE addr = 0x1234;
```
`r2xsql` exposes radare2's analysis state as live SQL virtual tables: every
query reads native analysis data or the JSON form of the relevant r2 command
(`aflj`, `izj`, `axtj`, …) from an in-process core or a spawned `radare2 -q0`
pipe. Most live tables cache rows only for the duration of a query.
## It's not read-only — and your work saves
Load the plugin in radare2 and mutate analysis through ordinary SQL writes.
Use radare2's `-w` option when writing binary bytes:
```sql
UPDATE funcs SET name = 'parse_header' WHERE addr = 0x401000; -- rename
UPDATE funcs SET prototype = 'int parse_header(char *buf)' WHERE name = 'parse_header';
INSERT INTO comments(addr, text) VALUES (0x401044, 'bounds check missing');
UPDATE locals SET name = 'len', type = 'int' WHERE func_addr = 0x401000 AND name = 'var_8h';
INSERT INTO types(name, kind) VALUES ('Header', 'struct');
INSERT INTO bookmarks(addr, name) VALUES (0x401000, 'entry-of-interest');
```
Writable surfaces: **funcs** (rename, prototype), **comments**, **flags**,
**locals** (rename + retype), **bookmarks**, **types** (create struct/union/
enum), **patches** (byte edits), **io_maps**, **bytes**.
**Persist it.** Save analysis with `Ps NAME` in radare2 and reopen it with
`P NAME`. The plugin uses radare2's native project format; it does not
automatically save on exit.
Project save/open and full type declarations use native radare2 commands:
`Ps NAME`, `P NAME`, and `"td struct Header { int magic; };"`. The former SQL
functions `r2xsql_project_save`, `r2xsql_project_open`, and
`r2xsql_type_define` are removed. SQL can still list/delete projects and
create empty struct/union/enum types through the corresponding tables.
## Requirements
**You need radare2.** r2xsql does not bundle it and does not install it.
```bash
# 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)
brew install radare2 # macOS
```
Build `core_r2xsql` against the same radare2 installation that will load it:
the in-process plugin depends on radare2's ABI.
by [libxsql](https://github.com/0xeb/libxsql). The `core_r2xsql` plugin exposes
the host's live analysis as SQL tables, including writable annotations.
## Using the plugin
Load the built module and analyze a target:
Install radare2 6.2.6, then install the matching release plugin with r2pm:
```sh
r2 -i /absolute/path/core_r2xsql.so -A ./target
r2pm -U
r2pm -bi r2xsql
r2 -A ./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:
To build from source on Linux/macOS, use `r2pm -ci r2xsql` or run
`make user-install` in a checkout. Windows source builds use CMake's
`user-install` target. See [build instructions](docs/development.md).
Installed plugins load automatically. Check `Lcj` and `sql?` at the r2 prompt.
For an uninstalled build, use `r2 -i ./core_r2xsql.so -A ./target`; inside an
existing session use `L ./core_r2xsql.so`. Substitute `.dylib` or `.dll` as
appropriate. The plugin must match the host's radare2 ABI, including iaito's
engine. See [installation details](docs/usage.md#installation).
```text
sql SELECT name, size FROM funcs ORDER BY size DESC LIMIT 10
pdf @ entry0
q
"sql UPDATE funcs SET name='parse_header' WHERE addr=0x401000"
"sqlj SELECT name FROM funcs WHERE size > 100"
```
For a one-shot query:
Use `sql` for a native r2 table, `sql,:csv` for CSV, and `sqlj` for the JSON
envelope and multiple statements. Wrap the whole command in double quotes
to protect SQL operators from r2's parser. See [SQL access](docs/sql.md).
```sh
r2 -q -A -i /absolute/path/core_r2xsql.so \
-c '"sql SELECT name, size FROM funcs WHERE size > 100 LIMIT 5"' ./target
```
## Analyze by asking your agent
Each `sql` command executes one statement. Quote the whole r2 command when SQL
contains r2 operators such as `>` or `|`. Use repeated `-c` options, a radare2
command script, or a persistent session for multiple statements.
Install the [r2xsql skills](https://github.com/radareorg/radare2-skills/tree/main/r2xsql#install)
alongside the plugin, then ask naturally:
`sql` uses radare2's table renderer and honours `cfg.table.format` and
`scr.utf8`. Add a table query immediately after the command, before the SQL:
> Open `target.exe` headlessly and show me the ten largest functions.
>
> Rename that function based on its disassembly and pseudocode.
>
> Save the project beside the executable and keep it open.
The agent uses native radare2 and curl, writes SQL for you, and reuses the
session on follow-ups. Saving preserves a native r2 project; SQL results are
computed from its analysis. See the [workflow guide](docs/usage.md).
## Tandem mode
Share an analysis session from r2's prompt, its visual/panels text interface,
or [iaito](https://github.com/radareorg/iaito). Open and analyze the binary,
check that r2xsql loaded, then enter these commands in the r2 prompt or
iaito's **View > Console**:
```text
sql,:csv SELECT name, size FROM funcs
sql,size/gt/100,:quiet SELECT name, size FROM funcs
sql,?
e http.bind=127.0.0.1
e http.sandbox=false
e http.port=9090
=h&
```
Use `sql,:tsv` for tab-separated output. Native tables have no SQL NULL type;
in particular, `sql,:json` omits NULL and empty-string cells.
Choose a free port and wait for the server banner. Tell your agent:
**Use r2xsql at http://127.0.0.1:9090 on the binary already open here.**
The background server leaves the UI available. Take turns when changing
analysis; use distinct ports for several sessions. See [tandem mode](docs/usage.md#tandem-mode).
## Build
## Fully headless
Linux / macOS:
With an installed plugin, start a fresh binary in a background native process:
```sh
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DRadare2_ROOT=/path/to/radare2/prefix
cmake --build build
```
Windows (MSVC):
```powershell
cmake -S . -B build -G "Visual Studio 17 2022" -A x64 `
-DRadare2_ROOT=C:\path\to\radare2\prefix
cmake --build build --config Release
```
`Radare2_ROOT` is a radare2 **install prefix** containing `include/libr/`
and `lib/`. CMake requires radare2 and builds the libr backend and plugin by
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.
### Building the in-r2 plugin the radare2 way (Makefile / meson)
CMake above builds the library, examples, and plugin, including on Windows. If you
only want the in-r2 plugin — the `core_r2xsql` module that adds the `sql`
command inside radare2 — there is also a radare2-native build that needs no
CMake, matching how radare2 plugins are usually built (plugin paths from
`r2 -H`, radare2 from `pkg-config r_core`):
```bash
make # -> build-make/core_r2xsql.$(r2 -H R2_LIBEXT)
make user-install # install into the per-user plugin dir (no root)
make install # install system-wide (R2_LIBR_PLUGINS)
make test # load it in r2 and run one SQL query
# only `radare2` (not `r2`) on PATH? -> make R2=radare2
```
or with meson (what radare2 itself uses):
```bash
meson setup build-meson
meson compile -C build-meson
meson install -C build-meson # into R2_LIBR_PLUGINS
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`).
### JSON results
Use `sqlj` for programmatic queries and scripts:
```text
"sqlj SELECT NULL AS missing, '' AS empty; SELECT count(*) AS n FROM funcs"
```
It returns libxsql's JSON envelope with per-statement columns, rows, errors,
and timings. SQL NULL is JSON `null`, distinct from the empty string. Quoting
the entire r2 command also preserves statement separators and SQL operators.
The same command works through r2pipe and radare2's HTTP `/cmd/` endpoint.
## HTTP access through radare2
Load the plugin in a persistent radare2 session and start r2's web server:
```sh
r2 -N -q -i /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:
```sh
curl --data-binary '"sql SELECT name, size FROM funcs WHERE size > 100 LIMIT 5"' \
r2 -N -q -A -e scr.html=false -e http.bind=127.0.0.1 \
-e http.sandbox=false -e http.port=9090 -c '=h' ./target \
>r2.stdout.log 2>r2.stderr.log &
curl -d '"sqlj SELECT name,size FROM funcs ORDER BY size DESC LIMIT 10"' \
http://127.0.0.1:9090/cmd/
```
`GET /cmd/<encoded-command>` also works for short queries:
Wait for readiness before querying. r2's HTTP server carries SQL and native
commands; r2xsql has no separate server. Bind to loopback: this endpoint can
change analysis and write files. Reopen a saved project with `r2 -p PROJECT`
without `-A` or a separate binary argument. The [workflow guide](docs/usage.md)
covers Windows launches, saves, reopening, shutdown and HTTP version differences.
```python
from urllib.parse import quote
from urllib.request import urlopen
## Documentation
sql = "SELECT name, size FROM funcs WHERE size > 100 ORDER BY size DESC LIMIT 5"
command = '"sql ' + sql.replace('"', '\\"') + '"'
with urlopen("http://127.0.0.1:9090/cmd/" + quote(command, safe="")) as response:
print(response.read().decode(), end="")
```
Quote the **whole r2 command**, escape any SQL double quotes, then URL-encode
it for GET; POST sends the quoted command as the raw body. URL encoding alone
does not protect SQL operators such as `>` and `|` from r2's command parser.
Prefer POST for longer SQL to avoid r2's request-line length limit.
`sql` executes one statement; `sqlj` executes a script and returns a JSON
envelope. The session persists between requests, including analysis and
comment changes.
Responses are native tables for `sql`, with no added row-count footer, or JSON
for `sqlj`. SQL errors can arrive with HTTP status 200: check `ERROR: ...` in
table output or `success` and per-statement errors in the JSON envelope. The old
`sql.http*` and `/query` interface is removed;
configure the native server with r2's `http.*` settings instead.
## Tests
Run the regression suite with radare2, `r2r`, a C compiler and Python 3 on
`PATH` (Python's standard library is used only for the HTTP transport test):
```sh
make test
```
This builds the plugin and runs the r2r database in `tests/db`. Each test loads
the exact build explicitly, ignoring installed user plugins, and checks its
output; SQL exceptions fail the suite even when radare2 exits successfully.
The fixture is compiled locally, with no downloaded test binaries or pytest.
To test an existing CMake/Meson build, or select matching radare2 tools:
```sh
make -C tests R2XSQL_PLUGIN=/absolute/path/core_r2xsql.so R2=/path/to/r2 R2R=/path/to/r2r
# Only command tests (no Python needed):
make -C tests TESTS=db/cmd
```
`R2R_ARGS` accepts r2r options (default `-L`); for example, `R2R_ARGS=-i` enables
interactive failure review. One small standard-library helper retains real
HTTP GET/POST coverage, including server startup and shutdown; r2r checks its
result alongside the command tests.
The Make build tracks header dependencies. This prevents stale adapters after
C++ interface changes, which could previously cause `std::bad_alloc` during
`make test`. After updating this Makefile, existing objects are rebuilt once
to establish dependency files. For older checkouts, `make clean && make test`
clears those stale objects.
## Backends (internal detail)
The library retains these backends for embedding and testing:
| Backend | Used by | Notes |
|---|---|---|
| `R2PipeBackend` | C++ consumers (`open_r2pipe`) | Spawns and manages `radare2 -q0` over stdio. |
| `LibrBackend` | `core_r2xsql` and C++ consumers (`open_libr`) | Uses radare2 in-process; the plugin wraps its host core. |
| `MockBackend` | embedding/tests | Canned `{command → response}` map, records issued commands — no live radare2. |
The in-process backend reads analysis, flags, symbols, binary metadata, IO maps,
write caches, types, and runtime catalogs through native libr APIs. The command
producers remain available to r2pipe and mock backends. Instruction/operand text
rendering and byte/ROP searches still use radare2's command handlers to preserve
their rendering, search configuration, and hit behavior.
## Skills (Claude Code / Codex)
Agent skills teach Claude Code and Codex to drive `r2xsql`: query and annotate
radare2 analysis through SQL, pick the right backend. They ship in the radare2 community skills catalog,
**[`radare2-skills`](https://github.com/radareorg/radare2-skills)**, as the
`r2xsql/` category alongside `dev/` and `radius2/`. Install a category
with its `make install` (or `python install.py install` for `r2xsql/`, which copies
rather than symlinks so it needs no elevated rights on Windows).
Start with the `connect` skill; its `references/schema-catalog.md` is the
canonical reference for the SQL surface.
- [User workflows and HTTP access](docs/usage.md)
- [SQL queries, formats and annotations](docs/sql.md)
- [Building, packaging and testing](docs/development.md)
- [Agent skills and schema reference](https://github.com/radareorg/radare2-skills/tree/main/r2xsql)
## License and Terms of Use
@@ -282,6 +103,6 @@ You may not maintain a divergent private fork, port, rebrand, clone, API-compati
Permission requests: open a GitHub issue at [radareorg/r2xsql/issues](https://github.com/radareorg/r2xsql/issues).
If r2xsql materially informs a distributed project, preserve the human origin: credit r2xsql and Elias Bachaalany visibly in your README/docs and in About/credits UI when applicable. The license includes an examples/FAQ section for common allowed and permission-required uses. Third-party dependencies (libxsql, radare2 (libr / r2pipe), and their transitive dependencies) remain under their own licenses.
If r2xsql materially informs a distributed project, preserve the human origin: credit r2xsql and Elias Bachaalany visibly in your README/docs and in About/credits UI when applicable. The license includes an examples/FAQ section for common allowed and permission-required uses. Third-party dependencies (libxsql, radare2, and their transitive dependencies) remain under their own licenses.
See the full [Human-Origin Source License v1.0](LICENSE).
+22 -11
View File
@@ -22,7 +22,7 @@
# Radare2::libr (INTERFACE bundle of the lot)
#
# Plus:
# Radare2_EXECUTABLE — path to radare2(.exe) (for the r2pipe backend)
# Radare2_EXECUTABLE — path to radare2(.exe)
set(_r2_search_roots "")
@@ -66,18 +66,28 @@ endif()
if(NOT Radare2_FOUND)
find_package(PkgConfig QUIET)
if(PkgConfig_FOUND)
pkg_check_modules(_R2 QUIET r_core)
pkg_check_modules(_R2 QUIET IMPORTED_TARGET r_core)
if(_R2_FOUND)
set(Radare2_FOUND TRUE)
pkg_get_variable(Radare2_ROOT r_core prefix)
set(Radare2_VERSION "${_R2_VERSION}")
set(Radare2_INCLUDE_DIRS "${_R2_INCLUDE_DIRS}")
set(Radare2_LIBRARY_DIRS "${_R2_LIBRARY_DIRS}")
set(Radare2_LIBRARIES "${_R2_LIBRARIES}")
set(Radare2_LIBRARIES PkgConfig::_R2)
# r_core.pc includes its public dependencies, including r_util. Keep
# pkg-config's link flags and paths instead of guessing a libdir.
foreach(_target libr r_util)
if(NOT TARGET Radare2::${_target})
add_library(Radare2::${_target} INTERFACE IMPORTED)
set_target_properties(Radare2::${_target} PROPERTIES
INTERFACE_LINK_LIBRARIES PkgConfig::_R2)
endif()
endforeach()
endif()
endif()
endif()
if(Radare2_FOUND AND Radare2_ROOT)
if(_r2_resolved_root)
# Normalize separators: on Windows, Git-Bash passes -DRadare2_ROOT as
# "D:\a\...\r2prefix" (backslashes), and file(GLOB) treats backslashes as escapes
# rather than separators, so the libdir glob below matches nothing and we end up
@@ -118,8 +128,8 @@ if(Radare2_FOUND AND Radare2_ROOT)
endif()
set(_r2_libs r_core r_anal r_bin r_util r_io r_cons r_main r_socket
r_reg r_arch r_search r_syscall r_config r_lang r_egg r_debug
r_bp r_crypto r_flag r_fs r_magic r_parse)
r_reg r_arch r_asm r_esil r_search r_syscall r_config r_lang r_egg r_debug
r_bp r_crypto r_muta r_flag r_fs r_magic r_parse)
set(Radare2_LIBRARIES "")
foreach(_lib IN LISTS _r2_libs)
set(_full "")
@@ -148,10 +158,8 @@ if(Radare2_FOUND AND Radare2_ROOT)
# r_core_* symbols. Surface it at configure time instead.
list(LENGTH Radare2_LIBRARIES _r2_nlibs)
message(STATUS "FindRadare2: libdir='${_r2_libdir}', resolved ${_r2_nlibs} libraries")
if(_r2_nlibs EQUAL 0)
message(WARNING "FindRadare2: found radare2 at '${Radare2_ROOT}' but located ZERO "
"import libraries under '${_r2_libdir}'. Full/plugin links will fail "
"with undefined r_core_* symbols.")
if(NOT TARGET Radare2::r_core OR NOT TARGET Radare2::r_util)
set(Radare2_LIBRARIES "")
endif()
if(NOT TARGET Radare2::libr)
@@ -161,6 +169,9 @@ if(Radare2_FOUND AND Radare2_ROOT)
INTERFACE_LINK_LIBRARIES "${Radare2_LIBRARIES}")
endif()
endif()
if(Radare2_FOUND)
find_program(Radare2_EXECUTABLE
NAMES radare2 r2
PATHS "${Radare2_ROOT}/bin"
@@ -172,7 +183,7 @@ endif()
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(Radare2
REQUIRED_VARS Radare2_ROOT Radare2_INCLUDE_DIRS
REQUIRED_VARS Radare2_INCLUDE_DIRS Radare2_LIBRARIES
VERSION_VAR Radare2_VERSION)
mark_as_advanced(Radare2_INCLUDE_DIRS Radare2_LIBRARIES Radare2_EXECUTABLE)
+20
View File
@@ -0,0 +1,20 @@
# SPDX-License-Identifier: LicenseRef-Human-Origin-Source-1.0
# Query at install time so cross builds can still configure without running r2.
if(NOT PLUGIN_DIR)
execute_process(COMMAND "${RADARE2}" -NN -H R2_USER_PLUGINS
RESULT_VARIABLE _status OUTPUT_VARIABLE PLUGIN_DIR
OUTPUT_STRIP_TRAILING_WHITESPACE)
if(NOT _status STREQUAL "0" OR NOT PLUGIN_DIR)
message(FATAL_ERROR "Cannot determine radare2 user plugin directory; set R2XSQL_USER_PLUGINS.")
endif()
endif()
file(TO_CMAKE_PATH "${PLUGIN_DIR}" PLUGIN_DIR)
get_filename_component(_name "${PLUGIN}" NAME)
if(ACTION STREQUAL "user-install")
file(INSTALL "${PLUGIN}" DESTINATION "${PLUGIN_DIR}" TYPE FILE)
elseif(ACTION STREQUAL "user-uninstall")
file(REMOVE "${PLUGIN_DIR}/${_name}")
else()
message(FATAL_ERROR "Unknown install action: ${ACTION}")
endif()
message(STATUS "${ACTION}: ${PLUGIN_DIR}/${_name}")
+150
View File
@@ -0,0 +1,150 @@
# Building and testing r2xsql
## Build
Linux / macOS:
```sh
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DRadare2_ROOT=/path/to/radare2/prefix
cmake --build build
```
Windows (MSVC):
```powershell
cmake -S . -B build -G "Visual Studio 17 2022" -A x64 `
-DRadare2_ROOT=C:\path\to\radare2\prefix
cmake --build build --config Release
cmake --build build --config Release --target user-install
```
`Radare2_ROOT` is a radare2 **install prefix** containing `include/libr/`
and `lib/`. CMake requires radare2 and builds the libr backend and plugin by
default; a missing radare2 installation is a configure error. The plugin is in
`build/bin/` (`build/bin/Release/` for the MSVC Release configuration).
CMake's `user-install` target installs the plugin in the directory reported by
the matching radare2's `-NN -H R2_USER_PLUGINS`, without administrator access.
`user-uninstall` removes it. Override the destination at configure time with
`-DR2XSQL_USER_PLUGINS=/path/to/plugins` if needed. Both targets also work on
Linux/macOS; omit `--config Release` with a single-configuration generator.
`R2XSQL_BUILD_PLUGIN=OFF` disables the plugin target and
`R2XSQL_BUILD_EXAMPLES=OFF` disables examples. C++ consumers can embed the
library with `open_libr` and the `r2xsql_libr` target. The project also supports
being added as a CMake subdirectory.
### Building the in-r2 plugin the radare2 way (Makefile / meson)
CMake above builds the library, examples, and plugin, including on Windows. If you
only want the in-r2 plugin — the `core_r2xsql` module that adds the `sql`
command inside radare2 — there is also a radare2-native build that needs no
CMake, matching how radare2 plugins are usually built (plugin paths from
`r2 -H`, radare2 from `pkg-config r_core`):
```bash
make # -> build-make/core_r2xsql.$(r2 -H R2_LIBEXT)
make user-install # install into the per-user plugin dir (no root)
make install # install system-wide (R2_LIBR_PLUGINS)
make user-uninstall # remove the per-user plugin without building
make uninstall # remove the system-wide plugin without building
make test # run the command and HTTP regression suite
# only `radare2` (not `r2`) on PATH? -> make R2=radare2
```
`make install` and `make uninstall` accept `PLUGDIR` and `DESTDIR` for a
custom plugin directory or staged install. User targets use `R2_USER_PLUGINS`;
override it on the make command line when needed.
Or with meson (what radare2 itself uses):
```bash
meson setup build-meson
meson compile -C build-meson
meson install -C build-meson # into R2_LIBR_PLUGINS
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. All three builds pin the same libxsql release tag (`v1.0.21`).
When published, binary release assets are named `r2xsql-<radare2-version>-<R2PM_TRIPLET>.zip`, for
example `r2xsql-6.2.6-linux-x86-64.zip`. The first release, `v0.0.1`, targets
radare2 **6.2.6** on Linux x86-64, Windows x86-64 and macOS ARM64. The r2pm
package uses `R2PM_VERSION_MAP "6.2.6:v0.0.1"` to select this release.
Each archive contains the plugin in `plugins/` and dependency license notices.
With r2pm 6.2.6 or newer, use `r2pm -U` followed by
`r2pm -bi r2xsql`; uninstall with `r2pm -u r2xsql`. Only matching radare2
versions and targets are installed. To install manually, copy the plugin to
the directory printed by `radare2 -NN -H R2_USER_PLUGINS`.
Windows binaries use the dynamic MSVC runtime, matching radare2's MSVC build.
Use the matching radare2 distribution and its runtime dependencies; the plugin
archive does not bundle another copy of radare2's DLLs.
Tag the release as `v0.0.1` after the build workflow passes. Tag builds publish
the three ZIPs only after every platform succeeds. CI tests automatic plugin
discovery, JSON/CSV output, comment writes, error recovery, and normal/quick
process exit on all three platforms, plus the r2r suite on Linux and macOS.
CI also builds and checks `make user-install` on Linux/macOS and CMake's
`user-install` target on Windows before publishing.
After publication, CI installs the public binaries with `r2pm -bi r2xsql`
against the official Linux and Windows radare2 distributions and runs the
same smoke checks.
## Tests
Run the regression suite with radare2, `r2r`, a C compiler and Python 3 on
`PATH` (Python's standard library is used only for the HTTP transport test):
```sh
make test
```
This builds the plugin and runs the r2r database in `tests/db`. Each test loads
the exact build explicitly, ignoring installed user plugins, and checks its
output; SQL exceptions fail the suite even when radare2 exits successfully.
The fixture is compiled locally, with no downloaded test binaries or pytest.
To test an existing CMake/Meson build, or select matching radare2 tools:
```sh
make -C tests R2XSQL_PLUGIN=/absolute/path/core_r2xsql.so R2=/path/to/r2 R2R=/path/to/r2r
# Only command tests (no Python needed):
make -C tests TESTS=db/cmd
```
`R2R_ARGS` accepts r2r options (default `-L`); for example, `R2R_ARGS=-i` enables
interactive failure review. One small standard-library helper retains real
HTTP GET/POST coverage, including server startup and shutdown; r2r checks its
result alongside the command tests.
The Make build tracks header dependencies. This prevents stale adapters after
C++ interface changes, which could previously cause `std::bad_alloc` during
`make test`. After updating this Makefile, existing objects are rebuilt once
to establish dependency files. For older checkouts, `make clean && make test`
clears those stale objects.
## Backends (internal detail)
The plugin uses the host RCore; embedding and tests use these backends:
| Backend | Used by | Notes |
|---|---|---|
| `LibrBackend` | `core_r2xsql` and C++ consumers (`open_libr`) | Uses radare2 in-process; the plugin wraps its host core. |
| `MockBackend` | embedding/tests | Canned `{command → response}` map, records issued commands — no live radare2. |
The in-process backend reads analysis, flags, symbols, binary metadata, IO maps,
write caches, types, and runtime catalogs through native libr APIs. The command
fallbacks support mock responses where needed. Instruction/operand text
rendering and byte/ROP searches still use radare2's command handlers to preserve
their rendering, search configuration, and hit behavior.
The internal r2pipe backend was for the removed standalone CLI and servers.
It duplicated process management and command-based access without serving the
native plugin, so it has been removed. External r2pipe clients can still send
`sql` commands to radare2 with this plugin loaded; radare2 also provides HTTP
access through `/cmd/`.
+73
View File
@@ -0,0 +1,73 @@
# SQL access
## Queries
For direct console use or programmatic clients:
```sql
SELECT name, size FROM funcs WHERE name LIKE 'main%';
SELECT content FROM strings WHERE content LIKE '%password%';
UPDATE comments SET text = 'reviewed' WHERE addr = 0x1234;
```
Tables read native analysis data or the JSON form of the relevant r2 command
(`aflj`, `izj`, `axtj`, …) from the host's in-process core.
## It's not read-only — and your work saves
Load the plugin in radare2 and mutate analysis through ordinary SQL writes.
Use radare2's `-w` option when writing binary bytes:
```sql
UPDATE funcs SET name = 'parse_header' WHERE addr = 0x401000; -- rename
UPDATE funcs SET prototype = 'int parse_header(char *buf)' WHERE name = 'parse_header';
INSERT INTO comments(addr, text) VALUES (0x401044, 'bounds check missing');
UPDATE locals SET name = 'len', type = 'int' WHERE func_addr = 0x401000 AND name = 'var_8h';
INSERT INTO types(name, kind) VALUES ('Header', 'struct');
INSERT INTO bookmarks(addr, name) VALUES (0x401000, 'entry-of-interest');
```
Writable surfaces: **funcs** (rename, prototype), **comments**, **flags**,
**locals** (rename + retype), **bookmarks**, **types** (create struct/union/
enum), **patches** (byte edits), **io_maps**, **bytes**.
**Persist it.** Save analysis with `Ps NAME` in radare2 and reopen it with
`r2 -p NAME` without a separate binary or reanalysis. The plugin uses radare2's native project format; it does not
automatically save on exit.
Project save/open and full type declarations use native radare2 commands:
`Ps NAME`, `P NAME`, and `"td struct Header { int magic; };"`. The former SQL
functions `r2xsql_project_save`, `r2xsql_project_open`, and
`r2xsql_type_define` are removed. SQL can still list/delete projects and
create empty struct/union/enum types through the corresponding tables.
## Output formats
`sql` executes one statement using radare2's table renderer. It honours
`cfg.table.format`, `cfg.table.maxcol` and `scr.utf8`. Put a table query
immediately after `sql,`, before the SQL:
```text
sql,:csv SELECT name,size FROM funcs
sql,:tsv SELECT name,size FROM funcs
sql,?
```
Use SQL `WHERE`, `ORDER BY` and `LIMIT` for filtering. Table JSON (`sql,:json`)
omits NULL and empty-string cells; use `sqlj` for programmatic access.
`sql.tables` / `sql.views` discover the current schema; `sql.?` lists
subcommands. The `sql.` namespace does not carry SQL statements.
## JSON results
Use `sqlj` for programmatic queries and scripts:
```text
"sqlj SELECT NULL AS missing, '' AS empty; SELECT count(*) AS n FROM funcs"
```
It returns libxsql's JSON envelope with per-statement columns, rows, errors,
and timings. SQL NULL is JSON `null`, distinct from the empty string. Quoting
the entire r2 command also preserves statement separators and SQL operators.
Read [saving and reopening](usage.md#saving-and-reopening) for persistence.
The same command works through r2pipe and radare2's HTTP `/cmd/` endpoint.
+236
View File
@@ -0,0 +1,236 @@
# Using r2xsql with radare2 and an agent
The plugin supplies SQL over a live radare2 core. The skills teach an agent
how to choose tables, query and annotate that core, and manage its lifetime.
The user can ask in natural language in either tandem or headless mode.
## Installation
Install radare2, then install a matching r2xsql plugin:
```sh
r2pm -U
r2pm -bi r2xsql
```
Release `v0.0.1` targets radare2 6.2.6. For a source build on Linux/macOS,
use `r2pm -ci r2xsql` or `make user-install`. Windows source builds use
`cmake --build build --config Release --target user-install` after
[configuring CMake](development.md).
Installed plugins load automatically on normal startup. `make user-install`
does the same for a local build. Manual installation uses the directory
reported by `r2 -NN -H R2_USER_PLUGINS`. Restart a host after installation.
The plugin's radare2 ABI must match the engine loading it; iaito can use a
different engine from the standalone r2 on PATH. Binary installation with
`r2pm -bi r2xsql` requires an available asset matching the host and platform.
See [building and packaging](development.md).
For a build that is not installed:
```sh
r2 -i ./core_r2xsql.so -A ./target
```
Use the actual `.so`, `.dylib` or `.dll` file. `-i` also accepts native r2
command scripts. Inside an existing console, load a plugin with
`L ./core_r2xsql.so`; `L r2xsql` is not a package-name loader.
`-N` skips startup scripts while retaining plugins; `-NN` disables automatic
plugin loading too. For a deterministic local-build check, use `-NN -i FILE`.
Verify `r2xsql` in `Lcj`, then try `sql?` and `sqlj SELECT 1`.
Install the [r2xsql skills](https://github.com/radareorg/radare2-skills/tree/main/r2xsql#install)
in the coding agent's harness. Skills and the binary plugin are separate
installations. The harness selects the `r2xsql` entry skill and routes to
connection and domain instructions; explicit skill selection can be used
when the harness does not select it automatically.
## Tandem mode
Tandem shares an existing analysis in an interactive r2 CLI, its visual/panels
text interface, or [iaito](https://github.com/radareorg/iaito). For a fresh
binary run `r2 -A ./target`, or open it in iaito and let analysis finish.
For a saved project run `r2 -p PROJECT` without `-A`.
At the r2 prompt or iaito's **View > Console**, verify the plugin with `Lcj`,
then start background HTTP:
```text
e http.bind=127.0.0.1
e http.sandbox=false
e http.port=9090
=h&
```
Choose a free port. `http.port` selects it; `=h&` starts the background
server and leaves the console/UI available. It is an alias for `& =h`,
using r2's background-task facility. `=h` serves in the foreground instead.
Start HTTP at the prompt before entering the text interface.
Wait for `open http://127.0.0.1:9090/`, then tell the agent:
> Use r2xsql at http://127.0.0.1:9090 on the binary already open here.
> Show me the largest functions.
The agent checks the host, plugin and binary identity, authors SQL and reuses
the endpoint on later questions. It should not open another UI or rerun
analysis merely to attach. Renames and comments affect the human's analysis
too. Take turns with the agent when running analysis-changing native commands.
Use `=h-` at the interactive console to stop background HTTP while keeping
the human's host open.
For several hosts, select a distinct port per process and provide a mapping:
**application = http://127.0.0.1:9090, library = http://127.0.0.1:9091**.
The agent verifies each target and labels findings by source. SQL executes
inside one host; comparisons across hosts combine results in the client.
## Fully headless
With the plugin installed, an agent can start the native r2 process without
a human console or GUI. For a fresh binary, POSIX shell:
```sh
r2 -N -q -A -e scr.html=false -e http.bind=127.0.0.1 \
-e http.sandbox=false -e http.port=9090 -c '=h' ./target \
>r2.stdout.log 2>r2.stderr.log &
r2_pid=$!
```
Windows PowerShell:
```powershell
$engine = Get-Command r2,radare2 -CommandType Application -ErrorAction SilentlyContinue |
Select-Object -First 1
if (-not $engine) { throw 'Install radare2 or supply its executable location.' }
$target = (Resolve-Path -LiteralPath '.\target.exe').Path
$port = 9090 # choose a free port
$logDir = Join-Path $env:TEMP ('r2-session-' + [guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Path $logDir | Out-Null
$arguments = '-N -q -A -e scr.html=false -e http.bind=127.0.0.1 -e http.sandbox=false -e http.port={0} -c "=h" "{1}"' -f $port,$target
$process = Start-Process -FilePath $engine.Source -ArgumentList $arguments -WindowStyle Hidden `
-RedirectStandardOutput (Join-Path $logDir 'stdout.log') `
-RedirectStandardError (Join-Path $logDir 'stderr.log') -PassThru
$endpoint = 'http://127.0.0.1:{0}/cmd/' -f $port
```
These launch radare2 directly. No Python session/query wrapper or build
orchestrator is needed. `-A` runs analysis once; foreground `=h` keeps the
background OS process alive. Queries use that same core until shutdown.
For an uninstalled plugin, substitute `-NN -i PLUGIN_FILE` for `-N`.
Wait for the server banner on stderr, then verify the endpoint with curl.
A bind failure is not readiness. Retain the PID, endpoint, binary/project
and log locations; subsequent prompts reuse this session.
## HTTP queries
For radare2 **6.2.4**, POST the complete r2 command to `/cmd/`:
```sh
curl -d '?V' http://127.0.0.1:9090/cmd/
curl -d 'Lcj' http://127.0.0.1:9090/cmd/
curl -d '"sqlj SELECT * FROM binary"' http://127.0.0.1:9090/cmd/
curl -d '"sqlj SELECT name,size FROM funcs WHERE size > 100 LIMIT 5"' \
http://127.0.0.1:9090/cmd/
```
In PowerShell use `curl.exe` to avoid the shell alias:
```powershell
$command = '"sqlj SELECT name,size FROM funcs ORDER BY size DESC LIMIT 10"'
curl.exe --silent --show-error --max-time 30 -d $command $endpoint
```
The body is an r2 command. The **double quotes are part of that body** and
protect SQL operators such as `>` and `|` from r2's parser. Use `sqlj`
for the JSON envelope; `sql,:json` is a table format with different NULL and
empty-string behavior. Inspect top-level and per-statement success/error
fields; HTTP 200 alone does not establish success. See [SQL access](sql.md).
Native commands such as `pdf` and `Ps` travel over the same endpoint.
The command-prefix-plus-body form is implemented in development radare2
and is planned for **6.2.6**:
```sh
curl -d 'SELECT name,size FROM funcs ORDER BY size DESC LIMIT 5' \
http://127.0.0.1:9090/cmd/sqlj
```
Here the URL selects `sqlj` and the body contains only SQL. This is a
version-dependent alternative, not a 6.2.4 example. Use the full-command
form until the host is known to support it. An agent should verify the JSON
response to a harmless query before choosing this form for mutations.
The combined command still passes through r2's parser. For SQL containing
operators such as `>` or `|`, keep the full double-quoted command form above.
## Saving and reopening
Analysis lives in memory until saved. Ask the agent to save the project beside
the binary, or select another location. The save operation is native radare2;
there is no SQL `save_database()` in r2xsql.
```sh
curl -d 'Ps /absolute/path/target.exe.r2' http://127.0.0.1:9090/cmd/
```
PowerShell, using the absolute target path from the launch:
```powershell
$project = ($target + '.r2').Replace('\','/')
curl.exe --silent --show-error --max-time 30 -d ('Ps ' + $project) $endpoint
Get-Item -LiteralPath $project
```
Use forward slashes in native r2 commands on Windows. Verify the actual saved
artifact, host log and requested annotations; a refused save can return an
empty HTTP 200. Keep the original binary and any companion `.r2.d` directory.
Saving analysis does not patch the executable or shut down the host.
Reopen the saved project with `-p`, without a separate target or reanalysis:
```sh
r2 -N -q -p /absolute/path/target.exe.r2 \
-e scr.html=false -e http.bind=127.0.0.1 \
-e http.sandbox=false -e http.port=9090 -c '=h' \
>r2.stdout.log 2>r2.stderr.log &
```
The project stores the binary path. On Windows replace `-A` and the target
in the launch arguments with `-p "PROJECT"`; keep the same `-e` options
and `-c "=h"`. Verify binary identity and saved annotations after reopening,
rather than checking only function counts. Preserve the checkpoint and logs
if verification fails.
### Projects and command scripts
Native projects support binary serialization and script compatibility; do not
infer their format from the filename alone. `Ps PROJECT` and `-p PROJECT`
are the normal save/reopen interface. A bare project name uses `dir.projects`;
an absolute filename selects a sidecar. `Pl` lists named projects, not every
sidecar elsewhere.
Where available, `prj?` documents the binary-container operations, including
`prj save FILE` and `prj open FILE`. Select the matching native loader for
an existing container; a standalone container is not interchangeable with a
named `-p` project on every host. Explicit command-script exports use `PS`
or `PS*` and can be interpreted with `-i SCRIPT`; they are not a reason to
describe all projects as scripts. Check `P?` and `prj?` on the chosen host.
## Shutdown and session ownership
On a save-and-shutdown request, save and verify first, then stop an agent-owned
foreground server:
```sh
curl 'http://127.0.0.1:9090/cmd/=h--'
```
`=h--` is intercepted in the GET path; putting it in a POST body does not
stop that accept loop. With the headless `-q` launch and no later commands,
r2 exits when serving ends. Verify the intended PID and listener stopped.
Do not close the user's tandem UI; stop its background server at the console
only when requested. Saving alone leaves the host available for follow-ups.
The server provides local command access, including writes when
`http.sandbox=false`. Bind to `127.0.0.1` and do not expose it as a public
service.
+1 -1
View File
@@ -9,7 +9,7 @@ add_executable(list_funcs list_funcs.cpp)
target_link_libraries(list_funcs PRIVATE r2xsql::r2xsql)
target_compile_features(list_funcs PRIVATE cxx_std_20)
# Link both backends used by the example.
# Link the native backend used by the example.
target_link_libraries(list_funcs PRIVATE r2xsql_libr Radare2::libr)
# Auto-copy r2 DLLs (Windows) next to the example exe so it can launch
+6 -17
View File
@@ -6,7 +6,7 @@
//
// list_funcs.cpp — minimal libr2xsql consumer.
//
// Opens a binary (libr backend if available, else r2pipe), lists the
// Opens a binary through libr and lists the
// first N functions via SQL. Demonstrates the API a third-party tool
// would use to embed r2xsql.
@@ -33,23 +33,12 @@ int main(int argc, char** argv) {
const std::string path = argv[1];
const int limit = (argc >= 3) ? std::atoi(argv[2]) : 20;
std::unique_ptr<r2xsql::Backend> backend;
// Prefer libr; fall back to r2pipe if libr isn't linked or fails.
r2xsql::LibrOpenOptions libr_opts;
libr_opts.path = path;
backend = r2xsql::open_libr(libr_opts);
r2xsql::LibrOpenOptions opts;
opts.path = path;
r2xsql::BackendError error;
auto backend = r2xsql::open_libr(opts, &error);
if (!backend) {
std::fprintf(stderr, "libr backend unavailable, falling back to r2pipe...\n");
r2xsql::R2PipeOpenOptions pipe_opts;
pipe_opts.path = path;
backend = r2xsql::open_r2pipe(pipe_opts);
}
if (!backend) {
std::fprintf(stderr, "error: could not open '%s' with any backend\n",
path.c_str());
std::fprintf(stderr, "error: %s\n", error.message.c_str());
return 1;
}
+4 -6
View File
@@ -27,9 +27,9 @@ project('r2xsql', ['c', 'cpp'],
# --- radare2 plugin dir, straight from r2 itself ------------------------------
r2 = find_program('radare2', 'r2', required: true)
if get_option('r2_user_plugins')
res = run_command(r2, '-HR2_USER_PLUGINS', capture: true, check: false)
res = run_command(r2, '-NN', '-HR2_USER_PLUGINS', capture: true, check: false)
else
res = run_command(r2, '-HR2_LIBR_PLUGINS', capture: true, check: false)
res = run_command(r2, '-NN', '-HR2_LIBR_PLUGINS', capture: true, check: false)
endif
if res.returncode() == 0 and res.stdout().strip() != ''
r2_plugdir = res.stdout().strip()
@@ -44,13 +44,11 @@ threads = dependency('threads')
xsql_dep = subproject('libxsql').get_variable('xsql_dep')
# --- r2xsql core (all of src/lib/src is the full/libr plugin set) -------------
# The 43 core table adapters plus backend_libr.cpp + direct_libr.cpp, which are
# gated by R2XSQL_HAVE_LIBR (=1 here: this build is the in-process libr flavor).
# Compile the SQL tables and native backend into the plugin.
r2xsql_srcs = files(
'src/lib/src/backend_common.cpp',
'src/lib/src/backend_libr.cpp',
'src/lib/src/backend_mock.cpp',
'src/lib/src/backend_r2pipe.cpp',
'src/lib/src/direct_libr.cpp',
'src/lib/src/json_utils.cpp',
'src/lib/src/session.cpp',
@@ -106,7 +104,7 @@ r2xsql_inc = include_directories('src/lib/include', 'src/lib/src', 'src')
shared_module('core_r2xsql',
[r2xsql_srcs, plugin_src],
name_prefix: '',
cpp_args: ['-DR2XSQL_HAVE_LIBR=1'],
name_suffix: host_machine.system() == 'darwin' ? 'dylib' : [],
include_directories: r2xsql_inc,
dependencies: [xsql_dep, r_core, threads],
install: true,
+54 -118
View File
@@ -195,11 +195,12 @@ interface; commands act on the host session.
| Backend | Use |
|---------|-----|
| **r2pipe** | C++ consumers call `open_r2pipe` to spawn and manage `radare2 -q0`. |
| **libr** | C++ consumers call `open_libr`; the plugin wraps the host core. Requires a matching radare2 build. |
| **mock** | Embedding/testing with canned command responses and no live radare2. |
The SQL table descriptions below apply to both live library backends.
The plugin uses the host RCore. The former internal r2pipe backend served the
removed standalone CLI and servers. External r2pipe clients can still issue
`sql` commands to a radare2 process with the plugin loaded.
---
@@ -246,19 +247,14 @@ a single entry and exit — the unit of control-flow analysis.
`post_dominators`/`loops` straight from radare2's in-process analysis state**
(`fcn->bbs`) instead of running `afbj @ X` and parsing its JSON — all six share
the same underlying block list, so this is one port that speeds up all six at
once, an unfiltered scan going from one command per function to none. The
r2pipe backend drives a separate radare2 process, so it uses the `afbj @ X`
commands described below on both the full scan and the `func_addr` pushdown.
`switch_tables.table_addr` stays NULL on both flavors: the C API has the jump
table's own address, but no radare2 command exposes it, so surfacing it only
in-process would make the two flavors disagree on that column for the same
query.
once, an unfiltered scan going from one command per function to none.
`switch_tables.table_addr` remains NULL for compatibility with the SQL schema.
### Control-flow edges
`cfg_edges` is the intra-function control-flow graph: one row per edge between two
basic blocks — `func_addr`, `from_addr`, `to_addr` (source/target block start
addresses), and `edge_type` (`normal`, `true`, `false`). Always filter by
`func_addr` (pushdown → `afbj @ X` on the pipe-only flavor). Branch points are
`func_addr`. Branch points are
blocks with >1 successor; find them with `SELECT from_addr, COUNT(*) FROM
cfg_edges WHERE func_addr = X GROUP BY from_addr HAVING COUNT(*) > 1`.
@@ -267,8 +263,7 @@ Canonical cross-tool CFG-graph tables (aligned with ghidrasql). Each maps a
function's basic blocks onto opaque node ids and runs the generic
`xsql::graph` module — pure r2xsql code, not a radare2 dominance API, so the
in-process speedup above comes entirely from how the block list itself is
read. Always filter by `func_addr` (pushdown → `afbj @ X` on the pipe-only
flavor).
read. Always filter by `func_addr`.
| Table | Columns |
|-------|---------|
@@ -348,11 +343,8 @@ from the section/segment maps (omitted when unmapped). `bits` doubles as the
canonical bits key. No `md5`/`sha256` (best-effort core; radare2 hashing is
unreliable).
`radare2_version` is the running radare2 ENGINE's own version (from `?Vj`),
distinct from `r2xsql_version`/`tool_version` above — on the r2pipe flavor
the engine actually running is whatever `radare2` binary is on `PATH` at
runtime, which can differ from what r2xsql itself was built against, so
this is the only reliable source for "which radare2 is this."
`radare2_version` is the running radare2 engine version, distinct from
`r2xsql_version`/`tool_version`.
```sql
-- everything at a glance
@@ -480,10 +472,9 @@ address-scoped,
so on the in-process backend each one dispatches through a structural
command-dispatch path that never evaluates r2's own command separators at all
— a second, independent layer beneath the name/signature validation above (the
same mechanism `comments` writes already use). The pipe backend is unaffected:
it always uses the plain command form.
same mechanism `comments` writes already use).
`name`'s rename stays on that command path (`afn`) on **both** flavors —
`name`'s rename stays on that command path (`afn`) —
deliberately not ported to a direct C-API call, unlike `flags`/`locals`
(below). `afn`'s command handler also renames the function's backing flag and
its stored signature metadata, side effects the raw C-API rename function
@@ -556,18 +547,9 @@ split `funcs.prototype` documents against `funcs`'s other columns — and is
redundant with the structured `args`/`ret`/`rets`/`argn` fields above.
#### `locals` (writable)
Source per-function `afvj @ <func_addr>` on the pipe-only flavor; the libr backend
reads `fcn->vars` directly instead of running that command — identical rows,
one command per function replaced by none on a full scan. One row per local
variable/argument, keyed on `(func_addr, name)`. Both `name` and `type` are
writable on both flavors: the libr backend calls `r_anal_var_rename`/
`r_anal_var_set_type` directly (measured 78-150x over `afvn`/`afvt` on a bulk
rename/retype workload, exact parity); the pipe-only flavor still issues
`afvn`/`afvt`. On the in-process (libr) flavor, whenever that C-API leg is
unavailable, the command-path fallback (also the pipe-only flavor's only
path) dispatches `afvn`/`afvt` through the same structural no-eval command
path every other address-scoped write uses — see "Structural no-eval
dispatch" below.
Reads `fcn->vars` directly, with one row per local variable/argument keyed on
`(func_addr, name)`. Both `name` and `type` are writable through
`r_anal_var_rename` and `r_anal_var_set_type`.
| Column | Type | Notes |
|---------------|-------|------------------------------------------------|
@@ -592,7 +574,7 @@ Locals are analysis-derived — no INSERT/DELETE. r2 reflects a rename/retype
immediately, so the next read returns the new value.
#### `blocks` (read-only)
Source per-function `afbj @ <addr>` on the pipe-only flavor; the libr backend
The libr backend
reads `fcn->bbs` directly (see "Basic blocks" above). Filter by `func_addr`.
| Column | Type | Notes |
@@ -628,9 +610,7 @@ path (`pdfj`/`pdj`, same as always). Every other column (`addr`, `size`,
of `disasm`/`mnemonic` reads it straight from radare2's analysis state
instead — measured 4-9x faster end to end, with exact field parity, on every
one of this table's three shapes (full scan, `func_addr` pushdown,
`start_addr`/`count` pushdown). This applies to the libr backend only — the
r2pipe backend connection always uses the command path, since it spawns
a separate radare2 process with no in-process state to read. In practice:
`start_addr`/`count` pushdown). In practice:
`SELECT addr, op_type, esil FROM instructions WHERE func_addr = X` is the
fast shape; adding `disasm` or `mnemonic` to that same query switches it back
to the command path — not a bug, and not something to avoid when you
@@ -735,7 +715,7 @@ WHERE o.func_addr = 0x401000 ORDER BY o.seq, p.operand_index;
```
#### `sections` / `segments` (read-only)
Source `iSj` / `iSSj` on the pipe-only flavor; the libr backend reads
The libr backend reads
`RBinSection` directly instead (see "Sections vs segments vs io_maps" above).
| Column | Type | Notes |
@@ -755,8 +735,9 @@ FROM sections WHERE perm LIKE '%x%';
```
#### `io_maps` (writable — r2's memory map)
Source `omj`. radare2 IO maps: the live, editable address space (distinct from the
read-only binary `sections`/`segments`). INSERT issues `om`, DELETE issues `om-<id>`.
Native radare2 IO maps: the live, editable address space (distinct from the
read-only binary `sections`/`segments`). INSERT and DELETE use RIO APIs; names,
including spaces and command metacharacters, are passed as data.
| Column | Type | Notes |
|---------|-------|--------------------------------|
@@ -828,8 +809,7 @@ independent layer beneath the hex-charset validation above (which, on its
own, already keeps this argument free of every r2 command metacharacter).
`wci`/`wcu` (the UPDATE/DELETE primitives above) are fixed commands with no
user-controlled text in them at all — nothing to dispatch differently — so
they stay on the plain command path, unchanged. The pipe backend is
unaffected on all three writes.
they stay on the plain command path.
#### `bytes` (writable: UPDATE only — direct byte overwrite)
Source `omj`/`p8`/`iSj` for reads (see *Raw bytes* above); `value` is writable
@@ -865,11 +845,11 @@ this table. No INSERT or DELETE: a byte can't be created or removed, only
changed. `addr`/`is_initialized` stay read-only.
#### `strings` (read-only)
Source `izj` on the pipe-only flavor; the libr backend reads `r_bin_get_strings`
The libr backend reads `r_bin_get_strings`
directly instead — identical rows (that command handler already calls the
same function internally). (The `content` column is skipped at build time
when you don't select it — so `COUNT(*)` / length histograms over `strings`
are cheap on both flavors.)
are cheap.)
| Column | Type | Notes |
|-----------|-------|------------------------------------|
@@ -912,7 +892,7 @@ FROM data_items ORDER BY is_string DESC, size DESC LIMIT 20;
```
#### `imports` (read-only)
Source `iij` on the pipe-only flavor; the libr backend reads `RBinImport`
The libr backend reads `RBinImport`
directly instead (via the same `r_bin_name_tostring`/`r_bin_demangle`/
`r_core_bin_impaddr` helpers the command emitter itself calls) — identical
rows, no command dispatch.
@@ -941,10 +921,10 @@ WHERE name LIKE 'Crypt%' OR name LIKE 'BCrypt%'
#### `entries` (read-only)
Sources `iej` (program entry points) plus `iEj` (exports), deduplicated by address,
on the pipe-only flavor; the libr backend reads `RBinAddr`/`RBinSymbol` directly
in radare2; the libr backend reads `RBinAddr`/`RBinSymbol` directly
instead and merges them the same way — identical rows. Executables normally
have at least their loader entry point even when they export no symbols. Note
`entries.name` is the ORIGINAL symbol name, never demangled, on either flavor.
`entries.name` is the ORIGINAL symbol name, never demangled, in the SQL output.
| Column | Type | Notes |
|--------|-------|---------------------|
@@ -968,12 +948,7 @@ across every function.
**The libr backend reads this table straight from radare2's in-process analysis
state instead of running those commands** — same rows, roughly an order of
magnitude faster on a large binary, where an unfiltered scan would otherwise be
one command per function. The r2pipe backend drives a separate radare2
process, so it uses the commands above. (`funcs`, `byte_search`, `locals`,
`flags`, the `blocks`/`cfg_edges`/`switch_tables`/`dominators`/
`post_dominators`/`loops` cluster, and `strings`/`sections`/`segments`/
`imports`/`entries` use the same in-process path; every other table runs
commands on both flavors.)
one command per function.
| Column | Type | Notes |
|-------------|-------|----------------------------------------|
@@ -1203,10 +1178,8 @@ SELECT hex FROM assemble WHERE asm = 'jmp 0x401010' AND addr = 0x401000;
`asm` is charset-validated (letters, digits, space, and
`,.+-*/[]():%#_$`) BEFORE it is ever spliced into a command — real assembly
text never needs `;`/`@`/`|`/backtick/quotes, so this alone makes the
argument injection-safe; `Backend::cmd_noeval` is layered on top for
defense-in-depth on the libr backend (same "wired in anyway, not because a
real gap was found here" posture `patches`' `wx` write already has).
text is passed through literal command dispatch, so `$()` and other r2
metacharacters cannot trigger command expansion.
#### `search_asm` (read-only)
Assembles `asm` and searches for the resulting bytes via the plain
@@ -1231,21 +1204,13 @@ SELECT printf('0x%x', addr) AS at, matched_hex FROM search_asm
WHERE asm = 'push rbp' AND max_results = 20;
```
Sets `search.in=io.maps.x` for the duration of its own search and restores
`io.maps` afterward, in ONE `Backend::cmd()` call — the same atomic
set/search/restore idiom `rop_gadgets` uses, and for the same reason
(`search.in` is session-global config that leaks past the command that set
it, shared across concurrent interactive callers). Does **not** use
`Backend::cmd_noeval` — the atomic three-command chain is structurally
incompatible with cmd_noeval's single no-eval-command dispatch, so `asm`'s
charset allowlist (same validator as `assemble`) is what makes this safe on
its own, the identical trade-off `rop_gadgets` already made for its own
`pattern` input. Command-path only; pair-tested identical on both backends
(11/11 hits for `asm = 'push rbp'` against the same fixture on pipe and
libr).
Searches executable maps (`search.in=io.maps.x`) and restores the caller's
previous setting through native config APIs. Both `search_asm` and the ROP
tables dispatch directly to radare2's command handler: SQL text cannot expand
`$()` substitutions, and `#` cannot comment out a restoration command.
#### `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 +1234,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`
@@ -1347,7 +1307,7 @@ is a plain validated integer with no free-text argument anywhere on this
table's surface, so there is no charset-allowlist injection surface to
defend (unlike `assemble`/`demangle`); `Backend::cmd_noeval` is still wired
in for defense-in-depth and consistency with every other address-scoped
read on the libr backend. Command-path only, identical on both backends (one
read on the libr backend. Uses radare2's command handler (one
full-scan-equivalent command, no C-API producer needed).
### Exploitation
@@ -1520,15 +1480,9 @@ typedef, or func needs more than a bare name to exist). For a **full**
declaration (members, an underlying primitive, a signature), use
radare2's `td <C declaration>` command.
Neither `td` nor `t-` is address-scoped (a type definition isn't tied to any
address, and `t-` deletes purely by name) — unlike every other writable
table's commands, which are anchored on a real row address. On the in-process
(libr) backend both still dispatch through the structural no-eval path
(`Backend::cmd_noeval`), seeked to a fixed anchor address (0) instead of a row
address, since neither command ever consults the current seek. This is the
same additional, structural layer every other writable table's commands get
— on top of, never instead of, the existing name/declaration validation
below; the pipe backend is unaffected.
SQL type writes use `r_anal_cparse` / `r_anal_save_parsed_type` and
`r_anal_remove_parsed_type` directly, without changing the seek or constructing
radare2 commands. The type catalog and members are also read through native APIs.
```sql
SELECT name, kind, size FROM types WHERE kind = 'struct' ORDER BY size DESC LIMIT 20;
@@ -1542,7 +1496,7 @@ At the radare2 prompt:
```
The full type set (e.g. the thousands of Windows API typedefs) lives in
radare2's `share/fcnsign/types-*.sdb`. The **r2pipe** backend always loads it.
radare2's `share/fcnsign/types-*.sdb`.
The plugin uses the host radare2 installation's data directory. Embedding
applications using the **libr** backend must also provide a complete radare2
installation; otherwise `types` can collapse to a few built-ins.
@@ -1641,7 +1595,7 @@ WHERE name NOT LIKE 'std::%' AND name <> 'type_info';
classes with `Dog`→`Animal` and `Bird`→`Animal` base links and 3 methods
each; Itanium/ELF gives only **2 classes** (the abstract base `Animal` is
missing), **no `base_name` links at all**, and 4 methods each. This is
radare2's `avrr` recovery, not an r2xsql defect — both backends agree with
radare2's `avrr` recovery, not an r2xsql defect — the SQL table agrees with
each other on both platforms. On GCC/Clang targets: **do not rely on
`base_name` to reconstruct a hierarchy, and do not treat a missing class as
proof it is absent**; cross-check `vtable_addr` and raw `acllj`. MSVC
@@ -1745,11 +1699,8 @@ SELECT name FROM libs ORDER BY name;
### Annotations (writable)
#### `flags` (writable)
Source `fj` (full dump) + one `fs <space>; fj; fs *` per flagspace on the
pipe-only flavor, to attribute radare2's real flagspace onto each flag by
name; the libr backend reads `r_flag_foreach` directly instead — identical rows,
the per-flagspace enumeration replaced by one pass with no `fs`
select/restore at all. Named addresses.
Named addresses read with `r_flag_foreach`, including each flag's actual
flagspace, without changing the selected flagspace.
| Column | Type | Notes |
|------------|-------|--------------------------------|
@@ -1770,14 +1721,10 @@ backend both dispatch through the same structural no-separator-evaluating
command path `comments`/`funcs` writes already use — a second, independent
layer beneath the name validation above. `name`'s rename calls radare2's C API
directly on the libr backend (`r_flag_get` + `r_flag_rename`, no address needed
at all — measured 48-62x over `fr` on a bulk-rename workload, exact parity);
the pipe backend still issues `fr` (seeked to the flag's own address, which
`fr` never consults — a harmless anchor, not an input into the rename).
at all — measured 48-62x over `fr` on a bulk-rename workload, exact parity).
#### `comments` (writable)
Source `CCj` on the pipe-only flavor; the libr backend reads radare2's meta
interval tree directly instead — identical rows (a single full-scan command,
not a per-function loop, so the win is skipping a json round trip).
Reads radare2's metadata interval tree directly.
| Column | Type | Notes |
|--------|-------|--------------------------------|
@@ -1795,13 +1742,7 @@ On the libr backend, `text`'s set/update calls `r_meta_set_string` directly
(measured 30-40x over `CCu` on a bulk-set workload, exact parity) — including
reproducing `CCu`'s own dedup quirk: a write whose new text is already a byte
substring of the EXISTING comment is silently skipped (no-op), matching
production behavior on both flavors. The pipe backend still sends
`CCu base64:<b64>`, so spaces, quotes, and `@` are stored verbatim and cannot
inject extra radare2 commands; on the in-process backend that same
base64-encoded command path remains the fallback whenever the C-API leg is
unavailable, additionally dispatched through a structural
no-separator-evaluating command path so a `;`/`@`/`|`/backtick in the argument
can never chain into a second command even in principle.
radare2 behavior.
#### `bookmarks` (writable)
Flags inside a dedicated `bookmarks` flagspace; every write saves/restores the
@@ -1826,9 +1767,7 @@ independent layer beneath the name validation above and the flagspace
save/select/restore. `name`'s rename (`fr`) carries no address in its own
grammar at all (same as `flags`' rename), but still gets the same dispatch:
seeked to the bookmark's own address first, which `fr` never consults, so
that seek is a harmless anchor, not an input into the rename. The pipe
backend is unaffected on all three writes: it always uses the plain command
form.
that seek is a harmless anchor, not an input into the rename.
### Persistence
@@ -1862,7 +1801,7 @@ table is an editable annotation. Each affected row issues one radare2 command.
| `locals` | — | `name` ⇒ `afvn <new> <old>`; `type` ⇒ `afvt <name> <type>` | — |
| `types` | `td "<kind> <name> {};"` (empty struct/union/enum shell only; a full declaration uses r2's `td`) | — | `t- <name>` |
| `projects` | — (use `Ps <name>`) | — | `P- <name>` |
| `io_maps` | `om $d <vaddr> <size> <paddr> <perm> <name>` (add region) | — | `om-<map_id>` (remove map) |
| `io_maps` | Native `r_io_map_add` / `r_io_map_set_name` | — | Native `r_io_map_del` |
| `patches` | `wx <hex> @ <addr>` (stage) — `patched_bytes` (raw hex) or `asm_text` (assembled via `pa` first, exactly one required) | `committed` ⇒ `1` only: `wci` (commits **ALL** pending patches, never row-selective — see the table's own section) | `wcu` (uncommitted **and** most-recently-staged row only) |
| `bytes` | — | `value` ⇒ `wx <hex> @ <addr>`, **direct/immediate** — NOT staged, bypasses `io.cache` entirely (see the table's own section for how this differs from `patches`) | — |
| `runtime_settings` | — | `value` ⇒ UPDATE (validated) | — |
@@ -1915,21 +1854,18 @@ Rules and gotchas:
user-controlled text.** On the in-process (libr) backend, every
address-scoped write in this table (`funcs`, `flags`, `comments`,
`bookmarks`, `patches`' INSERT, `bytes`' UPDATE, `locals`' `name`/`type`
whenever the C-API leg is unavailable) and the addr-less `types` writes
whenever the C-API leg is unavailable)
dispatch through a command path that never evaluates radare2's own
`;`/`@`/`|`/backtick chaining grammar — a second, independent layer beneath
the name/hex/signature/declaration validation each table already does.
This is complete: every write-capable table has been examined (`locals`
was the last one — its command-path fallback was missed in the original
sweep and closed afterward). The exceptions (`patches`' `wci`/`wcu`, and
The exceptions (`patches`' `wci`/`wcu`, and
`bytes`' own `io.cache` save/set/restore calls, which must stay on the
plain, evaluating command path because they bracket the write rather than
carry user text through it) are fixed literal commands / config toggles
with no user-controlled text in them, so there was nothing to harden there
— left on the plain command path deliberately, not overlooked.
`bytes.value`'s actual injection defense is its strict 0-255 integer
validation, which leaves no free-text argument at all. The pipe backend is
unaffected everywhere: it always uses the original command form.
validation, which leaves no free-text argument at all.
```sql
-- bulk annotate: mark every init-like function for review
+13 -14
View File
@@ -4,17 +4,14 @@
# 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. The libr backend lives in the r2xsql_libr OBJECT library below;
# the mock backend supports consumers with canned command responses.
add_library(r2xsql STATIC
src/version.cpp
src/session.cpp
src/backend_common.cpp
src/backend_mock.cpp
src/backend_r2pipe.cpp
src/json_utils.cpp
src/tables_binary.cpp
src/tables_bininfo.cpp
@@ -69,7 +66,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)
@@ -77,7 +75,6 @@ if(WIN32)
WIN32_LEAN_AND_MEAN
NOMINMAX
_CRT_SECURE_NO_WARNINGS)
target_link_libraries(r2xsql PUBLIC ws2_32)
endif()
set_target_properties(r2xsql PROPERTIES POSITION_INDEPENDENT_CODE ON)
@@ -86,12 +83,11 @@ 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
)
target_compile_definitions(r2xsql_libr PRIVATE R2XSQL_HAVE_LIBR=1)
if(Radare2_ROOT)
# Fallback for r_sys_prefix()'s data-dir cliff: on Windows it resolves relative to the RUNNING
# executable's own location, not radare2's install, so an embedding app
@@ -116,22 +112,25 @@ if(WIN32)
NOMINMAX
_CRT_SECURE_NO_WARNINGS)
# On Windows the r2 import libs reference DLLs; copy them next to the full
# consumers (examples and core_r2xsql) so each loads radare2 without PATH
# juggling. Pipe-only consumers need none of these.
# Put the host DLLs beside embedding executables for both generator types.
if(Radare2_ROOT)
file(GLOB _r2_dlls "${Radare2_ROOT}/bin/*.dll")
if(_r2_dlls)
set(_r2_runtime_dest "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}")
if(CMAKE_CONFIGURATION_TYPES)
string(APPEND _r2_runtime_dest "/$<CONFIG>")
endif()
add_custom_target(r2xsql_copy_r2_dlls ALL
COMMAND ${CMAKE_COMMAND} -E make_directory
"${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/$<CONFIG>"
"${_r2_runtime_dest}"
VERBATIM)
foreach(_dll IN LISTS _r2_dlls)
add_custom_command(TARGET r2xsql_copy_r2_dlls POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${_dll}" "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/$<CONFIG>/"
"${_dll}" "${_r2_runtime_dest}/"
VERBATIM)
endforeach()
add_dependencies(r2xsql r2xsql_copy_r2_dlls)
endif()
endif()
endif()
+10 -54
View File
@@ -6,18 +6,16 @@
//
// backend.hpp — abstract interface for talking to a radare2 session.
//
// Three implementations:
// Implementations:
// - LibrBackend : in-process via libr (r_core_new / r_core_cmd_str)
// - R2PipeBackend : spawns radare2.exe -q0 and pipes commands over stdio
// - MockBackend : canned command -> response map (in-memory, no radare2)
#pragma once
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
#include <nlohmann/json.hpp>
@@ -50,42 +48,15 @@ public:
// is added; otherwise true.)
virtual bool alive() const = 0;
// Human-readable backend name for diagnostics ("libr", "r2pipe", "mock").
// Human-readable backend name for diagnostics ("libr", "mock").
virtual std::string_view name() const = 0;
// Direct C-API row producers, for the tables where reading libr's state
// in-process measured far cheaper than command + JSON. Returns nullptr on
// every backend with no in-process radare2 to read — r2pipe talks to a
// separate process, mock has no radare2 at all — and those keep the command
// path, which is never removed. A table that has been ported checks this once
// and falls back on null; a table that has not been ported ignores it.
// Native table operations. MockBackend uses the command fallbacks.
virtual direct::Api* direct() { return nullptr; }
// Structural no-eval command dispatch, for write paths on the in-process
// libr backend only. Sends `command` straight to its top-level r2 command
// handler, seeked to `addr` first, WITHOUT evaluating radare2's own
// command-chaining grammar (`;`, `@`, `|`, backtick, ...) at all — so any
// such character embedded in an argument to `command` (e.g. user-controlled
// text) is passed through as inert data to that one handler and can never be
// reinterpreted as chaining into a second command. This is an ADDITIONAL
// layer alongside a table's existing input validation/escaping, never a
// replacement for it — callers keep validating input exactly as before.
//
// `command` must be a single command's own text with NO `@`/`;`/`|`/backtick
// needed for addressing or chaining — pass the target address via `addr`
// instead of splicing `@ <addr>` into the string.
//
// Returns false and leaves `out` untouched when this backend cannot dispatch
// this way (every backend but libr: r2pipe talks to a separate process with
// no in-process command handler to call into; mock has no radare2 at all) —
// the caller must then fall back to its existing cmd()-based path unchanged.
// Returns true and fills `out` with the command's textual output otherwise;
// that output is checked for an error exactly like cmd()'s is (a `true`
// return means "dispatched", not "succeeded" — an in-band diagnostic in
// `out` is still possible and must still be checked).
virtual bool cmd_noeval(int64_t /*addr*/, std::string_view /*command*/, std::string* /*out*/) {
return false;
}
// Dispatch one command at addr without expanding r2 command syntax.
// Libr uses r_core_call_str_at; command-only consumers use '@addr'command.
virtual std::string cmd_noeval(int64_t addr, std::string_view command);
};
// ---------------------------------------------------------------------------
@@ -99,16 +70,14 @@ struct LibrOpenOptions {
bool write = false; // open in -w mode; on close, saves the project if `project` is set
};
// Returns nullptr if r2xsql was built without libr support (R2XSQL_HAVE_LIBR=0)
// or if the open fails. *err is populated on failure.
// Returns nullptr if opening fails; *err is populated on failure.
std::unique_ptr<Backend> open_libr(const LibrOpenOptions& opts, BackendError* err = nullptr);
// Wrap an existing radare2 core (an `RCore*`, passed as `void*` so this public
// header stays free of <r_core.h>) in a Backend WITHOUT taking ownership: the
// returned Backend never frees the core. This is the in-process entry used by
// the core_r2xsql plugin to share the host's RCore. The caller retains ownership
// and must keep the core alive for the Backend's lifetime. Returns nullptr in a
// pipe-only build (R2XSQL_HAVE_LIBR=0).
// and must keep the core alive for the Backend's lifetime.
//
// (Declared with C++ linkage and resolved at link time by its sole caller — no
// `extern "C"` is needed since both the definition and the caller are C++ TUs
@@ -116,17 +85,6 @@ std::unique_ptr<Backend> open_libr(const LibrOpenOptions& opts, BackendError* er
// return type" warning, as `std::unique_ptr` is not a C type.)
std::unique_ptr<Backend> make_libr_backend_from_core(void* core_ptr);
struct R2PipeOpenOptions {
std::string r2_executable; // path to radare2(.exe); searched on PATH if empty
std::string path; // binary to open; if empty, opens with --
std::string project; // r2 project name; managed after startup with Pl/P/Ps
bool analyze = true;
bool write = false;
std::vector<std::string> extra_args; // appended after -q0
};
std::unique_ptr<Backend> open_r2pipe(const R2PipeOpenOptions& opts, BackendError* err = nullptr);
// MockBackend is constructible directly (see backend_mock.hpp).
// ---------------------------------------------------------------------------
@@ -142,9 +100,7 @@ std::unique_ptr<Backend> open_r2pipe(const R2PipeOpenOptions& opts, BackendError
// of "Cannot find ...sdb" errors).
//
// This probe reports the directory r2 will look in and whether it exists, so
// callers can detect the misdeployment. In a pipe-only build the
// data dir is resolved by the spawned radare2.exe instead, so `found` is true
// and `probe_path` is empty (not applicable).
// callers can detect the misdeployment.
struct LibrDataDirStatus {
std::string probe_path; // the share/fcnsign directory r2 will probe
bool found = true; // false => r2xsql is not deployed next to radare2.exe
+5
View File
@@ -4,9 +4,14 @@
// This file is licensed under the Human-Origin Source License v1.0.
// See LICENSE.
#include <r2xsql/backend.hpp>
#include "json_utils.hpp"
namespace r2xsql {
std::string Backend::cmd_noeval(int64_t addr, std::string_view command) {
return cmd("'@" + json_utils::hex_addr(addr) + "'" + std::string(command));
}
nlohmann::json Backend::cmd_json(std::string_view command, BackendError* err) {
std::string text = cmd(command);
if (text.empty()) return nlohmann::json::value_t::null;
+16 -52
View File
@@ -3,19 +3,15 @@
//
// This file is licensed under the Human-Origin Source License v1.0.
// See LICENSE.
// LibrBackend — in-process wrapper around r_core, compiled when
// R2XSQL_HAVE_LIBR is set. In a pipe-only build (no radare2 at configure time)
// R2XSQL_HAVE_LIBR is unset and open_libr() returns nullptr.
// LibrBackend — in-process wrapper around r_core.
#include <r2xsql/backend.hpp>
#if R2XSQL_HAVE_LIBR
# include "direct.hpp"
#include "direct.hpp"
# include <r_core.h>
# include <r_userconf.h>
# include <r_util.h>
#endif
#include <r_core.h>
#include <r_userconf.h>
#include <r_util.h>
#include <cstdio>
#include <cstdlib>
@@ -25,7 +21,6 @@
namespace r2xsql {
#if R2XSQL_HAVE_LIBR
// Mirror r2's own data-dir resolution (cbin.c builds <r_sys_prefix>/<R2_SDB_FCNSIGN>
// for the type DB, and separately for the calling-convention/syscall SDBs) so
@@ -33,7 +28,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) {
@@ -222,7 +221,7 @@ class LibrBackend : public Backend {
"or report a generic fallback value rather than the correct "
"one.\n"
" Run the host application from the radare2 install bin/ directory "
"(next to radare2.exe), or use the r2pipe backend.\n",
"(next to radare2.exe).\n",
dd.probe_path.empty() ? "<unknown>" : dd.probe_path.c_str());
}
}
@@ -248,8 +247,7 @@ class LibrBackend : public Backend {
// reference scanning, so an in-process `aaa` finds fewer code/string
// xrefs than the canonical `radare2 <file>` launcher does. Reopening
// (`oo` / `oo+` in write mode) finalizes the mapping exactly as the
// launcher does, so the libr (full) flavor's analysis matches the
// pipe-only (spawned-radare2) reference 1:1.
// launcher does.
(void)r_core_cmd0(core, opts.write ? "oo+" : "oo");
}
@@ -286,33 +284,14 @@ class LibrBackend : public Backend {
return out;
}
// Structural no-eval dispatch: `r_core_call_str_at` (core.c) seeks to `addr`
// then hands `command` straight to `r_cmd_call`, which indexes directly into
// the top-level command handler keyed on the command's own first character
// and passes the remainder as that handler's raw argument — the command
// string is never re-scanned for `;`/`@`/`|`/backtick chaining the way
// `r_core_cmd0` (what `cmd()` above ultimately runs) does. A `;`/`@`/`|`/
// backtick inside an argument therefore cannot start a second command; it is
// opaque data to whichever single handler this dispatch reaches.
bool cmd_noeval(int64_t addr, std::string_view command, std::string* out) override {
if (!core_) return false;
std::string cmd_noeval(int64_t addr, std::string_view command) override {
if (!core_) return {};
std::lock_guard lock(m_);
std::string c(command);
char* res = r_core_call_str_at(core_, static_cast<ut64>(addr), c.c_str());
if (!res) {
// r_core_call_str_at returns NULL only when the command's own first
// character has no registered handler at all -- a dispatch-routing
// failure, not a normal validation rejection (those come back as
// in-band diagnostic text, exactly like cmd()'s failures). Surface it
// through the same "unknown command" marker table_utils::
// command_error_text already recognizes, so every caller's existing
// error check catches this without a second code path.
if (out) *out = "r2xsql: unknown command for no-eval dispatch";
return true;
}
if (out) *out = res;
std::string out = res ? res : "r2xsql: unknown command for no-eval dispatch";
free(res);
return true;
return out;
}
bool alive() const override { return core_ != nullptr; }
@@ -345,21 +324,6 @@ std::unique_ptr<Backend> make_libr_backend_from_core(void* core_ptr) {
return std::unique_ptr<Backend>(new LibrBackend(static_cast<RCore*>(core_ptr), /*owns=*/false));
}
#else // R2XSQL_HAVE_LIBR == 0
std::unique_ptr<Backend> open_libr(const LibrOpenOptions&, BackendError* err) {
if (err) err->message = "r2xsql was built without libr support (R2XSQL_HAVE_LIBR=0)";
return nullptr;
}
// Pipe-only: the data dir is resolved by the spawned radare2.exe, not by this
// process, so there is nothing to probe — report it as available (N/A).
LibrDataDirStatus libr_data_dir_status() { return LibrDataDirStatus{}; }
// Pipe-only stub so a consumer that references the symbol still links; there is
// no in-process core to wrap without libr.
std::unique_ptr<Backend> make_libr_backend_from_core(void*) { return nullptr; }
#endif
} // namespace r2xsql
-415
View File
@@ -1,415 +0,0 @@
// SPDX-License-Identifier: LicenseRef-Human-Origin-Source-1.0
// Copyright (c) 2024-2026 Elias Bachaalany
//
// This file is licensed under the Human-Origin Source License v1.0.
// See LICENSE.
// R2PipeBackend — spawns radare2(.exe) -q0 and communicates over stdio.
//
// The r2 -q0 protocol: write a command terminated by \n. Read response
// until a NUL (\0) byte is encountered (NUL terminates each reply).
#include <r2xsql/backend.hpp>
#include <cstdlib>
#include <cstring>
#include <memory>
#include <mutex>
#include <string>
#include <string_view>
#include <vector>
#ifdef _WIN32
# ifndef WIN32_LEAN_AND_MEAN
# define WIN32_LEAN_AND_MEAN
# endif
# ifndef NOMINMAX
# define NOMINMAX
# endif
# include <windows.h>
#else
# include <fcntl.h>
# include <signal.h>
# include <spawn.h>
# include <sys/types.h>
# include <sys/wait.h>
# include <unistd.h>
extern char** environ;
#endif
namespace r2xsql {
namespace {
#ifdef _WIN32
struct WinPipe {
HANDLE read = nullptr;
HANDLE write = nullptr;
~WinPipe() {
if (read) CloseHandle(read);
if (write) CloseHandle(write);
}
};
class R2PipeWinBackend : public Backend {
public:
~R2PipeWinBackend() override { shutdown(); }
bool start(const R2PipeOpenOptions& opts, BackendError* err) {
std::string exe = opts.r2_executable.empty() ? "radare2.exe" : opts.r2_executable;
SECURITY_ATTRIBUTES sa{};
sa.nLength = sizeof(sa);
sa.bInheritHandle = TRUE;
sa.lpSecurityDescriptor = nullptr;
HANDLE child_in_r = nullptr, child_in_w = nullptr;
HANDLE child_out_r = nullptr, child_out_w = nullptr;
if (!CreatePipe(&child_in_r, &child_in_w, &sa, 0) ||
!CreatePipe(&child_out_r, &child_out_w, &sa, 0)) {
if (err) err->message = "CreatePipe failed";
return false;
}
SetHandleInformation(child_in_w, HANDLE_FLAG_INHERIT, 0);
SetHandleInformation(child_out_r, HANDLE_FLAG_INHERIT, 0);
std::string cmdline;
cmdline.reserve(256);
cmdline += "\"";
cmdline += exe;
cmdline += "\"";
cmdline += " -q0";
if (opts.write) cmdline += " -w";
if (!opts.analyze) cmdline += " -e bin.cache=true";
for (auto& a : opts.extra_args) {
cmdline += " ";
cmdline += a;
}
if (!opts.path.empty()) {
cmdline += " \"";
cmdline += opts.path;
cmdline += "\"";
} else {
cmdline += " -";
}
STARTUPINFOA si{};
PROCESS_INFORMATION pi{};
si.cb = sizeof(si);
si.dwFlags = STARTF_USESTDHANDLES;
si.hStdInput = child_in_r;
si.hStdOutput = child_out_w;
si.hStdError = GetStdHandle(STD_ERROR_HANDLE);
std::vector<char> cmdbuf(cmdline.begin(), cmdline.end());
cmdbuf.push_back(0);
BOOL ok = CreateProcessA(nullptr, cmdbuf.data(), nullptr, nullptr,
TRUE, CREATE_NO_WINDOW, nullptr, nullptr, &si, &pi);
CloseHandle(child_in_r);
CloseHandle(child_out_w);
if (!ok) {
DWORD e = GetLastError();
CloseHandle(child_in_w);
CloseHandle(child_out_r);
if (err) {
err->message = "CreateProcess failed for ";
err->message += exe;
err->message += " (code ";
err->message += std::to_string(e);
err->message += ")";
}
return false;
}
proc_ = pi.hProcess;
CloseHandle(pi.hThread);
stdin_ = child_in_w;
stdout_ = child_out_r;
// q0 prints a single NUL byte once it's ready. Consume it.
char b;
DWORD got = 0;
if (!ReadFile(stdout_, &b, 1, &got, nullptr) || got != 1 || b != '\0') {
if (err) err->message = "r2 -q0 handshake failed";
shutdown();
return false;
}
if (opts.analyze && opts.project.empty()) {
// Best-effort analysis; ignore output. Some binaries have no need.
(void)cmd("aaa");
}
write_ = opts.write;
project_ = opts.project;
// If a project was requested, check whether it already exists. If yes,
// load it (this restores analysis so we skip `aaa`). If no, run a fresh
// analysis and let `Ps NAME` on close persist the project. Trying to
// load a non-existent project leaves r2 in a half-initialised state, so
// we use `Pl` (list projects) to detect existence first.
if (!project_.empty()) {
bool exists = project_exists_(project_);
if (exists) {
(void)cmd("P " + project_);
} else if (opts.analyze) {
(void)cmd("aaa");
}
}
return true;
}
bool project_exists_(const std::string& name) {
auto list = cmd("Pl");
size_t pos = 0;
while (pos < list.size()) {
size_t nl = list.find('\n', pos);
std::string_view line(list.data() + pos,
(nl == std::string::npos ? list.size() : nl) - pos);
pos = (nl == std::string::npos) ? list.size() : nl + 1;
while (!line.empty() && (line.back() == '\r' || line.back() == ' '))
line.remove_suffix(1);
while (!line.empty() && line.front() == ' ') line.remove_prefix(1);
if (line == name) return true;
}
return false;
}
void shutdown() {
if (stdin_) {
// If we opened in -w with a project name, persist analysis on close.
if (write_ && !project_.empty()) {
std::string save = "Ps " + project_ + "\n";
DWORD w = 0;
WriteFile(stdin_, save.c_str(), (DWORD)save.size(), &w, nullptr);
// Drain the response (NUL-terminated) before quitting.
char drain;
DWORD got = 0;
for (int n = 0; n < 1 << 20; ++n) {
if (!ReadFile(stdout_, &drain, 1, &got, nullptr) || got != 1) break;
if (drain == '\0') break;
}
}
const char quit[] = "q\n";
DWORD w = 0;
WriteFile(stdin_, quit, sizeof(quit) - 1, &w, nullptr);
CloseHandle(stdin_);
stdin_ = nullptr;
}
if (stdout_) {
CloseHandle(stdout_);
stdout_ = nullptr;
}
if (proc_) {
WaitForSingleObject(proc_, 2000);
TerminateProcess(proc_, 0);
CloseHandle(proc_);
proc_ = nullptr;
}
}
std::string cmd(std::string_view command) override {
if (!stdin_ || !stdout_) return {};
std::lock_guard lock(m_);
std::string line(command);
line += '\n';
DWORD written = 0;
if (!WriteFile(stdin_, line.data(), static_cast<DWORD>(line.size()),
&written, nullptr) ||
written != line.size()) {
return {};
}
std::string out;
out.reserve(64);
char buf[4096];
DWORD got = 0;
for (;;) {
if (!ReadFile(stdout_, buf, sizeof(buf), &got, nullptr) || got == 0) break;
// r2 -q0 terminates each reply with a NUL byte.
DWORD nul = static_cast<DWORD>(-1);
for (DWORD i = 0; i < got; ++i) {
if (buf[i] == '\0') { nul = i; break; }
}
if (nul != static_cast<DWORD>(-1)) {
out.append(buf, nul);
break;
}
out.append(buf, got);
}
return out;
}
bool alive() const override { return proc_ != nullptr; }
std::string_view name() const override { return "r2pipe"; }
private:
std::mutex m_;
HANDLE proc_ = nullptr;
HANDLE stdin_ = nullptr;
HANDLE stdout_ = nullptr;
bool write_ = false;
std::string project_;
};
#else // POSIX
class R2PipePosixBackend : public Backend {
public:
~R2PipePosixBackend() override { shutdown(); }
bool start(const R2PipeOpenOptions& opts, BackendError* err) {
int in_p[2] = {-1, -1};
int out_p[2] = {-1, -1};
if (pipe(in_p) != 0 || pipe(out_p) != 0) {
if (err) err->message = "pipe() failed";
return false;
}
std::string exe = opts.r2_executable.empty() ? "radare2" : opts.r2_executable;
posix_spawn_file_actions_t acts;
posix_spawn_file_actions_init(&acts);
posix_spawn_file_actions_adddup2(&acts, in_p[0], STDIN_FILENO);
posix_spawn_file_actions_adddup2(&acts, out_p[1], STDOUT_FILENO);
posix_spawn_file_actions_addclose(&acts, in_p[0]);
posix_spawn_file_actions_addclose(&acts, in_p[1]);
posix_spawn_file_actions_addclose(&acts, out_p[0]);
posix_spawn_file_actions_addclose(&acts, out_p[1]);
std::vector<std::string> args = {exe, "-q0"};
if (opts.write) args.emplace_back("-w");
for (auto& a : opts.extra_args) args.push_back(a);
if (!opts.path.empty()) args.push_back(opts.path);
else args.emplace_back("-");
std::vector<char*> argv;
argv.reserve(args.size() + 1);
for (auto& a : args) argv.push_back(const_cast<char*>(a.c_str()));
argv.push_back(nullptr);
pid_t pid = 0;
int rc = posix_spawnp(&pid, exe.c_str(), &acts, nullptr, argv.data(), environ);
posix_spawn_file_actions_destroy(&acts);
close(in_p[0]);
close(out_p[1]);
if (rc != 0) {
close(in_p[1]);
close(out_p[0]);
if (err) err->message = std::string("posix_spawnp failed: ") + std::strerror(rc);
return false;
}
pid_ = pid;
stdin_ = in_p[1];
stdout_ = out_p[0];
char b;
if (::read(stdout_, &b, 1) != 1 || b != '\0') {
if (err) err->message = "r2 -q0 handshake failed";
shutdown();
return false;
}
if (opts.analyze && opts.project.empty()) (void)cmd("aaa");
write_ = opts.write;
project_ = opts.project;
if (!project_.empty()) {
bool exists = project_exists_(project_);
if (exists) (void)cmd("P " + project_);
else if (opts.analyze) (void)cmd("aaa");
}
return true;
}
bool project_exists_(const std::string& name) {
auto list = cmd("Pl");
size_t pos = 0;
while (pos < list.size()) {
size_t nl = list.find('\n', pos);
std::string_view line(list.data() + pos,
(nl == std::string::npos ? list.size() : nl) - pos);
pos = (nl == std::string::npos) ? list.size() : nl + 1;
while (!line.empty() && (line.back() == '\r' || line.back() == ' '))
line.remove_suffix(1);
while (!line.empty() && line.front() == ' ') line.remove_prefix(1);
if (line == name) return true;
}
return false;
}
void shutdown() {
if (stdin_ >= 0) {
if (write_ && !project_.empty()) {
std::string save = "Ps " + project_ + "\n";
[[maybe_unused]] ssize_t wr = ::write(stdin_, save.c_str(), save.size());
char drain;
for (int n = 0; n < 1 << 20; ++n) {
if (::read(stdout_, &drain, 1) != 1) break;
if (drain == '\0') break;
}
}
const char quit[] = "q\n";
[[maybe_unused]] ssize_t wq = ::write(stdin_, quit, sizeof(quit) - 1);
close(stdin_);
stdin_ = -1;
}
if (stdout_ >= 0) {
close(stdout_);
stdout_ = -1;
}
if (pid_ > 0) {
int status = 0;
for (int i = 0; i < 20; ++i) {
if (waitpid(pid_, &status, WNOHANG) != 0) { pid_ = -1; return; }
usleep(50 * 1000);
}
kill(pid_, SIGKILL);
waitpid(pid_, &status, 0);
pid_ = -1;
}
}
std::string cmd(std::string_view command) override {
if (stdin_ < 0 || stdout_ < 0) return {};
std::lock_guard lock(m_);
std::string line(command);
line += '\n';
ssize_t w = ::write(stdin_, line.data(), line.size());
if (w != static_cast<ssize_t>(line.size())) return {};
std::string out;
char buf[4096];
for (;;) {
ssize_t n = ::read(stdout_, buf, sizeof(buf));
if (n <= 0) break;
ssize_t nul = -1;
for (ssize_t i = 0; i < n; ++i) if (buf[i] == '\0') { nul = i; break; }
if (nul >= 0) { out.append(buf, nul); break; }
out.append(buf, n);
}
return out;
}
bool alive() const override { return pid_ > 0; }
std::string_view name() const override { return "r2pipe"; }
private:
std::mutex m_;
pid_t pid_ = -1;
int stdin_ = -1;
int stdout_ = -1;
bool write_ = false;
std::string project_;
};
#endif
} // namespace
std::unique_ptr<Backend> open_r2pipe(const R2PipeOpenOptions& opts, BackendError* err) {
#ifdef _WIN32
auto b = std::make_unique<R2PipeWinBackend>();
#else
auto b = std::make_unique<R2PipePosixBackend>();
#endif
if (!b->start(opts, err)) return nullptr;
return b;
}
} // namespace r2xsql
+32 -50
View File
@@ -6,26 +6,7 @@
//
// direct.hpp — the seam between a table and radare2's C API.
//
// WHY A SECOND PRODUCER EXISTS AT ALL. Every table is normally filled by
// running a radare2 text command and parsing the JSON it prints. For a handful
// of tables that path was measured to cost two orders of magnitude more than
// reading the same state through libr directly — an `xrefs` full scan over
// ~2000 functions is 2001 commands, ~8 MB of JSON DOM and 2001 cross-CRT
// free()s, and every live table sets `.no_shared_cache()`, so it pays that per
// query. The C-API producer is strictly ADDITIVE: r2pipe spawns a separate
// radare2 process and has no C API at all, so the command path remains the only
// implementation there and the fallback everywhere. It is never deleted.
//
// WHY IT IS A SEPARATE HEADER INSTEAD OF AN `#if` INSIDE tables_xrefs.cpp.
// The core `r2xsql` static library holds all 30 `tables_*.cpp` and deliberately
// links NO radare2 — that is precisely what keeps the pipe-only binary free of
// r_*.dll imports. A C-API call cannot be compiled into a table file, because
// that file is only ever compiled into the library with no radare2 to call.
// The implementations live in `direct_libr.cpp`, compiled into the
// `r2xsql_libr` OBJECT library, and reach the table through `Backend::direct()`.
// 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.
#pragma once
@@ -137,13 +118,6 @@ struct SwitchCaseData {
// live here: each of the four `tables_*.cpp` files and `direct_libr.cpp`
// build this shape from either leg (command JSON or the C API).
//
// NOT here: `RAnalSwitchOp::daddr` (the jump table's own memory address).
// The C API has it, but no r2 command surfaces it (`afbj`'s JSON never emits
// it — verified by reading `print_bb` in `cmd_anal.inc.c`), so populating it
// only on the libr leg would make the two backends disagree on
// `switch_tables.table_addr` for the same query on the same file. It stays
// NULL on both backends by design — the same shape as `funcs.realname` never
// being surfaced above.
struct BlockData {
int64_t addr = 0;
int64_t size = 0;
@@ -183,8 +157,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.
@@ -213,10 +187,6 @@ struct FuncRow {
// the command path (they arrive in the same JSON object) and one call each on
// the C-API path.
//
// NOT here: radare2's `RAnalFunction` also carries a `realname` (the mangled
// form), but **no r2 command emits it**. Surfacing it would give the
// in-process flavor a column the pipe-only flavor can never fill — a query
// that silently returns blanks on one of the two, which is worse than the gap.
int64_t end_addr = 0; // `maxaddr`: past the highest block, NOT addr+size
int64_t ninstrs = 0;
int64_t stackframe = 0;
@@ -282,7 +252,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 +272,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 +294,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 +310,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 +343,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 +351,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,9 +477,21 @@ 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;
// Search executable maps without command expansion; restore search.in afterward.
virtual std::string search(const std::string& command) = 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;
virtual bool type_create(const std::string& name, const std::string& kind) = 0;
virtual bool type_delete(const std::string& name) = 0;
virtual bool io_map_create(int64_t addr, uint64_t size, int64_t paddr,
const std::string& perm, const std::string& name) = 0;
virtual bool io_map_delete(int64_t id) = 0;
virtual void types_scan(std::vector<TypeRow>& out) = 0;
virtual void projects_scan(std::vector<ProjectRow> &out) = 0;
virtual void libs_scan(std::vector<LibRow> &out) = 0;
@@ -557,7 +538,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 +599,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 +641,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
@@ -712,13 +701,6 @@ class Api {
const std::string& type) = 0;
};
// Defined in `direct_libr.cpp` and called from `backend_libr.cpp`. Both are
// compiled only into the `r2xsql_libr` OBJECT library, so the core library
// never references this symbol and a pipe-only build needs no stub for it.
// `core` is an `RCore*`, passed as void* to keep <r_core.h> out of this header;
// `core_mutex` is the owning backend's, so a direct read cannot interleave with
// a command issued on another thread (the in-r2 plugin shares one core between
// its interactive and concurrent callers).
std::unique_ptr<Api> make_libr_api(void* core, std::mutex& core_mutex);
} // namespace r2xsql::direct
+188 -92
View File
@@ -9,11 +9,6 @@
// `backend_libr.cpp`; the core `r2xsql` library links no radare2 and never
// sees this TU.
//
// The contract each producer must honour is stricter than "return the same
// rows": it must return what the corresponding radare2 COMMAND would have
// printed, quirks included, because both backends answer the same SQL and the
// e2e suite compares their rows. Each producer below therefore names the
// emitter it mirrors and the specific quirk it reproduces.
#include "direct.hpp"
#include <nlohmann/json.hpp>
@@ -43,6 +38,18 @@ void for_each_rlist(RList* list, F&& fn) {
}
}
template <typename F>
void for_each_function_block(RAnalFunction* fcn, F&& fn) {
#if R2_ABIVERSION >= 168
RAnalBlock** it;
R_VEC_FOREACH(&fcn->bbs, it) {
if (*it) fn(*it);
}
#else
for_each_rlist<RAnalBlock>(fcn->bbs, fn);
#endif
}
// `afxj` and `axfj` both print `r_anal_ref_type_tostring(ref->type)`
// unconditionally, so a zero-typed ref reads back as the literal "NULL".
std::string ref_type_string(RAnalRefType type) {
@@ -50,21 +57,10 @@ std::string ref_type_string(RAnalRefType type) {
return s ? s : "";
}
// `axtj` is the exception: it guards the field with `if (ref->type)`, so a
// zero-typed ref has NO "type" key and our JSON reader yields "". Reproduce the
// absence rather than printing "NULL", or the two backends disagree on a row
// that neither of them is wrong about.
std::string ref_type_string_axtj(RAnalRefType type) {
return type ? ref_type_string(type) : std::string();
}
// The function containing `addr`, or none. This is the same call `axtj` makes
// to fill its `fcn_addr` field. For the from_addr producer it stands in for the
// command path's `afij @ <addr>`, which filters `core->anal->fcns` by
// `r_anal_function_contains` and takes the first hit; `r_anal_get_fcn_in` takes
// the first of `r_anal_get_functions_in`. The two agree whenever one function
// contains the address, and can only differ where functions overlap — the e2e
// row-level parity check against r2pipe is what would catch that.
void set_owner(XrefRow& r, RCore* core, ut64 addr) {
RAnalFunction* fcn = r_anal_get_fcn_in(core->anal, addr, R_ANAL_FCN_TYPE_NULL);
if (fcn) {
@@ -121,6 +117,44 @@ 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;
}
std::string search(const std::string& command) override {
std::lock_guard lock(mu_);
if (!core_) return {};
RConfigHold* hold = r_config_hold_new(core_->config);
if (!hold) return {};
r_config_hold(hold, "search.in", nullptr);
r_config_set(core_->config, "search.in", "io.maps.x");
char* result = r_core_call_str_at(core_, core_->addr, command.c_str());
r_config_hold_restore(hold);
r_config_hold_free(hold);
std::string out = result ? result : "";
free(result);
return out;
}
bool function_is_start(int64_t addr) override {
std::lock_guard lock(mu_);
return core_ && core_->anal &&
@@ -434,7 +468,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 +480,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);
@@ -544,6 +584,41 @@ class LibrApi : public Api {
free(members);
}
bool type_create(const std::string& name, const std::string& kind) override {
std::lock_guard lock(mu_);
const std::string declaration = kind + " " + name + " {};";
char* error = nullptr;
char* parsed = r_anal_cparse(core_->anal, declaration.c_str(), &error);
free(error);
if (!parsed) return false;
r_anal_save_parsed_type(core_->anal, parsed);
free(parsed);
return sdb_exists(core_->anal->sdb_types, name.c_str());
}
bool type_delete(const std::string& name) override {
std::lock_guard lock(mu_);
r_anal_remove_parsed_type(core_->anal, name.c_str());
return !sdb_exists(core_->anal->sdb_types, name.c_str());
}
bool io_map_create(int64_t addr, uint64_t size, int64_t paddr,
const std::string& perm, const std::string& name) override {
std::lock_guard lock(mu_);
const int fd = r_io_fd_get_current(core_->io);
RIOMap* map = r_io_map_add(core_->io, fd, r_str_rwx(perm.c_str()),
static_cast<ut64>(paddr), static_cast<ut64>(addr), size);
if (!map) return false;
if (!name.empty()) r_io_map_set_name(map, name.c_str());
return true;
}
bool io_map_delete(int64_t id) override {
std::lock_guard lock(mu_);
r_io_map_del(core_->io, static_cast<ut32>(id));
return !r_io_map_get(core_->io, static_cast<ut32>(id));
}
void types_scan(std::vector<TypeRow>& out) override {
std::lock_guard lock(mu_);
if (!core_ || !core_->anal) return;
@@ -598,29 +673,28 @@ class LibrApi : public Api {
});
}
// Mirrors the command path's `aflj` + `afxj @ <fn>` per function.
// Every reference radare2 knows -- the same store `axj` lists and the
// to/from pushdowns read -- with the owner resolved per row, as they do.
// Walking each function's own refs instead missed references whose source
// lies outside every analysed function (a call past a function's last
// block), so a full scan returned fewer rows than `WHERE to_addr = X`.
void xrefs_scan(std::vector<XrefRow>& out) override {
std::lock_guard lock(mu_);
if (!core_ || !core_->anal) return;
for_each_rlist<RAnalFunction>(core_->anal->fcns, [&](RAnalFunction* fcn) {
RVecAnalRef* refs = r_anal_function_get_refs(fcn);
if (!refs) return;
RAnalRef* ref;
R_VEC_FOREACH (refs, ref) {
XrefRow r;
// Refs FROM a function: the varying source is `ref->at`, the target is
// `ref->addr`. This is the OPPOSITE of `r_anal_xrefs_get` below, where
// `at` comes back equal to the queried address on every row. Reading
// `at` for both yields the right row COUNT and entirely wrong data.
r.from_addr = static_cast<int64_t>(ref->at);
r.to_addr = static_cast<int64_t>(ref->addr);
r.set_type(ref_type_string(ref->type));
r.from_func = static_cast<int64_t>(fcn->addr);
r.has_from_func = true;
out.push_back(std::move(r));
}
RVecAnalRef_free(refs);
});
RVecAnalRef* refs = r_anal_refs_get(core_->anal, UT64_MAX);
if (!refs) return;
out.reserve(out.size() + RVecAnalRef_length(refs));
RAnalRef* ref;
R_VEC_FOREACH (refs, ref) {
XrefRow r;
// From the refs side: `at` is the source, `addr` the target.
r.from_addr = static_cast<int64_t>(ref->at);
r.to_addr = static_cast<int64_t>(ref->addr);
r.set_type(ref_type_string(ref->type));
set_owner(r, core_, ref->at);
out.push_back(std::move(r));
}
RVecAnalRef_free(refs);
}
// Mirrors `axtj @ <to_addr>`.
@@ -721,61 +795,87 @@ 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));
#if R2_ABIVERSION >= 168
r.nbbs = static_cast<int64_t>(RVecAnalBlockPtr_length(&fcn->bbs));
#else
r.nbbs = static_cast<int64_t>(r_list_length(fcn->bbs));
#endif
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) {
@@ -814,7 +914,7 @@ class LibrApi : public Api {
// tables use is always keyed on a function START, so mirror that here too
// — an address merely inside a function must not silently answer for it.
if (!fcn || static_cast<int64_t>(fcn->addr) != func_addr) return;
for_each_rlist<RAnalBlock>(fcn->bbs, [&](RAnalBlock* b) {
for_each_function_block(fcn, [&](RAnalBlock* b) {
BlockData d;
fill_block(b, d);
out.push_back(std::move(d));
@@ -830,7 +930,7 @@ class LibrApi : public Api {
if (!core_ || !core_->anal) return;
for_each_rlist<RAnalFunction>(core_->anal->fcns, [&](RAnalFunction* fcn) {
std::vector<BlockData> blocks;
for_each_rlist<RAnalBlock>(fcn->bbs, [&](RAnalBlock* b) {
for_each_function_block(fcn, [&](RAnalBlock* b) {
BlockData d;
fill_block(b, d);
blocks.push_back(std::move(d));
@@ -839,10 +939,6 @@ class LibrApi : public Api {
});
}
// Formats a base register plus a signed delta as "<base>+0x.." /
// "<base>-0x.." -- byte-identical to the command path's
// `storage_ref_of`/`compact_type`-adjacent helper in tables_locals.cpp, so
// the two backends can never disagree on this string's spelling.
static std::string base_delta_ref(const char* base, int64_t delta) {
const std::string b = base ? base : "";
char buf[48];
@@ -983,7 +1079,7 @@ class LibrApi : public Api {
void instructions_for_func(RAnalFunction* fcn, std::vector<InsnRow>& out) {
struct Bb { ut64 addr; int size; };
std::vector<Bb> bbs;
for_each_rlist<RAnalBlock>(fcn->bbs, [&](RAnalBlock* b) {
for_each_function_block(fcn, [&](RAnalBlock* b) {
if (b->size > 0) bbs.push_back({b->addr, static_cast<int>(b->size)});
});
std::sort(bbs.begin(), bbs.end(),
@@ -1034,7 +1130,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 {
-64
View File
@@ -56,33 +56,6 @@ 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);
out.push_back('"');
for (char c : s) {
switch (c) {
case '\\': out += "\\\\"; break;
case '"': out += "\\\""; break;
case '\n': out += "\\n"; break;
case '\r': out += "\\r"; break;
default: out.push_back(c);
}
}
out.push_back('"');
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) {
@@ -248,41 +221,4 @@ std::string base64_encode(std::string_view s) {
return out;
}
std::unordered_map<std::string, std::string> parse_tk_dump(std::string_view tk) {
std::unordered_map<std::string, std::string> out;
size_t pos = 0;
// tk* emits one key=value record per line; type names can contain spaces.
const bool multiline = tk.find('\n') != std::string_view::npos;
auto is_sep = [multiline](char c) { return c == '\n' || (!multiline && c == ' '); };
while (pos < tk.size()) {
size_t end = pos;
while (end < tk.size() && !is_sep(tk[end])) ++end;
std::string_view rec(tk.data() + pos, end - pos);
pos = end + 1;
while (!rec.empty() && rec.front() == '\r') {
rec.remove_prefix(1);
}
while (!rec.empty() && (rec.back() == '\r')) {
rec.remove_suffix(1);
}
if (rec.empty() || rec.front() == '#') continue;
auto eq = rec.find('=');
if (eq == std::string_view::npos) continue;
std::string name(rec.substr(0, eq));
std::string kind(rec.substr(eq + 1));
// Member rows look like `NAME.field=...`; only top-level entries are types.
if (name.find('.') != std::string::npos) continue;
if (kind != "atomic" && kind != "struct" && kind != "union" &&
kind != "enum" && kind != "typedef" && kind != "func" &&
kind != "type") {
continue;
}
out.emplace(std::move(name), std::move(kind));
}
return out;
}
} // namespace r2xsql::json_utils
+3 -35
View File
@@ -10,7 +10,6 @@
#include <cstdint>
#include <string>
#include <string_view>
#include <unordered_map>
#include <vector>
namespace r2xsql::json_utils {
@@ -29,17 +28,6 @@ bool boolean(const nlohmann::json& obj, std::string_view key, bool dflt =
// so the tables don't break when r2 renames a key between versions.
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);
// 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 —
// [A-Za-z0-9_.* space []] . r2's `afvt <name> <type>` takes the type verbatim
@@ -61,23 +49,9 @@ bool valid_c_signature(std::string_view text);
// injecting r2 commands when spliced into `om $d <vaddr> <size> <paddr> <perm>`.
bool valid_perm(std::string_view perm);
// True iff `text` is safe assembly-mnemonic source text to splice unquoted
// into radare2's assembler-family commands (`pa`/`/a`, see tables_assemble.cpp
// and the `patches` table's `asm_text` INSERT column): alnum, space, and the
// punctuation real disassembly/assembly text actually uses
// (`, . + - * / [ ] ( ) : % # _ $`). Rejects every r2 command
// separator/metacharacter (`; @ | & < > " ' backtick newline`, ...), so a
// caller-supplied instruction can never inject a second command -- the same
// allowlist-over-quoting CHOICE tables_rop.cpp's local `valid_gadget_pattern`
// already made for the nearly identical "opstr text" shape (a separate,
// slightly narrower charset there since it only ever matches EXISTING
// disassembly text, never assembles new instructions -- this one is shared
// across `assemble`/`search_asm`/`patches.asm_text`, which all splice
// caller-supplied ASSEMBLY SOURCE, so it lives here in json_utils rather
// than as a fourth local copy). NOT used for `demangle`'s `mangled` input:
// MSVC/Rust manglings routinely need `?`/`@` characters this charset
// excludes, so that table uses Backend::cmd_noeval instead (see
// tables_demangle.cpp).
// Accepted assembly syntax for assemble, search_asm and patches.asm_text.
// This allows $() and # for assembly syntax; it is not a command-escaping
// function. Callers must use literal dispatch or a native API.
bool valid_asm_source_text(std::string_view text);
// Hex address formatter ("0x401000").
@@ -118,11 +92,5 @@ void hex_decode_partial(std::string_view hex, std::vector<uint8_t>& out);
// store the quotes literally.
std::string base64_encode(std::string_view s);
// Parse `tk*` output (sdb format). Each non-empty record is "NAME=KIND".
// Records may be separated by '\n', "\r\n", or ' ' depending on which r2
// command path produced them (libr cmd vs r2pipe interactive). Returns the
// map of top-level type entries (those without '.' in the name) whose kind
// is one of: atomic, struct, union, enum, typedef, func, type.
std::unordered_map<std::string, std::string> parse_tk_dump(std::string_view tk);
} // namespace r2xsql::json_utils
+20 -82
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));
@@ -116,91 +134,11 @@ inline size_t quick_count(Backend* be, direct::Collection collection,
}
}
// Fixed seek anchor for structural no-eval dispatch of a command that is NOT
// address-scoped at all -- currently only radare2's type-system commands,
// `td`/`t-` (see tables_types.cpp's INSERT/DELETE). Each address-scoped
// table write has a real row address to pass to `Backend::cmd_noeval`; the type system has none (a
// struct/union/enum definition is not tied to any address). Verified against
// radare2's own source rather than assumed: `cmd_type.inc.c`'s handlers for
// both `td` (case 'd', the `input[1]==' '` branch) and `t-` (case '-') read
// only from `core->anal` -- never `core->addr` or the current seek -- so
// which address is current during their dispatch is unobservable to them,
// and any fixed value works. `r_core_call_at` (`libr/core/cmd.c`) also skips
// the seek entirely when the target already equals the current position
// (`mustseek = addr != oaddr`), so this constant is not even guaranteed to
// cost a seek; when it does, that cost is the same plain `r_io_seek` +
// block-buffer re-read (restored right after -- `cmd_noeval`'s seek is
// strictly temporary) that every other ported table's write already pays
// whenever its own real address differs from the current position. 0 was
// chosen simply because it needs no session state to produce.
constexpr int64_t kTypeSystemAnchorAddr = 0;
// Structural no-eval dispatch for a command with NO address anywhere in its
// own grammar (unlike `dispatch_write`-shaped helpers elsewhere, which splice
// `@ <addr>` onto the fallback command) -- currently only the type system's
// `td`/`t-`. Tries `Backend::cmd_noeval` first, seeked to
// `kTypeSystemAnchorAddr`; falls back to `fallback_command` VERBATIM (no
// address suffix -- there is nothing to suffix) on every backend that reports
// it unsupported (r2pipe, mock), so those backends keep issuing the exact
// byte-for-byte command text this write path always issued.
//
// `noeval_command` and `fallback_command` may differ: a command whose
// fallback needs r2's own quoting to survive `r_core_cmd0`'s command-chaining
// evaluation (e.g. `td` wrapped in `json_utils::r2_quote`) must NOT be quoted
// for the no-eval path -- `cmd_noeval` already never evaluates that grammar
// (it dispatches through `r_cmd_call`, which does no re-scanning at all), so
// a quote wrapper would be taken as literal argument text instead of being
// stripped the way `r_core_cmd0`'s evaluator strips it.
inline std::string dispatch_type_write(Backend* be, const std::string& noeval_command,
const std::string& fallback_command) {
std::string out;
if (be->cmd_noeval(kTypeSystemAnchorAddr, noeval_command, &out)) return out;
return be->cmd(fallback_command);
}
// Convenience overload for when the same command text is correct on both
// paths (e.g. `t-`, which never needed quoting on either path).
inline std::string dispatch_type_write(Backend* be, const std::string& command) {
return dispatch_type_write(be, command, command);
}
// Assemble `asm_text` at `addr` via radare2's `pa` command (verified live
// against the current radare2 build: `pa <asm>` prints a bare hexpairs line
// on success -- no JSON mode exists, confirmed against `cmd_print.inc.c`'s
// `cmd_pa`/help_msg_pa -- and prints NOTHING to stdout on failure; its
// diagnostic, e.g. "Cannot assemble '<asm>' at line 1", goes to stderr via
// R_LOG_ERROR and is invisible to Backend::cmd()/cmd_noeval's captured
// output, so an empty result is the only failure signal available here).
//
// `addr` matters even for a simple assemble: `cmd_pa` calls
// `r_asm_set_pc(core->rasm, core->addr)` before assembling, so pc-relative
// encodings (rip-relative loads, short branches) depend on which address is
// current. `Backend::cmd_noeval`'s own temporary seek supplies this
// correctly on the libr backend; the pipe/mock fallback uses the plain
// `@ <addr>` seek suffix every other address-scoped write in this project
// already relies on (tables_patches.cpp's `dispatch_write`, etc.).
//
// `asm_text` MUST already pass `json_utils::valid_asm_source_text` before
// this is called (not re-checked here, so a caller can produce its own
// table-specific error message) -- that charset allowlist is what makes
// splicing `asm_text` unquoted into the command safe on every backend, the
// same reasoning `patches`' hex-validated `wx` path and `rop_gadgets`'
// charset-validated `pattern` already established; cmd_noeval is layered on
// top for defense-in-depth (same "wired in anyway... not because a real gap
// was found here" posture tables_patches.cpp documents for its own `wx`),
// not because the charset guarantee alone is insufficient.
//
// Returns false (leaving `out_hex` untouched) when radare2 produced no
// usable output at all -- a bad mnemonic, an operand radare2's assembler
// rejects, or (defensively) a result that isn't valid hex.
// Assemble at addr without command expansion; pa prints hex on success.
inline bool assemble_hex(Backend* be, int64_t addr, const std::string& asm_text,
std::string* out_hex) {
const std::string command = "pa " + asm_text;
std::string out;
if (!be->cmd_noeval(addr, command, &out)) {
out = be->cmd(command + " @ " + json_utils::hex_addr(addr));
}
out = trim_ascii(out);
std::string out = trim_ascii(be->cmd_noeval(addr, command));
if (out.empty() || !json_utils::valid_hex_bytes(out)) return false;
*out_hex = out;
return true;
+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)
+36 -79
View File
@@ -4,68 +4,9 @@
// This file is licensed under the Human-Origin Source License v1.0.
// See LICENSE.
//
// assemble / search_asm -- two genuine gaps closed together since both are
// thin SQL wrappers over radare2's assembler, verified against the CURRENT
// radare2 source rather than guessed:
//
// - `assemble`: one row per query, encodes an assembly-mnemonic string to
// bytes via `pa <asm>` (`cmd_print.inc.c`'s `cmd_pa`) -- "print hexpairs
// of the given assembly expression." No JSON mode exists for `pa`
// (`paj` is not a real subcommand -- verified live: it gets parsed as
// `pa` with asm text "j ...", not a JSON flag); a bare hexpairs line is
// the only success shape, and radare2 prints NOTHING to stdout on
// failure (its "Cannot assemble '<asm>' at line 1" diagnostic is
// stderr-only, invisible to Backend::cmd()/cmd_noeval).
// - `search_asm`: one row per match, assembles `asm` and searches for the
// resulting bytes via the plain (non-JSON) `/a <asm>` command
// (`cmd_search.inc.c`, the `case ' '` branch under `case 'a':`).
// IMPORTANT, verified live: `/aj <asm>` is NOT this command's JSON
// form -- appending `j` directly after `/a` misroutes to the UNRELATED
// `/at` (search-by-instruction-TYPE) family instead (confirmed: `/aj
// push rbp` returned `{"cmd":"/atj","arg":"push rbp","result":[]}`, and
// `/aj mov rdi, rsi` matched by TYPE "mov" rather than assembling the
// literal text at all) -- a real radare2 command-dispatch collision,
// not a bug in this table. So this table parses the plain-text output
// format instead: one `<hex addr> <hitN_M> <hex bytes>` line per match,
// confirmed stable across multiple hits and architectures.
//
// Both commands take a free-text assembly argument that this table
// charset-validates via `json_utils::valid_asm_source_text` (alnum, space,
// and disassembly-safe punctuation) BEFORE it is ever spliced into a
// command string -- the same allowlist-over-quoting choice `byte_search`
// (hex pattern), `patches` (hex bytes), and `rop_gadgets` (opstr pattern)
// already made for their own free-text inputs, verified live to close a
// real hole: `/a push rbp; ?e INJECTED` spliced through a plain
// `Backend::cmd()` call DOES execute the injected `?e` (radare2's own `;`
// chaining) -- exactly the class of bug the charset allowlist prevents by
// construction. `assemble` additionally layers `table_utils::assemble_hex`
// (which tries `Backend::cmd_noeval` first) for defense-in-depth, mirroring
// `patches`' own `wx` write (also charset-safe on its own, cmd_noeval wired
// in anyway) -- `search_asm` does NOT, because its search needs an ATOMIC
// three-command chain (`e search.in=...; /a ...; e search.in=...`, see
// below), which is structurally incompatible with cmd_noeval's single
// no-eval-command dispatch; the charset allowlist alone is what makes that
// chain safe, the identical trade-off `rop_gadgets` already made for the
// exact same reason (see its own file header).
//
// `search_asm` sets `search.in=io.maps.x` (executable maps only) for the
// duration of its own search and restores `io.maps` afterward, in ONE
// `Backend::cmd()` call (never as separate calls with anything else able to
// interleave) -- the identical atomic set/search/restore idiom
// `rop_gadgets` already established for the same reason: `search.in` is
// session-global config that leaks past the command that set it, and this
// project's r2 core is shared across concurrent interactive/remote
// callers, so leaving it mutated (or trusting whatever a PRIOR caller left
// it as) would make this table's results depend on unrelated session
// history.
//
// No C-API producer on either table: an `assemble` call is a single
// assemble operation (no N-per-row cost to collapse), and `search_asm`'s
// cost is dominated by radare2's OWN search algorithm, not by anything this
// table's own command-vs-API choice could change -- the same
// "one full-scan-equivalent command, no per-row shape" reasoning
// `bininfo`/`rop_gadgets` already documented for themselves. Command-path
// only, identical on both backends.
// assemble uses pa; search_asm uses /a and parses its text output.
// /aj selects the instruction-type search family, not a JSON form of /a.
// Search arguments bypass command expansion and search.in is restored natively.
#include <cstdint>
#include <cstdlib>
@@ -180,23 +121,13 @@ std::vector<SearchAsmRow> run_search_asm(Backend* be, const std::string& asm_tex
std::vector<SearchAsmRow> out;
if (asm_text.empty() || !json_utils::valid_asm_source_text(asm_text)) return out;
// Atomic: set scope, search, restore scope -- one Backend::cmd() call, see
// file header. asm_text is already charset-validated above, so splicing it
// unquoted here can never break out of this command chain.
const std::string atomic =
"e search.in=io.maps.x; /a " + asm_text + "; e search.in=io.maps";
const std::string text = be->cmd(atomic);
auto* api = be->direct();
const std::string command = "/a " + asm_text;
const std::string text = api ? api->search(command) : be->cmd("'" + command);
size_t pos = 0;
while (pos <= text.size()) {
size_t nl = text.find('\n', pos);
// Trim trailing '\r' (and any other whitespace) before parsing: the
// r2pipe backend spawns radare2 as a subprocess and its captured stdout
// can carry "\r\n" line endings even though the in-process libr backend's
// r_cons output never does -- a trailing '\r' left on `hex_tok` would
// fail json_utils::valid_hex_bytes for every single line (verified live:
// this table returned 0 rows on r2pipe vs 11 on libr for the identical
// search until this trim was added).
const std::string line = table_utils::trim_ascii(
text.substr(pos, nl == std::string::npos ? std::string::npos : nl - pos));
SearchAsmRow row;
@@ -233,11 +164,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 +217,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;
}
}
-11
View File
@@ -7,17 +7,6 @@
// fact). Columns: (key TEXT, value TEXT, type TEXT), type in {string, hex,
// bool, int}.
//
// Carries the raw bin-info fields from `iIj` (arch, bits, bintype, os, baddr,
// endian, compiler, lang, ...) plus these r2xsql identity / entry / count keys:
// r2xsql_version — tool version (raw key; tool_version is the canonical dup)
// radare2_version — running engine version from r_core_version, or `?Vj`
// over r2pipe; independent of r2xsql's build-time version.
// file — input file path (ij -> core.file)
// entry — program entry vaddr (iej[0].vaddr)
// entry_name — flag/name at the entry (fd @ entry)
// func_count / string_count / import_count / section_count / segment_count /
// symbol_count — JSON-array lengths, so they match the corresponding tables
// summary — a one-line synthesized digest
//
// Canonical core keys (family-shared names, values aliased from the raw
// radare2 facts above): tool_name, tool_version, processor (=arch),
+3 -2
View File
@@ -3,7 +3,6 @@
//
// This file is licensed under the Human-Origin Source License v1.0.
// See LICENSE.
// Binary metadata from the shared native source or the r2pipe fallback.
#include <r2xsql/session.hpp>
#include <r2xsql/backend.hpp>
@@ -86,7 +85,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));
})
-15
View File
@@ -8,16 +8,6 @@
// `afbj @ X`. An unfiltered scan walks `aflj` and enumerates every function's
// blocks lazily.
//
// A second, faster producer reads libr's analysis state through the C API
// instead of running the command and parsing its JSON (see `direct.hpp`).
// The command path below is NOT a legacy leftover: it is the only
// implementation r2pipe can have, since that backend drives a separate
// radare2 process. Both must return the same rows, which the e2e suite
// checks row by row across the two backends. `blocks`/`cfg_edges`/
// `switch_tables`/`dominators`/`post_dominators`/`loops` all share the
// identical `aflj` + N×`afbj` full scan today; each is ported independently
// here, reading the same `direct::BlockData` shape `direct.hpp` defines once
// for all six.
#include <memory>
#include <utility>
@@ -65,11 +55,6 @@ std::vector<BlockRow> blocks_for_func(Backend* be, int64_t fa) {
return out;
}
// Full-scan: on the direct backend, one `blocks_scan()` call replaces the
// `aflj` + N×`afbj @` round-trips entirely; on the command backend it is
// exactly the original lazy `aflj` then `afbj @` per function, streamed one
// block at a time — UNCHANGED from before this port, since r2pipe has no C
// API and must keep paying for each command as it goes.
class BlocksGen : public xsql::Generator<BlockRow> {
Backend* be_;
bool use_api_ = false;
+11 -29
View File
@@ -13,7 +13,7 @@
// (libr) backend — the same structural no-eval idiom piloted on `comments`
// and rolled out to `funcs`/`flags`. INSERT's `f` and DELETE's `f-` are
// address-scoped exactly like those tables' writes, so they use the same
// `dispatch_write(be, addr, command)` shape.
// `be->cmd_noeval(addr, command)` shape.
//
// `fr <old> <new>` (the rename) is the same odd one out `flags` already hit:
// it carries no address anywhere in its own grammar at all — radare2
@@ -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"
@@ -52,17 +53,6 @@ struct BookmarkRow {
std::string name;
};
// Dispatch a write command scoped to `addr`. Tries the structural no-eval
// path first (libr backend only, via Backend::cmd_noeval); falls back to the
// original command-string form (`command @ addr`) on every backend that
// reports it unsupported, so r2pipe's behavior and command text stay
// byte-for-byte unchanged. Same helper shape as the `flags`/`funcs` tables'
// rollout of this dispatch.
std::string dispatch_write(Backend* be, int64_t addr, const std::string& command) {
std::string out;
if (be->cmd_noeval(addr, command, &out)) return out;
return be->cmd(command + " @ " + json_utils::hex_addr(addr));
}
class BookmarkSpaceScope {
public:
@@ -126,24 +116,15 @@ 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;
BookmarkSpaceScope scope(*be);
// `fr` takes no address at all (see the file header for the full
// reasoning) — dispatch it through cmd_noeval anyway, seeked to this
// row's own `r.addr`, which the command never consults; a plain
// Backend::cmd_noeval-unsupported backend (r2pipe, mock) falls back to
// the exact same bare `fr <old> <new>` string this table has always
// issued, with no `@ <addr>` ever spliced onto it.
std::string cmd = "fr " + r.name + " " + v;
std::string out;
if (!be->cmd_noeval(r.addr, cmd, &out)) {
out = be->cmd(cmd);
}
std::string out = be->cmd_noeval(r.addr, cmd);
if (std::string e = table_utils::command_error_text(out); !e.empty()) {
xsql::set_vtab_error("bookmarks: rename failed: " + e);
return false;
@@ -157,7 +138,7 @@ void register_bookmarks_table(Session& s) {
})
.deletable([be = s.backend_ptr()](BookmarkRow& r) -> bool {
BookmarkSpaceScope scope(*be);
const std::string out = dispatch_write(be, r.addr, "f-");
const std::string out = be->cmd_noeval(r.addr, "f-");
if (std::string e = table_utils::command_error_text(out); !e.empty()) {
xsql::set_vtab_error("bookmarks: delete failed: " + e);
return false;
@@ -174,20 +155,21 @@ 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);
const std::string out =
dispatch_write(be, addr, std::string("f ") + name);
be->cmd_noeval(addr, std::string("f ") + name);
if (std::string e = table_utils::command_error_text(out); !e.empty()) {
xsql::set_vtab_error("bookmarks: insert failed: " + e);
return false;
+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")
+9 -38
View File
@@ -272,27 +272,6 @@ class BytesWindowIterator : public xsql::RowIterator {
int64_t rowid() const override { return rows_[i_].addr; }
};
// Dispatch the `wx` write scoped to `addr`. Tries the structural no-eval path
// first (libr backend only, via Backend::cmd_noeval); falls back to the
// original command-string form (`wx <hex> @ <addr>`) on every backend that
// reports it unsupported (r2pipe, mock), so r2pipe's command text stays
// byte-for-byte what it always was. Same helper shape as every other
// address-scoped write in this project (comments/funcs/flags/bookmarks/
// patches). Note this dispatches ONLY the `wx` itself -- the io.cache
// save/set/restore around it (see write_byte_direct below) is issued as its
// own plain Backend::cmd() calls, never folded into this one, because
// cmd_noeval sends `command` straight to `wx`'s own handler WITHOUT
// evaluating `;` chaining at all (that is the whole point of the primitive)
// -- so a chained "e io.cache=false; wx ...; e io.cache=true" string could
// never survive a cmd_noeval call; it would be handed to the `e` handler as
// one garbled argument. The two concerns (no-eval dispatch of `wx` itself,
// and atomically bracketing it with the cache toggle) are orthogonal and
// each gets its own mechanism below.
std::string dispatch_write(Backend* be, int64_t addr, const std::string& command) {
std::string out;
if (be->cmd_noeval(addr, command, &out)) return out;
return be->cmd(command + " @ " + json_utils::hex_addr(addr));
}
// UPDATE bytes SET value = X WHERE addr = Y -- a direct, immediate, ONE-BYTE
// overwrite via `wx`. This is deliberately separate from the `patches` table
@@ -333,21 +312,6 @@ std::string dispatch_write(Backend* be, int64_t addr, const std::string& command
// restore-value bundling, which does not apply here since io.cache's
// restore target is a value read at runtime, not a hardcoded constant.
//
// The byte value is validated to a strict 0-255 integer BEFORE any of this
// runs, so the only text ever spliced into `wx` is exactly two hex digits
// derived from that already-range-checked int -- never raw user text. That
// validation is the layer that actually closes any injection surface here;
// `dispatch_write`'s cmd_noeval attempt below is wired in for consistency
// with every other address-scoped write in this project (comments/funcs/
// flags/bookmarks/patches), not because there is a free-text argument left
// for it to protect -- same "nothing new to harden, wired in anyway"
// finding already recorded for `patches`' INSERT.
// Physically re-reads exactly one byte via `p8` (bypassing the
// is_initialized-gated `has_value` machinery `window_rows` normally applies)
// so write verification checks what is actually AT the address, not this
// table's static, write-independent classification of whether that address
// falls in a raw-file-backed section. See write_byte_direct's caller for why
// this distinction matters.
bool read_one_byte(Backend* be, int64_t addr, int* out_value) {
std::vector<int> raw = read_p8(be, addr, 1);
if (raw.empty()) return false;
@@ -362,7 +326,7 @@ bool write_byte_direct(Backend* be, int64_t addr, int value, std::string* err_ou
const std::string prior = table_utils::trim_ascii(be->cmd("e io.cache"));
be->cmd("e io.cache=false");
const std::string out = dispatch_write(be, addr, "wx " + hex);
const std::string out = be->cmd_noeval(addr, "wx " + hex);
// Restore unconditionally, even on failure below -- io.cache must never be
// left disabled by a failed write. radare2's own default is "false" (see
// cconfig.c's SETCB), used only if the read above somehow came back empty.
@@ -395,7 +359,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,7 +3,6 @@
//
// This file is licensed under the Human-Origin Source License v1.0.
// See LICENSE.
// Native reads for libr; command fallback for r2pipe and mock.
#include <r2xsql/session.hpp>
#include <r2xsql/backend.hpp>
-9
View File
@@ -9,15 +9,6 @@
// equality filter is pushed down to a single `afbj @ X`; an unfiltered scan
// walks `aflj` and enumerates every function's edges lazily.
//
// A second, faster producer reads libr's analysis state through the C API
// instead of running the command and parsing its JSON (see `direct.hpp`).
// The command path below is NOT a legacy leftover: it is the only
// implementation r2pipe can have. Both must return the same rows, which the
// e2e suite checks row by row across the two backends. `blocks`/`cfg_edges`/
// `switch_tables`/`dominators`/`post_dominators`/`loops` all share the
// identical `aflj` + N×`afbj` full scan today; each is ported independently,
// reading the same `direct::BlockData` shape `direct.hpp` defines once for
// all six.
#include <memory>
#include <unordered_set>
-1
View File
@@ -3,7 +3,6 @@
//
// This file is licensed under the Human-Origin Source License v1.0.
// See LICENSE.
// Native reads for libr; command fallback for r2pipe and mock.
#include <r2xsql/session.hpp>
#include <r2xsql/backend.hpp>
+5 -39
View File
@@ -3,18 +3,7 @@
//
// This file is licensed under the Human-Origin Source License v1.0.
// See LICENSE.
// comments — `CCj` for read; `CCu <text> @ <addr>` for UPDATE; `CC- @ <addr>`
// for DELETE. Writes go through Backend so any backend (libr/r2pipe) works.
//
// Pilot table for the structural no-eval write dispatch (Backend::cmd_noeval):
// every write here is already address-scoped (`@ <addr>`), which maps
// directly onto that primitive's (addr, command) shape. On the libr backend,
// each write below dispatches straight to its command handler instead of
// through radare2's separator-evaluating command grammar; on every other
// backend (r2pipe, mock) cmd_noeval reports "unsupported" and the exact same
// `cmd()` + spliced `@ <addr>` string this table always used runs unchanged.
// The existing input handling (base64-encoding the comment text) is kept
// as-is — this is an additional, structural layer, not a replacement for it.
//
// The READ path is a separate, later change: `CCj` is already a single
// full-scan command (not an N+1-per-function shape), and reading radare2's
@@ -27,17 +16,6 @@
// for the full citation. This is unrelated to, and does not modify, the
// cmd_noeval write path above.
//
// 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
// 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
// reproduces `CCu`'s own dedup-skip quirk (see `Api::comment_set` in
// `direct.hpp`). Proven live, not just cited: a substring-of-existing-text
// UPDATE is a silent no-op on both legs; a genuine overwrite lands on both.
// On every non-libr backend (r2pipe, mock), `Backend::direct()` returns
// nullptr and the exact `cmd_noeval`/`cmd()` fallback above runs unchanged.
#include <r2xsql/session.hpp>
#include <r2xsql/backend.hpp>
@@ -70,15 +48,6 @@ void build_comments(Backend* be, std::vector<CommentRow>& rows) {
}
}
// Dispatch a write command scoped to `addr`. Tries the structural no-eval
// path first (libr backend only); falls back to the original command-string
// form (`command @ addr`) on every backend that reports it unsupported, so
// r2pipe's behavior and command text are byte-for-byte unchanged.
std::string dispatch_write(Backend* be, int64_t addr, const std::string& command) {
std::string out;
if (be->cmd_noeval(addr, command, &out)) return out;
return be->cmd(command + " @ " + json_utils::hex_addr(addr));
}
} // namespace
void register_comments_table(Session& s) {
@@ -118,12 +87,8 @@ void register_comments_table(Session& s) {
if (v) r.text = v;
return true;
}
// Fallback (r2pipe, mock): CCu takes the comment verbatim; pass it as
// base64 so spaces, quotes, and `@` can't corrupt the comment or
// inject extra r2 commands. Kept even with the structural dispatch
// below: defense-in-depth, not either/or.
std::string cmd = "CCu base64:" + json_utils::base64_encode(text);
if (std::string e = table_utils::command_error_text(dispatch_write(be, r.addr, cmd));
if (std::string e = table_utils::command_error_text(be->cmd_noeval(r.addr, cmd));
!e.empty()) {
xsql::set_vtab_error("comments: UPDATE failed: " + e);
return false;
@@ -132,7 +97,7 @@ void register_comments_table(Session& s) {
return true;
})
.deletable([be = s.backend_ptr()](CommentRow& r) -> bool {
if (std::string e = table_utils::command_error_text(dispatch_write(be, r.addr, "CC-"));
if (std::string e = table_utils::command_error_text(be->cmd_noeval(r.addr, "CC-"));
!e.empty()) {
xsql::set_vtab_error("comments: DELETE failed: " + e);
return false;
@@ -145,7 +110,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()) {
@@ -157,7 +123,7 @@ void register_comments_table(Session& s) {
return true;
}
std::string cmd = "CCu base64:" + json_utils::base64_encode(body);
if (std::string e = table_utils::command_error_text(dispatch_write(be, addr, cmd));
if (std::string e = table_utils::command_error_text(be->cmd_noeval(addr, cmd));
!e.empty()) {
xsql::set_vtab_error("comments: INSERT failed: " + e);
return false;
+4 -1
View File
@@ -140,7 +140,10 @@ void build_data_items(Backend* be, std::vector<DataItemRow>& rows) {
r.has_value = !r.value_repr.empty();
r.segment_name = j.section;
r.has_segment = !r.segment_name.empty();
if (!r.has_size) { r.size = j.size; r.has_size = true; }
// Mach-O symbols and ELF mapping symbols (e.g. ARM's $d) can have
// unknown/zero size. Preserve a known symbol size, otherwise use the
// string's byte size, including its terminator.
if (!r.has_size || r.size <= 0) { r.size = j.size; r.has_size = true; }
}
rows.reserve(by_addr.size());
+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;
}
}
+12 -56
View File
@@ -18,25 +18,12 @@
// name/realname/size/comment/alias/addr and nothing else). So the space has to
// be established by enumerating flagspaces and listing each one.
//
// 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
// 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
// producer lives in another TU). `r_flag_foreach` needs no `fs` selection or
// session-state save/restore at all — it is not scoped by the
// currently-selected flagspace the way the `fj` COMMAND is — so the port also
// deletes the enumeration loop's own atomic-select/list/restore workaround
// entirely on the ported leg. The command path remains the only
// implementation r2pipe can have.
//
// Writes (INSERT's `f`, DELETE's `f-`, and the rename's `fr`) dispatch through
// Backend::cmd_noeval on the in-process (libr) backend — the same structural
// no-eval idiom piloted on `comments` and rolled out to `funcs`. `f`/`f-` are
// address-scoped exactly like those tables' writes, so they use the same
// `dispatch_write(be, addr, command)` shape.
// `be->cmd_noeval(addr, command)` shape.
//
// `fr <old> <new>` is the odd one out: unlike every other write in this
// family, it carries NO address anywhere in its own grammar at all — radare2
@@ -60,23 +47,12 @@
// to be) would still need a genuine addr-less primitive; that remains
// 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
// 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
// exact parity — `fr`'s own handler (`cmd_flag.inc.c`) was read and found to
// be nothing more than the same `r_flag_get` (by name) + `r_flag_rename` the
// API leg calls directly, unlike `funcs.name`'s `afn` (see that table's
// comment for why THAT rename was refused). The C-API leg needs no address
// at all — see `Api::flag_rename` in `direct.hpp`. On every non-libr backend
// (r2pipe, mock), `Backend::direct()` returns nullptr and the exact `fr`
// dispatch above runs unchanged.
#include <r2xsql/session.hpp>
#include <r2xsql/backend.hpp>
#include <xsql/vtable.hpp>
#include <r_util.h>
#include "tables.hpp"
#include "direct.hpp"
#include "json_utils.hpp"
@@ -92,17 +68,6 @@ namespace r2xsql {
namespace {
using direct::FlagRow;
// Dispatch a write command scoped to `addr`. Tries the structural no-eval
// path first (libr backend only, via Backend::cmd_noeval); falls back to the
// original command-string form (`command @ addr`) on every backend that
// reports it unsupported, so r2pipe's behavior and command text stay
// byte-for-byte unchanged. Same helper shape as the `comments`/`funcs`
// tables' pilot of this dispatch.
std::string dispatch_write(Backend* be, int64_t addr, const std::string& command) {
std::string out;
if (be->cmd_noeval(addr, command, &out)) return out;
return be->cmd(command + " @ " + json_utils::hex_addr(addr));
}
void build_flags(Backend* be, std::vector<FlagRow>& rows) {
auto arr = be->cmd_json("fj");
@@ -127,7 +92,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 +128,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;
@@ -210,18 +175,8 @@ void register_flags_table(Session& s) {
r.name = v;
return true;
}
// Fallback (r2pipe, mock): `fr` takes no address at all (see the file
// header for the full reasoning) — dispatch it through cmd_noeval
// anyway, seeked to this row's own `r.addr`, which the command never
// consults; a plain Backend::cmd_noeval-unsupported backend (r2pipe,
// mock) falls back to the exact same bare `fr <old> <new>` string
// this table has always issued, with no `@ <addr>` ever spliced onto
// it.
std::string cmd = std::string("fr ") + r.name + " " + v;
std::string out;
if (!be->cmd_noeval(r.addr, cmd, &out)) {
out = be->cmd(cmd);
}
std::string out = be->cmd_noeval(r.addr, cmd);
if (std::string e = table_utils::command_error_text(out); !e.empty()) {
xsql::set_vtab_error("flags: rename failed (addr=" +
json_utils::hex_addr(r.addr) + "): " + e);
@@ -240,7 +195,7 @@ void register_flags_table(Session& s) {
.column_text ("realname", [](const FlagRow& r) { return r.realname; })
.column_text ("space", [](const FlagRow& r) { return r.space; })
.deletable([be = s.backend_ptr()](FlagRow& r) -> bool {
std::string out = dispatch_write(be, r.addr, "f-");
std::string out = be->cmd_noeval(r.addr, "f-");
if (std::string e = table_utils::command_error_text(out); !e.empty()) {
xsql::set_vtab_error("flags: DELETE failed (addr=" +
json_utils::hex_addr(r.addr) + "): " + e);
@@ -254,18 +209,19 @@ 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);
std::string out = be->cmd_noeval(addr, std::string("f ") + name);
if (std::string e = table_utils::command_error_text(out); !e.empty()) {
xsql::set_vtab_error(std::string("flags: INSERT failed (name=") + name + "): " + e);
return false;
+141 -78
View File
@@ -6,22 +6,9 @@
// funcs — function inventory via `aflj`. Each row is one analysed function;
// the JSON is re-issued per query so the view is always live.
//
// Writes (`name` rename via `afn`, `prototype` via `afs`, `calltype` via
// `afc`, DELETE via `af-`, INSERT's `af`/`afn`) are all address-scoped, so on
// the in-process (libr) backend each dispatches through Backend::cmd_noeval —
// 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`,
// 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
// table always issued runs unchanged. Same shape as the `comments` table's
// 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 +19,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 +37,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 +59,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())) ||
@@ -182,17 +245,6 @@ bool calltype_readback_matches(
return func && json_utils::str(*func, "calltype") == expected;
}
// Dispatch a write command scoped to `addr`. Tries the structural no-eval
// path first (libr backend only, via Backend::cmd_noeval); falls back to the
// original command-string form (`command @ addr`) on every backend that
// reports it unsupported, so r2pipe's behavior and command text stay
// byte-for-byte unchanged. Same helper shape as the `comments` table's pilot
// of this dispatch.
std::string dispatch_write(Backend* be, int64_t addr, const std::string& command) {
std::string out;
if (be->cmd_noeval(addr, command, &out)) return out;
return be->cmd(command + " @ " + json_utils::hex_addr(addr));
}
} // namespace
void register_funcs_table(Session& s) {
@@ -202,13 +254,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,12 +280,12 @@ 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);
std::string out = be->cmd_noeval(r.addr, std::string("afn ") + v);
if (std::string e = table_utils::command_error_text(out); !e.empty()) {
xsql::set_vtab_error("funcs: rename failed (addr=" +
json_utils::hex_addr(r.addr) + "): " + e);
@@ -264,7 +326,7 @@ void register_funcs_table(Session& s) {
return false;
}
if (cc == r.calltype) return true; // no-op replay
std::string out = dispatch_write(be, r.addr, std::string("afc ") + cc);
std::string out = be->cmd_noeval(r.addr, std::string("afc ") + cc);
if (std::string e = table_utils::command_error_text(out); !e.empty()) {
xsql::set_vtab_error("funcs: set calltype failed (addr=" +
json_utils::hex_addr(r.addr) + "): " + e);
@@ -308,7 +370,7 @@ void register_funcs_table(Session& s) {
if (compact_signature(r.prototype) == compact_signature(sig)) {
return true; // no-op replay (rename-only update)
}
std::string out = dispatch_write(be, r.addr, std::string("afs ") + sig);
std::string out = be->cmd_noeval(r.addr, std::string("afs ") + sig);
if (std::string e = table_utils::command_error_text(out); !e.empty()) {
xsql::set_vtab_error("funcs: set prototype failed (addr=" +
json_utils::hex_addr(r.addr) + "): " + e);
@@ -337,16 +399,16 @@ 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 `@`
// (unlike afn/afs/af above), so this can't reuse `dispatch_write`'s
// generic "command @ addr" fallback shape unchanged; the no-eval leg is
// the same (addr passed structurally, never spliced into `command`).
std::string out;
if (!be->cmd_noeval(r.addr, "af-", &out)) {
out = be->cmd("af- " + json_utils::hex_addr(r.addr));
}
std::string out = be->cmd_noeval(r.addr, "af-");
if (std::string e = table_utils::command_error_text(out); !e.empty()) {
xsql::set_vtab_error("funcs: DELETE failed (addr=" +
json_utils::hex_addr(r.addr) + "): " + e);
@@ -362,17 +424,18 @@ 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()) {
if (std::string e = table_utils::command_error_text(be->cmd_noeval(addr, "af")); !e.empty()) {
xsql::set_vtab_error("funcs: INSERT failed to define a function at " + at + ": " + e);
return false;
}
// 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)) {
(void)dispatch_write(be, addr, std::string("afn ") + name);
if (name && *name && r_name_check(name)) {
(void)be->cmd_noeval(addr, std::string("afn ") + name);
}
}
return true;
+1 -7
View File
@@ -3,12 +3,6 @@
//
// This file is licensed under the Human-Origin Source License v1.0.
// See LICENSE.
// grep — composite view over funcs/flags/imports/exports/sections/comments.
// Each row tags its source via `kind`; native records (or r2pipe command
// results) are read per query, so the view is always live. The `pattern` pseudo-column
// pushes a case-insensitive SQL `LIKE`-style filter down: `WHERE pattern = 'x%'`
// matches `name`/`full_name` without materialising the full cache. An
// unfiltered scan returns every entity flat.
#include <cctype>
#include <memory>
@@ -69,7 +63,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 -30
View File
@@ -17,24 +17,6 @@
// know which function (if any) owns the addresses it walks, so `func_addr`
// is always 0 on rows this pushdown produces.
//
// A THIRD axis, orthogonal to which of the two shapes above ran: PROJECTION
// GATING. This table conflates two kinds of column — REPORTED DATA (`addr`,
// `size`, `bytes`, `func_addr`, `esil`, `op_type`, `family`, plus the
// client-computed `seq`/`canonical_op`) and RENDERED TEXT (`disasm`, and
// `mnemonic` derived from it — radare2's own disassembly renderer, no
// exported C-API entry point). Whenever a query's projection does NOT read
// `disasm`/`mnemonic`, every producer below (full scan, `func_addr`
// pushdown, `start_addr`/`count` pushdown) uses a fast C-API leg instead
// (`direct::Api::instructions_scan`/`instructions_in_func`/
// `instructions_range`, `direct_libr.cpp`) — measured at 3.97-8.11x with
// exact field parity on every reported-data column in a private standalone
// benchmark before this port was built. A query that DOES read either
// rendered-text column always falls back to the unchanged command path
// below, on every producer shape, so `disasm`/`mnemonic` stay exactly what
// radare2's own renderer produces — never approximated. The pipe backend
// has no C-API leg at all
// (`Backend::direct()` is null there), so it always uses the command path
// regardless of projection — unaffected by this gate.
#include <cstdint>
@@ -162,12 +144,6 @@ constexpr uint64_t kColUsedAll = 1ull << 63;
constexpr uint64_t kMnemonicColBit = 1ull << 2;
constexpr uint64_t kDisasmColBit = 1ull << 3;
// True whenever the query's projection could read RENDERED TEXT
// (`mnemonic`/`disasm` — see the file header). Every producer below stays on
// the unchanged command path whenever this is true, so rendered text is
// always exactly what radare2's own renderer produces; only when false, AND
// a C-API leg is available (`Backend::direct()` non-null — never true for
// the pipe backend), does a producer switch to the fast leg.
bool needs_render(uint64_t col_used) {
return (col_used & kColUsedAll) || (col_used & kMnemonicColBit) ||
(col_used & kDisasmColBit);
@@ -322,10 +298,6 @@ class InsnsInFuncIterator : public xsql::RowIterator {
void register_instructions_table(Session& s) {
auto def = xsql::generator_table<InsnRow>("instructions")
.estimate_rows([]() -> size_t { return 100000; })
// Full scan: projection-gated between the command path (InsnsGen, lazy)
// and the C-API leg (InsnsVecGen over instructions_scan) — see the file
// header. The pipe backend's Backend::direct() is always null, so it
// always takes the command-path branch regardless of colUsed.
.projection_generator([be = s.backend_ptr()](uint64_t col_used)
-> std::unique_ptr<xsql::Generator<InsnRow>> {
if (!needs_render(col_used)) {
@@ -373,8 +345,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));
+18 -46
View File
@@ -3,13 +3,7 @@
//
// This file is licensed under the Human-Origin Source License v1.0.
// See LICENSE.
// io_maps — radare2 IO maps (`omj`). This is r2's *writable* memory map: the
// read-only `segments`/`sections` tables come from the binary headers, whereas
// IO maps are the live address space you can add to, rebase, and remove.
// read : `omj`
// insert : `om $d <vaddr> <size> <paddr> <perm> [name]`
// delete : `om-<map_id>`
// Adding/removing an IO map round-trips through `omj` immediately.
// io_maps — read and mutate the live address space through RIO.
#include <r2xsql/session.hpp>
#include <r2xsql/backend.hpp>
@@ -24,16 +18,6 @@ namespace r2xsql {
namespace {
using direct::IoMapRow;
// Build `om $d <vaddr> <size> <paddr> <perm> [name]` from an IO-map row spec.
std::string make_om_command(int64_t start, int64_t end, int64_t paddr,
const std::string& perm, const std::string& name) {
const int64_t size = end - start;
std::string cmd = "om $d " + json_utils::hex_addr(start) + " " +
json_utils::hex_addr(size) + " " + json_utils::hex_addr(paddr);
if (!perm.empty()) cmd += " " + perm;
if (!name.empty()) cmd += (perm.empty() ? " rwx " : " ") + name;
return cmd;
}
} // namespace
void register_io_maps_table(Session& s) {
@@ -45,21 +29,6 @@ void register_io_maps_table(Session& s) {
.cache_builder([be = s.backend_ptr()](std::vector<IoMapRow>& rows) {
if (auto* api = be->direct()) {
api->io_maps_scan(rows);
return;
}
auto arr = be->cmd_json("omj");
if (!arr.is_array()) return;
rows.reserve(arr.size());
for (auto& j : arr) {
IoMapRow r;
r.start_addr = json_utils::i64(j, "from");
r.end_addr = json_utils::i64(j, "to") + 1; // r2 "to" is inclusive
r.paddr = json_utils::i64(j, "delta");
r.map_id = json_utils::i64(j, "map");
r.fd = json_utils::i64(j, "fd");
r.perm = json_utils::str(j, "perm");
r.name = json_utils::str(j, "name");
rows.push_back(std::move(r));
}
})
.column_int64("start_addr", [](const IoMapRow& r) { return r.start_addr; })
@@ -70,10 +39,10 @@ void register_io_maps_table(Session& s) {
.column_int64("map_id", [](const IoMapRow& r) { return r.map_id; })
.column_int64("fd", [](const IoMapRow& r) { return r.fd; })
.deletable([be = s.backend_ptr()](IoMapRow& r) -> bool {
std::string out = be->cmd("om-" + std::to_string(r.map_id));
if (std::string e = table_utils::command_error_text(out); !e.empty()) {
auto* api = be->direct();
if (!api || !api->io_map_delete(r.map_id)) {
xsql::set_vtab_error("io_maps: DELETE failed (map_id=" +
std::to_string(r.map_id) + "): " + e);
std::to_string(r.map_id) + ")");
return false;
}
return true;
@@ -85,8 +54,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,16 +71,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)");
return false;
}
}
const int64_t paddr = (argc > 4 && !argv[4].is_null()) ? argv[4].as_int64() : 0;
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 "
"$d resolves to a valid fd): " + e);
int64_t paddr = 0;
if (argc > 4 && !argv[4].is_null() &&
!table_utils::exact_write_int64(argv[4], "io_maps: paddr", paddr)) {
return false;
}
auto* api = be->direct();
const uint64_t size = static_cast<uint64_t>(end) - static_cast<uint64_t>(start);
if (!api || !api->io_map_create(start, size, paddr, perm, name)) {
xsql::set_vtab_error("io_maps: INSERT failed (check a file is open)");
return false;
}
return true;
-1
View File
@@ -3,7 +3,6 @@
//
// This file is licensed under the Human-Origin Source License v1.0.
// See LICENSE.
// Native reads for libr; command fallback for r2pipe and mock.
#include <r2xsql/session.hpp>
#include <r2xsql/backend.hpp>
+7 -57
View File
@@ -11,48 +11,8 @@
// to batch: the table is a plain no_shared_cache table (no commit hook — r2xsql
// declined the libxsql commit hook for exactly this reason).
//
// 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
// 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
// stays the implementation of the single-function readback used to verify a
// rename/retype landed (a scoped read, not the wide scan the port targets).
// The table stays a plain writable cached table (no filter_eq): a filter_eq
// would disable the positional mutation snapshot that keeps a multi-row
// UPDATE correct (see query_scoped_uses_mutation_snapshot: it requires
// filters.empty()).
//
// 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
// 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
// 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
// backend (r2pipe, mock), `Backend::direct()` returns nullptr and the command
// path below runs.
//
// That command-path fallback (the `else` leg when `Backend::direct()` is
// null — the pipe backend's only implementation, and the libr backend's
// fallback whenever the C-API leg above is unavailable) now ALSO dispatches
// through `Backend::cmd_noeval` on the in-process (libr) backend, closing a
// gap this table was the last one to carry: both `afvn` (rename) and `afvt`
// (retype) are address-scoped exactly like `funcs`'s writes (same
// `<cmd> <args> @ <addr>` shape), so they use the identical
// `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
// replacement for it. On the r2pipe backend, `cmd_noeval` reports unsupported
// and the exact `afvn`/`afvt` command text this table always issued runs
// unchanged.
#include <cctype>
#include <string>
@@ -64,6 +24,7 @@
#include <r2xsql/backend.hpp>
#include <xsql/vtable.hpp>
#include <r_util.h>
#include "tables.hpp"
#include "direct.hpp"
#include "json_utils.hpp"
@@ -141,17 +102,6 @@ std::string compact_type(std::string_view type) {
return out;
}
// Dispatch a write command scoped to `addr`. Tries the structural no-eval
// path first (libr backend only, via Backend::cmd_noeval); falls back to the
// original command-string form (`command @ addr`) on every backend that
// reports it unsupported, so r2pipe's behavior and command text stay
// byte-for-byte unchanged. Same helper shape as the `comments`/`funcs`/
// `flags`/`bookmarks`/`types`/`patches` tables' rollout of this dispatch.
std::string dispatch_write(Backend* be, int64_t addr, const std::string& command) {
std::string out;
if (be->cmd_noeval(addr, command, &out)) return out;
return be->cmd(command + " @ " + json_utils::hex_addr(addr));
}
bool local_readback_matches(Backend* be, const LocalRow& before,
const std::string& expected_name,
@@ -191,12 +141,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;
@@ -211,7 +161,7 @@ void register_locals_table(Session& s) {
return false;
}
} else {
std::string out = dispatch_write(be, r.func_addr,
std::string out = be->cmd_noeval(r.func_addr,
std::string("afvn ") + v + " " + r.name);
if (std::string e = table_utils::command_error_text(out); !e.empty()) {
xsql::set_vtab_error("locals: rename failed (func_addr=" +
@@ -245,7 +195,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;
@@ -260,7 +210,7 @@ void register_locals_table(Session& s) {
return false;
}
} else {
std::string out = dispatch_write(be, r.func_addr,
std::string out = be->cmd_noeval(r.func_addr,
std::string("afvt ") + r.name + " " + v);
if (std::string e = table_utils::command_error_text(out); !e.empty()) {
xsql::set_vtab_error("locals: retype failed (func_addr=" +
+3 -39
View File
@@ -72,30 +72,6 @@
// rolled out to `funcs`/`flags`/`bookmarks`/`types`. Its three writes split
// two different ways, not one:
//
// - INSERT's `wx <hex> @ <addr>` IS address-scoped, the same shape every
// prior table's address-scoped write already used -- it dispatches
// through the same `dispatch_write(be, addr, command)` helper. Its own
// free-text argument (`patched_bytes`) is already constrained, before
// this dispatch ever runs, to `json_utils::valid_hex_bytes`'s charset
// ([0-9a-fA-F], even length) -- a set that structurally cannot contain
// `;`/`@`/`|`/backtick, so the hex validator alone already makes chaining
// through THIS argument impossible. cmd_noeval adds no NEW closed hole
// for this one input; it is wired in anyway for defense-in-depth and
// consistency with the other five tables' address-scoped writes, not
// because a real gap was found here.
// - UPDATE's `wci` and DELETE's `wcu` are NEITHER address-scoped NOR
// user-parameterized at all: both are issued as fixed, literal strings
// with zero interpolation (see the two `be->cmd(...)` call sites below --
// neither ever concatenates anything into the command text). Unlike
// `td`/`t-` (also addr-less, but whose user-supplied declaration/name IS
// a real injection surface, which is why `types` needed
// `table_utils::dispatch_type_write`'s fixed anchor), there is no
// attacker-controlled text anywhere in `wci`'s or `wcu`'s construction to
// protect -- inventing a fixed-anchor cmd_noeval call for a hardcoded
// literal would add a code path with no security benefit, so both stay
// on the plain `Backend::cmd()` path, unchanged. This is a deliberate,
// audited "nothing to harden" finding, not an oversight: with it, all six
// write-capable tables in this project have been examined for this idiom.
#include <cstdint>
#include <string>
@@ -115,19 +91,6 @@ namespace r2xsql {
namespace {
// Dispatch a write command scoped to `addr`. Tries the structural no-eval
// path first (libr backend only, via Backend::cmd_noeval); falls back to the
// original command-string form (`command @ addr`) on every backend that
// reports it unsupported, so r2pipe's behavior and command text stay
// byte-for-byte unchanged. Same helper shape as the `comments`/`funcs`/
// `flags`/`bookmarks` tables' rollout of this dispatch. Only INSERT's `wx`
// uses this -- `wci`/`wcu` are fixed literals with no address and nothing to
// dispatch differently (see the file header).
std::string dispatch_write(Backend* be, int64_t addr, const std::string& command) {
std::string out;
if (be->cmd_noeval(addr, command, &out)) return out;
return be->cmd(command + " @ " + json_utils::hex_addr(addr));
}
using direct::PatchRow;
@@ -294,7 +257,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) {
@@ -358,7 +322,7 @@ void register_patches_table(Session& s) {
// even though the hex validator above already makes this argument
// injection-proof on its own.
if (std::string e = table_utils::command_error_text(
dispatch_write(be, addr, "wx " + hex));
be->cmd_noeval(addr, "wx " + hex));
!e.empty()) {
xsql::set_vtab_error(
"patches: INSERT failed (addr=" + json_utils::hex_addr(addr) + "): " + e);
+10 -2
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,11 +43,18 @@ 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;
}
if (std::string e = table_utils::command_error_text(be->cmd("P- " + r.name)); !e.empty()) {
// With scr.interactive on, P- asks "Confirm project deletion? (Y/n)" on
// the console and the statement blocks on it (over =h, so does the
// request). The DELETE is the confirmation, so answer it here.
const std::string prior = table_utils::trim_ascii(be->cmd("e scr.interactive"));
be->cmd("e scr.interactive=false");
const std::string out = be->cmd("'P- " + r.name);
be->cmd("e scr.interactive=" + (prior.empty() ? std::string("false") : prior));
if (std::string e = table_utils::command_error_text(out); !e.empty()) {
xsql::set_vtab_error("projects: DELETE failed (name=" + r.name + "): " + e);
return false;
}
+17 -9
View File
@@ -104,15 +104,9 @@ struct ReadCstrRow {
enum Col { kContent = 0, kLength = 1, kSection = 2, kType = 3,
kTruncated = 4, kError = 5, kAddr = 6, kMaxLen = 7 };
// Seeked no-eval dispatch for `pszj <max_len>`, mirroring
// table_utils::assemble_hex's exact shape (try cmd_noeval first, fall back
// to the plain `@ <addr>`-suffixed command on every backend that reports it
// unsupported) -- kept local since this table is the only user.
std::string dispatch_read_cstr(Backend* be, int64_t addr, int64_t max_len) {
const std::string command = "pszj " + std::to_string(max_len);
std::string out;
if (be->cmd_noeval(addr, command, &out)) return out;
return be->cmd(command + " @ " + json_utils::hex_addr(addr));
return be->cmd_noeval(addr, command);
}
ReadCstrRow run_read_cstr(Backend* be, int64_t addr, int64_t max_len) {
@@ -164,11 +158,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;
}
}
-1
View File
@@ -3,7 +3,6 @@
//
// This file is licensed under the Human-Origin Source License v1.0.
// See LICENSE.
// Native reads for libr; command fallback for r2pipe and mock.
#include <r2xsql/session.hpp>
#include <r2xsql/backend.hpp>
-1
View File
@@ -3,7 +3,6 @@
//
// This file is licensed under the Human-Origin Source License v1.0.
// See LICENSE.
// Relocations from RBin, with a command fallback for r2pipe and mock.
#include <r2xsql/session.hpp>
#include <r2xsql/backend.hpp>
+40 -33
View File
@@ -26,19 +26,8 @@
// required: an unfiltered `/gj` scan over the whole configured search scope
// is a real, bounded answer, not an error.
//
// `search.in` is r2 session config that leaks past the command that sets it
// (verified directly: set it, run an unrelated command, read it back — the
// new value is still there). There is no automatic restore, exactly the
// property that forced the flags table's atomic flagspace-select pattern.
// Every search here runs as ONE `Backend::cmd()`/`cmd_json()` call that sets
// the scope, searches, and puts the scope back — never as separate calls
// with anything else in between, since the in-process plugin shares one
// core across its interactive/remote callers and a leaked scope would
// corrupt a concurrent caller's search. The scope is always restored to
// radare2's own documented default (`io.maps`) rather than a value read back
// in a separate round trip first — the same "restore to the neutral
// default, not a racily-read prior value" choice the flagspace fix already
// made for the same class of hazard.
// Searches use native config save/restore and direct command dispatch, so
// pattern text cannot expand commands or interfere with restoring search.in.
//
// The default search scope is `io.maps.x` (executable maps only), not
// radare2's own CLI default of `io.maps` (all IO maps, code and data).
@@ -113,6 +102,7 @@
#include <xsql/vtable.hpp>
#include "tables.hpp"
#include "direct.hpp"
#include "json_utils.hpp"
namespace r2xsql {
@@ -230,10 +220,9 @@ std::vector<GadgetRec> run_gadget_search(Backend* be, const SearchParams& p) {
search_cmd += p.pattern;
}
// Atomic: set scope, search, restore scope -- one command, see file header.
const std::string atomic =
"e search.in=io.maps.x; " + search_cmd + "; e search.in=io.maps";
auto hits = be->cmd_json(atomic);
auto* api = be->direct();
const std::string text = api ? api->search(search_cmd) : be->cmd("'" + search_cmd);
auto hits = nlohmann::json::parse(text, nullptr, false);
if (!hits.is_array()) return out;
for (auto& g : hits) {
@@ -401,26 +390,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 +453,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;
}
-10
View File
@@ -10,16 +10,6 @@
// r2 DOES give a default target (`switch_op.def_val`). A `func_addr` equality filter
// is pushed down to a single `afbj @ X`; an unfiltered scan walks `aflj`.
//
// A second, faster producer reads libr's analysis state through the C API
// instead of running the command and parsing its JSON (see `direct.hpp`).
// `table_addr` stays NULL on this leg too, by design: the C API DOES have the
// jump table's own address (`RAnalSwitchOp::daddr`), but no r2 command
// surfaces it, so populating it only here would make the two backends
// disagree on this column for the same query — see `direct::BlockData`'s
// comment. `blocks`/`cfg_edges`/`switch_tables`/`dominators`/
// `post_dominators`/`loops` all share the identical `aflj` + N×`afbj` full
// scan today; each is ported independently, reading the same
// `direct::BlockData` shape `direct.hpp` defines once for all six.
#include <memory>
#include <utility>
-1
View File
@@ -3,7 +3,6 @@
//
// This file is licensed under the Human-Origin Source License v1.0.
// See LICENSE.
// Native reads for libr; command fallback for r2pipe and mock.
#include <r2xsql/session.hpp>
#include <r2xsql/backend.hpp>
+10 -141
View File
@@ -3,66 +3,17 @@
//
// This file is licensed under the Human-Origin Source License v1.0.
// See LICENSE.
// types — radare2 type system catalog.
//
// Backed by:
// tk* list of `NAME=KIND` pairs (atomic|struct|union|enum|typedef|func)
// tj array of atomic-type records with size + pf format
// tsj N struct definition (name, format, fields[])
// tuj N union definition (same shape as tsj)
// tej N enum definition (name, values{key:int, ...})
//
// One row per type. Columns:
// ordinal INT64 — 0-based position of this row in the type set, in the
// SAME name-sorted order the row walk below already
// produces (a free running counter over the existing
// scan -- no extra command, no extra cost). NOT a
// persistent per-type identity: this table has
// .no_shared_cache() (the type database is mutable), so
// the row set -- and therefore every ordinal from the
// matched row onward -- is recomputed from scratch on
// every query. Two queries in the same session agree on
// a given name's ordinal ONLY while the type set is
// unchanged; an intervening INSERT/DELETE (or
// `td`) shifts the alphabetical position
// of every name that sorts after the change. Never
// derived from any address. Both backends share this
// exact producer (no direct::/C-API path exists for
// `types` -- see the "Resolved -- won't build" record),
// so pipe and full assign identical ordinals for
// identical type sets structurally, not by coincidence.
// name TEXT — fully qualified type name
// kind TEXT — atomic | struct | union | enum | typedef | func | type
// size INT64 — byte size (-1 for unsized/opaque/pointer-like)
// format TEXT — pf-style format string (atomics/structs/unions); empty otherwise
//
// Both writes here (`td` for INSERT, `t-` for DELETE) dispatch through
// Backend::cmd_noeval on the in-process (libr) backend -- the fifth
// (`comments`/`funcs`/`flags`/`bookmarks` came first) and structurally
// DIFFERENT rollout of that no-eval idiom: every prior table's write had a
// real row address to seek to; `td`/`t-` have none at all -- a struct/union/
// enum definition is not tied to any address, and `t-` deletes purely by
// name. Verified against radare2's own source (cmd_type.inc.c), not assumed:
// both handlers read only `core->anal`, never `core->addr`/the current seek,
// so `Backend::cmd_noeval`'s mandatory seek target is genuinely unobservable
// to either one -- see `table_utils::kTypeSystemAnchorAddr` /
// `dispatch_type_write` (table_utils.hpp) for the fixed-anchor helper this
// uses instead of a per-row address, and the full citation. `t-`'s name
// argument is charset-validated; INSERT builds an empty declaration from
// the validated name and kind before calling dispatch_type_write.
// types — native type catalog with empty struct/union/enum creation.
#include <r2xsql/session.hpp>
#include <r2xsql/backend.hpp>
#include <xsql/vtable.hpp>
#include <r_util.h>
#include "tables.hpp"
#include "direct.hpp"
#include "json_utils.hpp"
#include "table_utils.hpp"
#include <algorithm>
#include <string>
#include <unordered_map>
#include <vector>
namespace r2xsql {
@@ -80,64 +31,6 @@ void register_types_table(Session& s) {
.cache_builder([be = s.backend_ptr()](std::vector<TypeRow>& rows) {
if (auto* api = be->direct()) {
api->types_scan(rows);
return;
}
auto kinds = json_utils::parse_tk_dump(be->cmd("tk*"));
if (kinds.empty()) return;
// Enrich atomics with size + pf format from `tj`.
std::unordered_map<std::string, std::pair<int64_t, std::string>> atomic_info;
auto tj = be->cmd_json("tj");
if (tj.is_object()) {
const auto& arr = tj.contains("types") ? tj["types"] : nlohmann::json::array();
for (const auto& j : arr) {
atomic_info[json_utils::str(j, "type")] =
{json_utils::i64(j, "size", -1), json_utils::str(j, "format")};
}
}
rows.reserve(kinds.size());
for (auto& [name, kind] : kinds) {
TypeRow r;
r.name = name;
r.kind = kind;
r.size = -1;
if (kind == "atomic" || kind == "type") {
auto it = atomic_info.find(name);
if (it != atomic_info.end()) {
r.size = it->second.first;
r.format = it->second.second;
}
} else if (kind == "struct" || kind == "union") {
auto cmd = (kind == "struct" ? std::string("tsj ") : std::string("tuj ")) + name;
auto j = be->cmd_json(cmd);
if (j.is_object()) {
r.format = json_utils::str(j, "format");
int64_t total = 0;
int64_t max_field = 0;
if (j.contains("fields") && j["fields"].is_array()) {
for (const auto& f : j["fields"]) {
int64_t sz = json_utils::i64(f, "size");
int64_t ar = json_utils::i64(f, "array_size", 1);
if (ar <= 0) ar = 1;
int64_t fsize = sz * ar;
if (kind == "struct") total += fsize;
else if (fsize > max_field) max_field = fsize;
}
}
r.size = (kind == "struct") ? total : max_field;
}
}
rows.push_back(std::move(r));
}
std::sort(rows.begin(), rows.end(),
[](const TypeRow& a, const TypeRow& b) { return a.name < b.name; });
// Free running counter over the row walk above, taken AFTER the sort so
// ordinal reflects the same name-sorted order every column reads --
// matches the default cache-position rowid this table already reports
// (no .rowid() override exists for `types`), so ordinal and rowid agree.
for (size_t i = 0; i < rows.size(); ++i) {
rows[i].ordinal = static_cast<int64_t>(i);
}
})
.column_int64("ordinal", [](const TypeRow& r) { return r.ordinal; })
@@ -148,20 +41,13 @@ 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;
}
// `t-` has no address in its own grammar at all (see the file header);
// dispatch_type_write seeks Backend::cmd_noeval to the fixed
// kTypeSystemAnchorAddr instead of a per-row address, falling back to
// this exact same "t- <name>" string, byte-for-byte, on every backend
// that reports the primitive unsupported (r2pipe, mock).
const std::string cmd = "t- " + r.name;
if (std::string e = table_utils::command_error_text(
table_utils::dispatch_type_write(be, cmd));
!e.empty()) {
xsql::set_vtab_error("types: DELETE failed (name=" + r.name + "): " + e);
auto* api = be->direct();
if (!api || !api->type_delete(r.name)) {
xsql::set_vtab_error("types: DELETE failed (name=" + r.name + ")");
return false;
}
return true;
@@ -177,7 +63,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;
}
@@ -189,26 +75,9 @@ void register_types_table(Session& s) {
"radare2's `td <C decl>` command instead");
return false;
}
const std::string decl = kind + " " + name + " {};";
// `td` is not address-scoped either (see the file header); the no-eval
// leg takes the declaration completely unquoted (r_cmd_call never
// re-scans it for ';'/'{'/'}' the way r_core_cmd0 does, so there is
// nothing for a quote wrapper to protect there -- and a quote wrapper
// would be taken as literal argument text, breaking the declaration).
// The fallback leg keeps the pre-existing r2_quote wrapping verbatim,
// since only r_core_cmd0's evaluating path needs it.
const std::string out = table_utils::dispatch_type_write(
be, "td " + decl, json_utils::r2_quote("td " + decl));
if (std::string e = table_utils::command_error_text(out); !e.empty()) {
xsql::set_vtab_error("types: INSERT failed (name=" + name + "): " + e);
return false;
}
// A td parser failure may log only to stderr and otherwise look like
// a successful no-output command -- read the name back rather than
// trusting the silence.
if (table_utils::trim_ascii(be->cmd("tk " + name)).empty()) {
xsql::set_vtab_error("types: INSERT failed (name=" + name +
"): type was not present after td");
auto* api = be->direct();
if (!api || !api->type_create(name, kind)) {
xsql::set_vtab_error("types: INSERT failed (name=" + name + ")");
return false;
}
return true;
-45
View File
@@ -21,11 +21,8 @@
#include "tables.hpp"
#include "direct.hpp"
#include "json_utils.hpp"
#include <algorithm>
#include <string>
#include <unordered_map>
#include <vector>
namespace r2xsql {
@@ -43,49 +40,7 @@ void register_types_members_table(Session& s) {
.cache_builder([be = s.backend_ptr()](std::vector<MemberRow>& rows) {
if (auto* api = be->direct()) {
api->type_members_scan(rows);
return;
}
auto kinds = json_utils::parse_tk_dump(be->cmd("tk*"));
if (kinds.empty()) return;
for (auto& [name, kind] : kinds) {
if (kind != "struct" && kind != "union" && kind != "enum") continue;
if (kind == "enum") {
auto j = be->cmd_json("tej " + name);
if (!j.is_object() || !j.contains("values") || !j["values"].is_object()) continue;
for (auto it = j["values"].begin(); it != j["values"].end(); ++it) {
MemberRow r;
r.type_name = name;
r.parent_kind = "enum";
r.member_name = it.key();
r.array_size = 0;
r.value = it.value().is_number_integer()
? it.value().get<int64_t>()
: 0;
rows.push_back(std::move(r));
}
} else {
auto cmd = (kind == "struct" ? std::string("tsj ") : std::string("tuj ")) + name;
auto j = be->cmd_json(cmd);
if (!j.is_object() || !j.contains("fields") || !j["fields"].is_array()) continue;
for (const auto& f : j["fields"]) {
MemberRow r;
r.type_name = name;
r.parent_kind = kind;
r.member_name = json_utils::str(f, "name");
r.member_type = json_utils::str(f, "type");
r.offset = json_utils::i64(f, "offset");
r.size = json_utils::i64(f, "size");
r.array_size = json_utils::i64(f, "array_size", 1);
if (r.array_size <= 0) r.array_size = 1;
rows.push_back(std::move(r));
}
}
}
std::sort(rows.begin(), rows.end(), [](const MemberRow& a, const MemberRow& b) {
if (a.type_name != b.type_name) return a.type_name < b.type_name;
return a.offset < b.offset;
});
})
.column_text ("type_name", [](const MemberRow& r) { return r.type_name; })
.column_text ("parent_kind", [](const MemberRow& r) { return r.parent_kind; })
-8
View File
@@ -6,14 +6,6 @@
// xrefs — per-function enumeration via `afxj @ <fn>` for full scans, with
// scoped `axtj @ <addr>` / `axfj @ <addr>` pushdown for point lookups.
//
// All three shapes have a second, faster producer that reads libr's analysis
// state through the C API instead of running the command and parsing its JSON
// (see `direct.hpp` for why that lives in another TU, and what it costs not to).
// The command path below is NOT a legacy leftover: it is the only
// implementation r2pipe can have, since that backend drives a separate radare2
// process. Each shape picks the direct producer when the backend offers one and
// runs the command otherwise; both must return the same rows, which the e2e
// suite checks row by row across the two backends.
#include <r2xsql/session.hpp>
#include <r2xsql/backend.hpp>
-10
View File
@@ -24,16 +24,6 @@
// the most interesting ones. So an edge here is one landing on a function
// *or* an import, expressed as joins that also supply the callee's name.
//
// 2. `from_func IS NOT NULL` is not cosmetic here, it is what makes these
// views give one answer. radare2 has two ways to enumerate references and
// they do not agree: the per-function walk behind a full scan cannot see a
// reference whose source lies outside every analyzed function, while the
// scoped `axtj` lookup behind the `to_addr` pushdown can. On that binary that
// is a real 38-vs-34 split on the same logical question, on BOTH backends
// (so it is radare2's semantics, not ours). Every such row has a NULL
// `from_func` — it belongs to no function by definition — so filtering
// them out makes these views return the same rows whichever plan SQLite
// picks. Removing the filter would reintroduce a plan-dependent answer.
#include <r2xsql/session.hpp>
#include <xsql/database.hpp>
+18
View File
@@ -19,6 +19,9 @@ set_target_properties(core_r2xsql PROPERTIES
OUTPUT_NAME "core_r2xsql"
PREFIX ""
)
if(APPLE)
set_target_properties(core_r2xsql PROPERTIES SUFFIX ".dylib")
endif()
target_include_directories(core_r2xsql PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/../lib/include
@@ -48,3 +51,18 @@ xsql_add_versioninfo(core_r2xsql
FILETYPE VFT_DLL
DESCRIPTION "r2xsql in-r2 plugin (sql command)"
ORIGINAL_FILENAME "core_r2xsql.dll")
if(R2XSQL_STANDALONE)
set(R2XSQL_USER_PLUGINS "" CACHE PATH "Override the per-user radare2 plugin directory")
foreach(_action user-install user-uninstall)
add_custom_target(${_action}
COMMAND ${CMAKE_COMMAND}
"-DRADARE2=${Radare2_EXECUTABLE}"
"-DPLUGIN=$<TARGET_FILE:core_r2xsql>"
"-DPLUGIN_DIR=${R2XSQL_USER_PLUGINS}"
"-DACTION=${_action}"
-P "${PROJECT_SOURCE_DIR}/cmake/user-install.cmake"
VERBATIM)
endforeach()
add_dependencies(user-install core_r2xsql)
endif()
+37 -33
View File
@@ -17,8 +17,8 @@
// sql.views list available views
//
// The plugin wraps the host RCore* with libr2xsql's LibrBackend (no
// ownership) and shares a single Session across calls so SQLite caches
// stay warm across queries.
// ownership) and keeps one Session per core across calls so SQLite caches
// stay warm across queries. radare2 owns its lifetime through ctx->data.
#include <r2xsql/r2xsql.hpp>
#include <xsql/query_script.hpp>
@@ -30,6 +30,7 @@
#include <cstring>
#include <memory>
#include <mutex>
#include <new>
#include <string>
// r2xsql::make_libr_backend_from_core (declared in <r2xsql/backend.hpp>, included
@@ -39,18 +40,16 @@ namespace {
struct PluginState {
std::unique_ptr<r2xsql::Session> session;
RCore* bound_core = nullptr;
std::mutex mu;
};
PluginState g_state;
void ensure_session(RCore* core) {
std::lock_guard<std::mutex> lock(g_state.mu);
if (g_state.session && g_state.bound_core == core) return;
// Called with state.mu held. Do not keep a namespace-scope owning Session:
// quick exit skips plugin_fini, and static destruction can close SQLite after
// libxsql's cache registry has already been destroyed.
void ensure_session(PluginState& state, RCore* core) {
if (state.session) return;
auto backend = r2xsql::make_libr_backend_from_core(core);
g_state.session = std::make_unique<r2xsql::Session>(std::move(backend));
g_state.bound_core = core;
state.session = std::make_unique<r2xsql::Session>(std::move(backend));
}
static RCoreHelpMessage help_msg_sql = {
@@ -106,7 +105,7 @@ std::string render_table(RCore* core, const xsql::Result& res,
return out ? out.get() : "";
}
bool dispatch(RCore* core, const char* input) {
bool dispatch(PluginState& state, RCore* core, const char* input) {
if (!input) return false;
std::string table_query;
@@ -151,15 +150,12 @@ bool dispatch(RCore* core, const char* input) {
return true;
}
ensure_session(core);
std::string out;
{
// Serialize against concurrent callers (e.g. from other plugins or tools)
// which also touch g_state.session / SQLite under this same mutex.
std::lock_guard<std::mutex> lock(g_state.mu);
std::lock_guard<std::mutex> lock(state.mu);
try {
auto res = g_state.session->query(input);
ensure_session(state, core);
auto res = state.session->query(input);
out = render_table(core, res, table_query.c_str());
} catch (const std::exception& e) {
out = std::string("EXCEPTION: ") + e.what() + "\n";
@@ -171,7 +167,7 @@ bool dispatch(RCore* core, const char* input) {
return true;
}
bool dispatch_json(RCore* core, const char* input) {
bool dispatch_json(PluginState& state, RCore* core, const char* input) {
input = r_str_trim_head_ro(input);
if (!*input || std::strcmp(input, "?") == 0) {
print_help(core);
@@ -179,10 +175,10 @@ bool dispatch_json(RCore* core, const char* input) {
}
std::string out;
try {
ensure_session(core);
std::lock_guard<std::mutex> lock(g_state.mu);
auto exec = [](const std::string& sql, xsql::ScriptStatementResult& result) {
auto res = g_state.session->query(sql);
std::lock_guard<std::mutex> lock(state.mu);
ensure_session(state, core);
auto exec = [&state](const std::string& sql, xsql::ScriptStatementResult& result) {
auto res = state.session->query(sql);
result.success = res.ok();
result.error = res.error;
result.warnings = std::move(res.warnings);
@@ -205,11 +201,12 @@ bool dispatch_json(RCore* core, const char* input) {
}
bool plugin_call(RCorePluginSession* ctx, const char* input) {
if (!ctx || !input) return false;
if (!ctx || !ctx->data || !input) return false;
auto& state = *static_cast<PluginState*>(ctx->data);
if (std::strncmp(input, "sqlj", 4) == 0) {
const char* sql = input + 4;
if (*sql && *sql != ' ' && *sql != '\t' && *sql != '?') return false;
return dispatch_json(ctx->core, sql);
return dispatch_json(state, ctx->core, sql);
}
// r2 calls every registered plugin's call() for every command; only
// claim the ones that start with our prefix.
@@ -219,17 +216,24 @@ bool plugin_call(RCorePluginSession* ctx, const char* input) {
if (*rest != '\0' && *rest != ' ' && *rest != '?' && *rest != '.' && *rest != ',') {
return false;
}
return dispatch(ctx->core, rest);
return dispatch(state, ctx->core, rest);
}
bool plugin_init(RCorePluginSession* /*ctx*/) {
return true;
bool plugin_init(RCorePluginSession* ctx) {
ctx->data = new (std::nothrow) PluginState;
return ctx->data != nullptr;
}
bool plugin_fini(RCorePluginSession* /*ctx*/) {
std::lock_guard<std::mutex> lock(g_state.mu);
g_state.session.reset();
g_state.bound_core = nullptr;
bool plugin_fini(RCorePluginSession* ctx) {
auto* state = static_cast<PluginState*>(ctx->data);
if (state) {
{
std::lock_guard<std::mutex> lock(state->mu);
state->session.reset();
}
delete state;
ctx->data = nullptr;
}
return true;
}
@@ -242,10 +246,10 @@ static RCorePlugin r_core_plugin_r2xsql = {
/*.name =*/ const_cast<char*>("r2xsql"),
/*.desc =*/ const_cast<char*>("SQL interface for radare2 (libr2xsql)"),
/*.author =*/ const_cast<char*>("0xeb"),
/*.version =*/ const_cast<char*>("0.0.1"),
/*.version =*/ const_cast<char*>(R2XSQL_VERSION_STRING),
/*.license =*/ const_cast<char*>("LicenseRef-Human-Origin-Source-1.0"),
/*.contact =*/ const_cast<char*>("[email protected]"),
/*.copyright =*/ const_cast<char*>("Copyright (c) 2024-2026 Elias Bachaalany"),
/*.copyright =*/ const_cast<char*>(R2XSQL_COPYRIGHT_STRING),
/*.status =*/ R_PLUGIN_STATUS_BASIC,
},
/*.init =*/ plugin_init,
+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')
+64
View File
@@ -0,0 +1,64 @@
NAME=native IO maps sanitize names without executing metacharacters
FILE=malloc://64
ARGS=-e log.level=0 -i build/load.r2
CMDS=<<CMDS
'sql,:quiet INSERT INTO io_maps(start_addr,end_addr,paddr,perm,name) VALUES(4096,4112,4,'r-x','map ; f map_injected $(f map_subst) ''quote'' # tail' || char(10) || 'f map_newline')
'sql,:quiet SELECT count(*) FROM io_maps WHERE start_addr=4096 AND end_addr=4112 AND paddr=4 AND perm='r-x' AND name LIKE 'map%map_injected%map_subst%quote%tail%map_newline'
'sql,:quiet SELECT count(*) FROM flags WHERE name IN ('map_injected','map_subst','map_newline')
'sql,:quiet DELETE FROM io_maps WHERE start_addr=4096
'sql,:quiet SELECT count(*) FROM io_maps WHERE start_addr=4096
'sql,:quiet SELECT count(*) FROM io_maps WHERE start_addr=0
CMDS
EXPECT=<<EXPECTED
1
0
0
1
EXPECTED
EXPECT_ERR=
RUN
NAME=native type writes preserve seek and create each supported kind
FILE=malloc://64
ARGS=-e log.level=0 -i build/load.r2
CMDS=<<CMDS
s 16
'sql,:quiet INSERT INTO types(name,kind) VALUES('SqlStruct','struct')
'sql,:quiet INSERT INTO types(name,kind) VALUES('SqlUnion','union')
'sql,:quiet INSERT INTO types(name,kind) VALUES('SqlEnum','enum')
'sql,:quiet SELECT count(*) FROM types WHERE (name='SqlStruct' AND kind='struct') OR (name='SqlUnion' AND kind='union') OR (name='SqlEnum' AND kind='enum')
'sql,:quiet DELETE FROM types WHERE name='SqlStruct'
'sql,:quiet DELETE FROM types WHERE name='SqlUnion'
'sql,:quiet DELETE FROM types WHERE name='SqlEnum'
'sql,:quiet SELECT count(*) FROM types WHERE name IN ('SqlStruct','SqlUnion','SqlEnum')
s
CMDS
EXPECT=<<EXPECTED
3
0
0x10
EXPECTED
EXPECT_ERR=
RUN
NAME=SQL comment and assembly arguments do not expand commands
FILE=malloc://64
ARGS=-e log.level=0 -i build/load.r2 -a x86 -b 64
CMDS=<<CMDS
'sql,:quiet INSERT INTO comments(addr,text) VALUES(4,'comment;f comment_injected $(f comment_subst)')
'sql,:quiet SELECT count(*) FROM comments WHERE addr=4 AND text='comment;f comment_injected $(f comment_subst)'
'sql,:quiet SELECT size FROM assemble WHERE asm='nop' AND addr=16
'sql,:quiet SELECT size FROM assemble WHERE asm='nop $(f assemble_injected)'
'sql,:quiet SELECT count(*) FROM flags WHERE name IN ('comment_injected','comment_subst','assemble_injected')
'sql,:quiet DELETE FROM comments WHERE addr=4
'sql,:quiet SELECT count(*) FROM comments WHERE addr=4
CMDS
EXPECT=<<EXPECTED
1
1
0
0
0
EXPECTED
EXPECT_ERR=
RUN
+1 -1
View File
@@ -3,7 +3,7 @@ FILE=build/fixture.bin
ARGS=-e log.level=0 -i build/load.r2
CMDS=<<CMDS
aaa
"sql,:quiet SELECT count(*)=1 FROM data_items WHERE name='r2xsql_marker' AND size>0 AND is_string=0 AND is_initialized=1"
"sql,:quiet SELECT count(*)=1 FROM data_items WHERE name IN ('r2xsql_marker','_r2xsql_marker') AND is_string=0 AND is_initialized=1"
"sql,:quiet SELECT count(*)>0 FROM data_items WHERE is_string=1 AND value_repr='r2xsql_test_marker_string' AND size>length(value_repr)"
"sql,:quiet SELECT (SELECT count(*) FROM grep WHERE kind='func')=(SELECT count(*) FROM funcs)"
"sql,:quiet SELECT (SELECT count(*) FROM grep WHERE kind='import')=(SELECT count(*) FROM imports)"
+61
View File
@@ -0,0 +1,61 @@
NAME=assembly search does not execute command substitutions
FILE=malloc://16
ARGS=-e log.level=0 -i build/load.r2 -a x86 -b 64
CMDS=<<CMDS
wx 90c3
e search.in=block
'sql,:quiet SELECT count(*) FROM search_asm WHERE asm='nop $(f sql_injected)'
'sql,:quiet SELECT count(*) FROM flags WHERE name='sql_injected'
e search.in
'sql,:quiet SELECT count(*) FROM search_asm WHERE asm='nop'
e search.in
CMDS
EXPECT=<<EXPECTED
0
0
block
1
block
EXPECTED
EXPECT_ERR=
RUN
NAME=assembly search preserves hash operands and search scope
FILE=malloc://16
ARGS=-e log.level=0 -i build/load.r2 -a arm -b 64
CMDS=<<CMDS
wx 00040091c0035fd6
e search.in=block
'sql,:quiet SELECT count(*) FROM search_asm WHERE asm='add x0, x0, #1'
e search.in
CMDS
EXPECT=<<EXPECTED
1
block
EXPECTED
EXPECT_ERR=
RUN
NAME=gadget pattern cannot comment out search scope restoration
FILE=malloc://1024
ARGS=-e log.level=0 -i build/load.r2 -a x86 -b 64
CMDS=<<CMDS
wx 9058c3 @ 0x40
e search.in=block
'sql,:quiet SELECT count(*) FROM rop_gadgets WHERE pattern='#'
e search.in
'sql,:quiet SELECT count(*) FROM rop_gadget_instructions WHERE pattern='#'
e search.in
'sql,:quiet SELECT count(*)>0 FROM rop_gadgets WHERE terminator='ret'
e search.in
CMDS
EXPECT=<<EXPECTED
0
block
0
block
1
block
EXPECTED
EXPECT_ERR=
RUN
+1 -1
View File
@@ -31,7 +31,7 @@ def test_r2_http(r2_bin, r2xsql_plugin, fixture_binary, tmp_path, post):
with log_path.open("w") as log:
proc = subprocess.Popen([
r2_bin, "-NN", "-q", "-e", "http.bind=127.0.0.1", "-e", f"http.port={port}",
"-e", "http.log=false", "-e", "scr.color=0",
"-e", "http.log=false", "-e", "http.sandbox=false", "-e", "scr.color=0",
"-c", f"L {r2xsql_plugin}", "-c", "aa", "-c", "=h", str(fixture_binary),
], stdout=log, stderr=log, cwd=tmp_path)
try:
+45
View File
@@ -0,0 +1,45 @@
# SPDX-License-Identifier: LicenseRef-Human-Origin-Source-1.0
"""Check an automatically discovered release plugin on every supported OS."""
import json
from pathlib import Path
import subprocess
import sys
def check(r2):
# -Q skips plugin finalizers; process teardown must still be safe.
for quiet in ("-q", "-Q"):
commands = [
'"sqlj SELECT NULL AS missing, 7 > 3 AS ok; SELECT 2 AS n"',
'"sqlj INSERT INTO comments(addr,text) VALUES(4, \'release smoke\'); '
"SELECT text FROM comments WHERE addr=4; "
"DELETE FROM comments WHERE addr=4; "
'SELECT count(*) FROM comments WHERE addr=4"',
'"sqlj SELECT * FROM missing_table"',
'"sqlj SELECT 42 AS alive"',
'"sql,:csv SELECT 1 AS n, \'a,b\' AS name"',
]
args = [r2, "-N", quiet, "-e", "scr.color=0", "-e", "scr.utf8=false"]
for command in commands:
args.extend(["-c", command])
result = subprocess.run(args + ["malloc://64"], capture_output=True,
text=True, timeout=30)
assert result.returncode == 0, (quiet, result.returncode, result.stderr)
lines = result.stdout.splitlines()
assert len(lines) == 6, (result.stdout, result.stderr)
multi, write, error, alive = map(json.loads, lines[:4])
assert multi["success"] and multi["statement_count"] == 2, multi
assert multi["results"][0]["rows"] == [[None, "1"]], multi
assert multi["results"][1]["rows"] == [["2"]], multi
assert write["success"] and write["statement_count"] == 4, write
assert write["results"][1]["rows"] == [["release smoke"]], write
assert write["results"][3]["rows"] == [["0"]], write
assert not error["success"], error
assert alive["success"] and alive["results"][0]["rows"] == [["42"]], alive
assert lines[4:] == ["n,name", '1,"a,b"'], result.stdout
print("Packaged plugin: JSON, CRUD, errors, CSV and process cleanup OK")
if __name__ == "__main__":
check(str(Path(sys.argv[1]).resolve()))