Author SHA1 Message Date
pancake 3bc4d0a611 Fix plugin installs and name release assets for r2pm 2026-10-03 08:53:05 +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
34 changed files with 550 additions and 324 deletions

No files matched your search

+26 -76
View File
@@ -1,87 +1,40 @@
#!/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
# 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
}
# The pinned radare2 predates r2pm -H R2PM_TRIPLET.
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
rm -rf dist
mkdir -p "dist/plugin/${REL_PLUGIN}"
mkdir -p dist/plugin/plugins
cp "build/bin/core_r2xsql.${ext}" dist/plugin/plugins/
cp LICENSE dist/plugin/
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
# Check automatic discovery of the packaged plugin, including its suffix.
result=$(R2_LIBR_PLUGINS="$PWD/dist/plugin/plugins" \
LD_LIBRARY_PATH="$PWD/r2prefix/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" \
DYLD_LIBRARY_PATH="$PWD/r2prefix/lib${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}" \
./r2prefix/bin/radare2 -Nq -c 'sqlj SELECT 1 AS ok' -)
[[ "$result" == *'"rows":[["1"]]'* ]] || { echo "$result" >&2; exit 1; }
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
@@ -100,10 +53,7 @@ The plugin also works through radare2's own HTTP server:
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
+10 -13
View File
@@ -16,10 +16,7 @@ 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"
@@ -50,7 +47,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
@@ -67,13 +64,14 @@ jobs:
make -C tests R2XSQL_PLUGIN="$GITHUB_WORKSPACE/build/bin/core_r2xsql.so"
- 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
@@ -130,13 +128,14 @@ jobs:
cmake --build build --config Release
- 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
@@ -173,19 +172,19 @@ jobs:
cmake --build build --config Release
- 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 +212,5 @@ 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
+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()
+10 -6
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,6 +35,7 @@ BUILD=build-make
# --- libxsql: sibling checkout by default, else fetched into build-make/ ------
LIBXSQL_DIR?=../libxsql
LIBXSQL_URL?=https://github.com/0xeb/libxsql.git
LIBXSQL_REF?=v1.0.21
ifeq ($(wildcard $(LIBXSQL_DIR)/CMakeLists.txt),)
LIBXSQL_DIR:=$(BUILD)/libxsql
endif
@@ -52,8 +53,8 @@ R2XSQL_INC=-I$(CWD)/src/lib/include -I$(CWD)/src/lib/src -I$(CWD)/src
# --- defines ------------------------------------------------------------------
# R2XSQL_HAVE_LIBR=1 -> compile the in-process libr backend (real, not stub).
# XSQL_VERSION_STRING -> libxsql's --version string. Derived from libxsql's own
# CMakeLists at build time so it always follows whatever libxsql main is at —
# no pinned value here (r2xsql tracks libxsql main). Override with
# CMakeLists at build time, so it follows the libxsql checkout in use (the
# pinned LIBXSQL_REF when fetched). Override with
# `make XSQL_VERSION=x` only if you must.
# (pattern has no '(' on purpose: an unbalanced paren inside $(shell ...) makes
# GNU make read it as an unterminated call.)
@@ -83,15 +84,18 @@ OBJS=$(SQLITE_OBJ) $(XSQL_OBJS) $(R2XSQL_OBJS) $(PLUGIN_OBJ)
all: $(PLUGIN)
# 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 $@)
+14 -1
View File
@@ -139,11 +139,17 @@ CMake, matching how radare2 plugins are usually built (plugin paths from
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 # 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):
`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
@@ -157,6 +163,13 @@ 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`).
Release assets are named `r2xsql-<radare2-version>-<R2PM_TRIPLET>.zip`, for
example `r2xsql-6.2.2-linux-x86-64.zip`. Each contains the plugin in `plugins/`.
With a recent r2pm supporting binary installs, 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`.
### JSON results
Use `sqlj` for programmatic queries and scripts:
+3 -2
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()
@@ -106,6 +106,7 @@ r2xsql_inc = include_directories('src/lib/include', 'src/lib/src', 'src')
shared_module('core_r2xsql',
[r2xsql_srcs, plugin_src],
name_prefix: '',
name_suffix: host_machine.system() == 'darwin' ? 'dylib' : [],
cpp_args: ['-DR2XSQL_HAVE_LIBR=1'],
include_directories: r2xsql_inc,
dependencies: [xsql_dep, r_core, threads],
+6 -6
View File
@@ -4,10 +4,9 @@
# This file is licensed under the Human-Origin Source License v1.0.
# See LICENSE.
# libr2xsql — static core library exposing radare2 state as SQLite virtual
# tables. Backend-neutral: it carries the mock + r2pipe backends only and links
# NO radare2, so consumers can link it without importing any r_*.dll. The libr
# backend lives in the r2xsql_libr OBJECT library below, used by
# in-process consumers and the in-r2 plugin.
# tables. It carries the mock + r2pipe backends and uses r_util (name checks
# go through r_name_check); the libr backend lives in the r2xsql_libr OBJECT
# library below, used by in-process consumers and the in-r2 plugin.
add_library(r2xsql STATIC
src/version.cpp
@@ -69,7 +68,8 @@ target_include_directories(r2xsql
${CMAKE_CURRENT_SOURCE_DIR}/src
)
target_link_libraries(r2xsql PUBLIC xsql::xsql)
# r_util for r_name_check, which the table sources call through <r_util.h>.
target_link_libraries(r2xsql PUBLIC xsql::xsql Radare2::r_util)
target_compile_features(r2xsql PUBLIC cxx_std_20)
if(WIN32)
@@ -86,7 +86,7 @@ set_target_properties(r2xsql PROPERTIES POSITION_INDEPENDENT_CODE ON)
# direct_libr.cpp is the C-API side of the src/direct.hpp seam: the row
# producers a ported table uses instead of running a command and parsing its
# JSON. It belongs here rather than in a tables_*.cpp because those are
# compiled into the radare2-free core library above.
# compiled into the core library above, which only uses r_util.
add_library(r2xsql_libr OBJECT
src/backend_libr.cpp
src/direct_libr.cpp
+5 -1
View File
@@ -33,7 +33,11 @@ namespace r2xsql {
// radare2 expects its data files.
LibrDataDirStatus libr_data_dir_status_at(const char* prefix) {
LibrDataDirStatus st;
char* pfx = r_sys_prefix(prefix);
// An explicit prefix is probed as given. Do NOT route it through
// r_sys_prefix(): up to radare2 6.1 that returned its argument, but from
// 6.2 it returns the executable-derived prefix (and exports R2_PREFIX=arg
// as a side effect), so the probe silently checked the wrong directory.
char* pfx = prefix ? strdup(prefix) : r_sys_prefix(nullptr);
if (pfx) {
char* dir = r_file_new(pfx, R2_SDB_FCNSIGN, nullptr); // <prefix>/share/fcnsign
if (dir) {
+20 -13
View File
@@ -185,8 +185,8 @@ struct BlockData {
// sliced by `asmop.size` (a SEPARATE ASM-side decode, `r_asm_disassemble`).
// This producer calls only `r_anal_op` (never `r_asm_disassemble` -- that
// call exists purely to build the mnemonic), reading `bytes` from its own
// per-block/per-range read buffer sliced by `op.size` -- `instructions-api-
// lab/FINDINGS.md` measured the two decoders never disagreeing on length or
// per-block/per-range read buffer sliced by `op.size` -- measured, the two decoders never
// disagreed on length or
// on which addresses fail to decode, across five real/generated fixtures,
// but this is a live risk to keep in mind if a future radare2 arch plugin
// ever changes that.
@@ -284,7 +284,7 @@ struct FlagRow {
// `RBinString` at all -- it is resolved per row via `r_bin_get_section_at`;
// `vaddr` is rebased through `r_bin_get_vaddr`, not read off the raw member;
// `type` is rendered through `r_bin_string_type`, not the raw char code.
// Measured in a private lab at 26-86x across a real PE, a large generated
// Measured at 26-86x across a real PE, a large generated
// fixture, and a real large DLL, with exact field parity.
struct StringRow {
int64_t addr = 0;
@@ -304,7 +304,7 @@ struct StringRow {
// (`r_str_srwx_i`), not the raw permission bitmask, and `addr` silently
// downgrades to NO rebase for a section lacking the READ permission bit --
// a real per-row branch on the section's own `perm`, not a fixed `va` mode
// for the whole table. Measured in a private lab at 19-58x (`sections`) and
// for the whole table. Measured at 19-58x (`sections`) and
// 58-195x (`segments`, vacuous on every PE fixture tested -- no PE section
// sets `is_segment`, so this rides `sections`' proven-live shared producer)
// with exact field parity.
@@ -326,7 +326,7 @@ struct SectionRow {
// `RBinInfo::has_lit`. All of this goes through PUBLIC `R_API` calls, never
// a private/static helper -- the producer calls the same functions the
// emitter calls, it does not reimplement demangling or address resolution.
// Measured in a private lab at 10-13x across a real PE, a large generated
// Measured at 10-13x across a real PE, a large generated
// fixture, and a real 1986-import DLL, with exact field parity (the
// genuine C++-demangling branch itself is unexercised by any available
// fixture -- radare2 is a C codebase, so every fixture's imports are
@@ -342,9 +342,8 @@ struct ImportRow {
// One row of `comments` -- the command path's `CCj`, a single full-scan
// command (not an N+1-per-function shape, unlike `funcs`/`xrefs`/`locals`/the
// blocks cluster -- the same "one command already" shape `binary-meta-lab`
// found for `strings`/`sections`/`imports`/`entries`). Measured in a private
// lab at 5-70x with exact field parity, including two quirks found by
// blocks cluster -- the same "one command already" shape measured for
// `strings`/`sections`/`imports`/`entries`). Measured at 5-70x with exact field parity, including two quirks found by
// reading `r_meta_print` (`libr/anal/meta.c`) rather than assumed from the
// struct:
//
@@ -376,7 +375,7 @@ struct CommentRow {
// (exports), merged by address (this project's own `merge_row`, preserved
// exactly on the API leg). `iej`'s JSON never emits `name`/`size`/`bind`
// (confirmed by reading `bin_entry`'s JSON block field-by-field) but DOES
// emit `type` via `r_bin_entry_type_string` -- an early lab probe omitted
// emit `type` via `r_bin_entry_type_string` -- an early probe omitted
// this and showed a real parity failure before the producer was corrected.
// `entries.name` is NOT the demangled name despite living one call away from
// a full demangling pipeline: reading `bin_symbols`' JSON emit block found
@@ -384,7 +383,7 @@ struct CommentRow {
// name), UTF8-escaped -- the demangled form goes to a separate `"demname"`
// key this table never reads. `its_an_export` (`!is_imported && bind ==
// "GLOBAL"`) is a private/static helper in radare2 but trivial to reproduce
// without calling it. Measured in a private lab at 9-78x across a real PE,
// without calling it. Measured at 9-78x across a real PE,
// a large generated fixture, and a real 755-export DLL, with exact
// field parity.
struct EntryRow {
@@ -564,7 +563,15 @@ class Api {
// eight fields are: radare2 builds it per function from the type database,
// and it dominates the cost of this scan. The caller passes false whenever
// the query does not read that column.
virtual void funcs_scan(bool with_prototype, std::vector<FuncRow>& out) = 0;
// `with_cost` gates r_anal_function_cost, which decodes every instruction
// and is most of a prototype-less scan (it left cost 0 when false).
virtual void funcs_scan(bool with_prototype, bool with_cost,
std::vector<FuncRow>& out) = 0;
// `funcs` equality lookup -- the command path's `afij @ <addr>`, restricted
// to a function STARTING at `addr`. False when none does.
virtual bool func_at(int64_t addr, bool with_prototype, bool with_cost,
FuncRow& out) = 0;
// `blocks`/`cfg_edges`/`switch_tables`/`dominators`/`post_dominators`/
// `loops` — ONE function's basic-block list, the command path's
@@ -617,7 +624,7 @@ class Api {
// `fi->{addr,size,name,realname,space->name}` directly -- no `fs`
// selection, no per-space re-listing, no session-state save/restore at
// all, since `r_flag_foreach` is not scoped by the currently-selected
// flagspace the way the `fj` COMMAND is. Measured in a private lab at
// flagspace the way the `fj` COMMAND is. Measured at
// 17-47x on a full scan across a real binary and generated fixtures, plus
// a real large DLL, with exact field parity -- including the pointer-
// equality `realname` quirk -- before being ported.
@@ -659,7 +666,7 @@ class Api {
//
// Unlike every READ producer above (an N+1-per-row scan, where command
// dispatch is paid once per row), a single write is one command either
// way -- there is no N+1 shape to collapse. A private standalone lab
// way -- there is no N+1 shape to collapse. A benchmark
// measured a BULK workload instead (N repeated writes in a loop,
// simulating a bulk-rename/bulk-annotate script) and found each of the
// four operations examined pays a real, per-call fixed cost for
+77 -49
View File
@@ -457,7 +457,11 @@ class LibrApi : public Api {
std::lock_guard lock(mu_);
if (!core_ || !core_->bin) return;
RIODesc* desc = r_io_desc_get(core_->io, r_io_fd_get_current(core_->io));
#if R2_ABIVERSION >= 144
if (desc && r_io_desc_info(desc).isdbg) return;
#else
if (desc && r_io_desc_is_dbg(desc)) return;
#endif
RVecRBinReloc* relocs = r_bin_get_relocs(core_->bin);
// `irj` applies pending relocations when requested, including the IO
// cache/overlay routing used for relocatable object files.
@@ -465,7 +469,9 @@ class LibrApi : public Api {
const bool cache = r_config_get_b(core_->config, "bin.cache");
if ((apply || cache) && core_->bin->cur) {
if (cache) {
if (!(core_->io->cachemode & R_PERM_W)) r_config_set_b(core_->config, "io.cache", true);
// `cachemode` is a bool, so radare2's own `cachemode & R_PERM_W` in
// cbin.c is always 0 and always sets io.cache; test the bool itself.
if (!core_->io->cachemode) r_config_set_b(core_->config, "io.cache", true);
core_->bin->iob.overlay_write_at = apply ? r_io_cache_write_at : r_io_write_at;
}
relocs = r_bin_patch_relocs(core_->bin->cur);
@@ -744,61 +750,83 @@ class LibrApi : public Api {
// what `aflj` prints.
//
// A field-by-field parity test against the command path guards all nine.
void funcs_scan(bool with_prototype, std::vector<FuncRow>& out) override {
void funcs_scan(bool with_prototype, bool with_cost,
std::vector<FuncRow>& out) override {
std::lock_guard lock(mu_);
if (!core_ || !core_->anal) return;
for_each_rlist<RAnalFunction>(core_->anal->fcns, [&](RAnalFunction* fcn) {
FuncRow r;
r.addr = static_cast<int64_t>(fcn->addr);
if (char* name = r_core_anal_fcn_name(core_, fcn)) {
r.name = name;
free(name);
}
r.size = static_cast<int64_t>(r_anal_function_linear_size(fcn));
r.nbbs = static_cast<int64_t>(r_list_length(fcn->bbs));
int ebbs = 0;
r.edges = r_anal_function_count_edges(fcn, &ebbs);
r.cc = r_anal_function_complexity(fcn);
if (const char* t = r_anal_functiontype_tostring(fcn->type)) r.type = t;
// `aflj` omits `calltype` entirely when radare2 has none, which reads
// back as the empty string — so an absent convention must stay empty
// rather than becoming some default.
if (const char* cc = r_anal_function_cc(fcn)) r.calltype = cc;
// The fields `aflj` emits and the table used to drop. Same calls
// `fcn_print_json` makes, in the same order.
r.end_addr = static_cast<int64_t>(r_anal_function_max_addr(fcn));
r.ninstrs = r_anal_function_instrcount(fcn);
r.stackframe = fcn->maxstack;
r.cost = r_anal_function_cost(fcn);
r.tracecov = r_anal_function_coverage(fcn);
r.is_noreturn = fcn->is_noreturn;
r.is_pure = r_anal_function_purity(fcn);
// `recursive` has no single call behind it: radare2 computes it inline
// (a static helper) as "does this function call itself". Same test here.
if (RVecAnalRef* calls = r_core_anal_fcn_get_calls(core_, fcn)) {
RAnalRef* callref;
R_VEC_FOREACH (calls, callref) {
if (callref->addr == fcn->addr) { r.is_recursive = true; break; }
}
RVecAnalRef_free(calls);
}
if (with_prototype) {
// `aflj` fills `signature` by running `afcf`, which is
// r_anal_function_format_sig with the function's own name (the NULL
// default below), then trimming. Note this is a DIFFERENT function
// from r_anal_function_get_signature_string, which formats its own way.
if (char* sig = r_anal_function_format_sig(core_->anal, fcn, nullptr,
nullptr, nullptr, nullptr)) {
r_str_trim(sig);
r.prototype = sig;
free(sig);
}
}
fill_func_row(fcn, with_prototype, with_cost, r);
out.push_back(std::move(r));
});
}
// One function, by its exact START address: the `WHERE addr = X` lookup a
// JOIN or correlated subquery onto `funcs` issues per outer row. An address
// inside a function (not its entry) answers false, as `addr = X` requires.
bool func_at(int64_t addr, bool with_prototype, bool with_cost,
FuncRow& out) override {
std::lock_guard lock(mu_);
if (!core_ || !core_->anal) return false;
RAnalFunction* fcn =
r_anal_get_function_at(core_->anal, static_cast<ut64>(addr));
if (!fcn) return false;
fill_func_row(fcn, with_prototype, with_cost, out);
return true;
}
// Reads one function into a FuncRow -- shared by `funcs_scan` and `func_at`
// so a filtered row and a scanned row can never drift. Caller holds `mu_`.
void fill_func_row(RAnalFunction* fcn, bool with_prototype, bool with_cost,
FuncRow& r) {
r.addr = static_cast<int64_t>(fcn->addr);
if (char* name = r_core_anal_fcn_name(core_, fcn)) {
r.name = name;
free(name);
}
r.size = static_cast<int64_t>(r_anal_function_linear_size(fcn));
r.nbbs = static_cast<int64_t>(r_list_length(fcn->bbs));
int ebbs = 0;
r.edges = r_anal_function_count_edges(fcn, &ebbs);
r.cc = r_anal_function_complexity(fcn);
if (const char* t = r_anal_functiontype_tostring(fcn->type)) r.type = t;
// `aflj` omits `calltype` entirely when radare2 has none, which reads
// back as the empty string — so an absent convention must stay empty
// rather than becoming some default.
if (const char* cc = r_anal_function_cc(fcn)) r.calltype = cc;
// The fields `aflj` emits and the table used to drop. Same calls
// `fcn_print_json` makes, in the same order.
r.end_addr = static_cast<int64_t>(r_anal_function_max_addr(fcn));
r.ninstrs = r_anal_function_instrcount(fcn);
r.stackframe = fcn->maxstack;
if (with_cost) r.cost = r_anal_function_cost(fcn);
r.tracecov = r_anal_function_coverage(fcn);
r.is_noreturn = fcn->is_noreturn;
r.is_pure = r_anal_function_purity(fcn);
// `recursive` has no single call behind it: radare2 computes it inline
// (a static helper) as "does this function call itself". Same test here.
if (RVecAnalRef* calls = r_core_anal_fcn_get_calls(core_, fcn)) {
RAnalRef* callref;
R_VEC_FOREACH (calls, callref) {
if (callref->addr == fcn->addr) { r.is_recursive = true; break; }
}
RVecAnalRef_free(calls);
}
if (with_prototype) {
// `aflj` fills `signature` by running `afcf`, which is
// r_anal_function_format_sig with the function's own name (the NULL
// default below), then trimming. Note this is a DIFFERENT function
// from r_anal_function_get_signature_string, which formats its own way.
if (char* sig = r_anal_function_format_sig(core_->anal, fcn, nullptr,
nullptr, nullptr, nullptr)) {
r_str_trim(sig);
r.prototype = sig;
free(sig);
}
}
}
// Reads one RAnalBlock into BlockData — shared by both `blocks_in_func` and
// `blocks_scan` below so the two can never drift on what a block carries.
static void fill_block(RAnalBlock* b, BlockData& d) {
@@ -1057,7 +1085,7 @@ class LibrApi : public Api {
// `func_addr` stays 0 on every row here). Refills its read buffer from IO
// whenever fewer than 32 bytes remain -- the same low-water threshold the
// command path's own `pdj` loop uses (`disasm.c`, `i >= nb_bytes - 32`,
// confirmed by `pd-range-lab`), since the longest x86 instruction is under
// confirmed by measurement), since the longest x86 instruction is under
// 16 bytes and every other arch this project targets is shorter still.
void instructions_range(int64_t start_addr, int64_t count,
std::vector<InsnRow>& out) override {
+16 -7
View File
@@ -4,6 +4,7 @@
// This file is licensed under the Human-Origin Source License v1.0.
// See LICENSE.
#include "json_utils.hpp"
#include <r_util.h>
#include <cctype>
#include <cstdio>
@@ -58,14 +59,22 @@ bool boolean(const nlohmann::json& obj, std::string_view key, bool dflt) {
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;
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);
}
}
return true;
out.push_back('"');
return out;
}
bool valid_type_text(std::string_view type) {
+5 -6
View File
@@ -33,12 +33,11 @@ int64_t i64_addr(const nlohmann::json& obj, int64_t dflt = 0);
// not a shell, but spaces, quotes and backslashes need escaping in CC args.
// True iff `name` is safe to splice into an r2 flag command unquoted: a
// non-empty string of [A-Za-z0-9._$] only. This is roughly r2's own flag-name
// charset; rejecting anything else prevents a name with spaces or command
// separators (`;`, `@`, `|`, backtick, …) from breaking parsing or injecting
// extra r2 commands. Use to gate INSERT/UPDATE on the flags/bookmarks tables.
bool valid_flag_name(std::string_view name);
// Quote a string for use as a shell-ish r2 command argument. r2 itself is
// not a shell, but spaces, quotes and backslashes need escaping in CC args.
std::string r2_quote(std::string_view s);
// True iff `type` is a safe C type string to splice into an r2 `afvt` command
// unquoted: a non-empty string of type-syntax characters only —
+18
View File
@@ -15,6 +15,8 @@
#include <string>
#include <string_view>
#include <xsql/vtable.hpp>
#include "direct.hpp"
#include "json_utils.hpp"
@@ -57,6 +59,22 @@ inline std::string command_error_text(const std::string& out) {
return {};
}
// An integer INSERT argument (an address, a size), taken only when it is
// exactly an integer (xsql::exact_int64: 4096, 4096.0 and '4096' all are).
// SQLite applies no column affinity to a virtual table's INSERT values, so
// as_int64() would write 'main' at address 0 and 4096.5 at 4096; this refuses
// both with a vtab error naming `what` instead.
inline bool exact_write_int64(const xsql::FunctionArg& v, std::string_view what,
int64_t& out) {
const auto exact = xsql::exact_int64(v);
if (!exact) {
xsql::set_vtab_error(std::string(what) + " must be an integer");
return false;
}
out = *exact;
return true;
}
inline bool resolve_func_owner(Backend* be, int64_t addr, int64_t& out) {
if (auto* api = be->direct()) return api->function_owner(addr, out);
auto info = be->cmd_json("afij @ " + json_utils::hex_addr(addr));
+30 -4
View File
@@ -233,11 +233,24 @@ void register_assemble_table(Session& s) {
-> std::unique_ptr<xsql::Generator<AssembleRow>> {
std::string asm_text;
int64_t addr = 0;
// omit=1: SQLite does not re-check these values, so each is compared the
// way a native column would (xsql::exact_*): a value nothing can equal
// (10.5 for an integer, NULL, a blob) yields no rows.
for (const auto& a : args) {
if (a.op != xsql::ConstraintOp::Eq) continue;
switch (a.column_index) {
case kAAsm: { const char* v = a.value.as_c_str(); if (v) asm_text = v; break; }
case kAAddr: addr = a.value.as_int64(); break;
case kAAsm: {
auto v = xsql::exact_text(a.value);
if (!v) return nullptr;
asm_text = std::move(*v);
break;
}
case kAAddr: {
const auto v = xsql::exact_int64(a.value);
if (!v) return nullptr;
addr = *v;
break;
}
default: break;
}
}
@@ -273,11 +286,24 @@ void register_search_asm_table(Session& s) {
-> std::unique_ptr<xsql::Generator<SearchAsmRow>> {
std::string asm_text;
int64_t max_results = 0;
// omit=1: SQLite does not re-check these values, so each is compared the
// way a native column would (xsql::exact_*): a value nothing can equal
// (10.5 for an integer, NULL, a blob) yields no rows.
for (const auto& a : args) {
if (a.op != xsql::ConstraintOp::Eq) continue;
switch (a.column_index) {
case kSAsm: { const char* v = a.value.as_c_str(); if (v) asm_text = v; break; }
case kSMaxResults: max_results = a.value.as_int64(); break;
case kSAsm: {
auto v = xsql::exact_text(a.value);
if (!v) return nullptr;
asm_text = std::move(*v);
break;
}
case kSMaxResults: {
const auto v = xsql::exact_int64(a.value);
if (!v) return nullptr;
max_results = *v;
break;
}
default: break;
}
}
+3 -1
View File
@@ -86,7 +86,9 @@ void register_bininfo_table(Session& s) {
r.has_pi = opt_bool01(info, "pic");
r.default_cc = opt_str(info, "cc");
r.file = table_sources::file_path(be);
// No file is SQL NULL, not '' -- the same "unknown reads NULL" contract
// the havecode-gated columns keep.
if (auto path = table_sources::file_path(be); !path.empty()) r.file = std::move(path);
rows.push_back(std::move(r));
})
+7 -5
View File
@@ -39,6 +39,7 @@
#include <r2xsql/backend.hpp>
#include <xsql/vtable.hpp>
#include <r_util.h>
#include "tables.hpp"
#include "json_utils.hpp"
#include "table_utils.hpp"
@@ -126,9 +127,9 @@ void register_bookmarks_table(Session& s) {
[](const BookmarkRow& r) { return r.name; },
[be = s.backend_ptr()](BookmarkRow& r, const char* v) -> bool {
if (!v || !*v) return false;
if (!json_utils::valid_flag_name(v)) {
if (!r_name_check(v)) {
xsql::set_vtab_error(
"bookmarks: name must be [A-Za-z0-9._$] (no spaces or r2 metacharacters)");
"bookmarks: name must start with a letter, '_' or ':' and use only [A-Za-z0-9._:]");
return false;
}
if (r.name == v) return true;
@@ -174,15 +175,16 @@ void register_bookmarks_table(Session& s) {
xsql::set_vtab_error("bookmarks: INSERT requires addr and name");
return false;
}
const int64_t addr = argv[1].as_int64();
int64_t addr = 0;
if (!table_utils::exact_write_int64(argv[1], "bookmarks: addr", addr)) return false;
const char* name = argv[2].as_c_str();
if (!name || !*name) {
xsql::set_vtab_error("bookmarks: INSERT requires a non-empty name");
return false;
}
if (!json_utils::valid_flag_name(name)) {
if (!r_name_check(name)) {
xsql::set_vtab_error(
"bookmarks: name must be [A-Za-z0-9._$] (no spaces or r2 metacharacters)");
"bookmarks: name must start with a letter, '_' or ':' and use only [A-Za-z0-9._:]");
return false;
}
BookmarkSpaceScope scope(*be);
+43 -34
View File
@@ -10,7 +10,6 @@
// client-side so no global r2 search config is mutated.
#include <cstdint>
#include <limits>
#include <memory>
#include <string>
#include <vector>
@@ -158,14 +157,13 @@ class ByteSearchGen : public xsql::Generator<ByteSearchRow> {
public:
ByteSearchGen(Backend* be, const std::string& command_token,
size_t pattern_size,
int64_t start, int64_t end, int64_t max_results) {
const xsql::Int64KeyRange& window, int64_t max_results) {
if (command_token.empty() || pattern_size == 0) return;
auto hits = be->cmd_json("/xj " + command_token);
if (!hits.is_array()) return;
for (auto& h : hits) {
const int64_t addr = json_utils::i64(h, "addr");
if (start >= 0 && addr < start) continue;
if (end >= 0 && addr >= end) continue;
if (!window.contains(addr)) continue;
ByteSearchRow r;
r.addr = addr;
r.matched_bytes = full_match_bytes(be, h, addr, pattern_size);
@@ -189,48 +187,59 @@ void register_byte_search_table(Session& s) {
auto make_gen = [be = s.backend_ptr()](
const std::vector<xsql::GeneratorConstraintArg>& args)
-> std::unique_ptr<xsql::Generator<ByteSearchRow>> {
// omit=1 on every constraint below: SQLite does not re-check these
// values, so each is compared the way a native column would
// (xsql::exact_* / Int64KeyRange). A value nothing can equal (10.5 for an
// integer, NULL, a blob) yields no rows rather than a truncated search.
//
// The window is in the addr column's own order -- signed int64, the way
// SQLite sorts it -- so a ut64 address at or above 2^63 (reported
// negative) is bounded like any other, never mistaken for "unset".
// start_addr is inclusive, end_addr exclusive.
std::string pattern;
int64_t start = -1, end = -1, max_results = 0;
xsql::Int64KeyRange window;
int64_t max_results = 0;
for (const auto& a : args) {
if (a.op != xsql::ConstraintOp::Eq) {
// addr range predicates (Ge/Gt lower, Lt/Le upper) tighten bounds.
if (a.column_index == kAddr) {
const int64_t v = a.value.as_int64();
if (a.op == xsql::ConstraintOp::Ge) start = (start < 0) ? v : (v > start ? v : start);
else if (a.op == xsql::ConstraintOp::Gt) {
if (v == std::numeric_limits<int64_t>::max()) {
start = v;
end = v;
} else {
int64_t w = v + 1;
start = (start < 0) ? w : (w > start ? w : start);
}
}
else if (a.op == xsql::ConstraintOp::Lt) end = (end < 0) ? v : (v < end ? v : end);
else if (a.op == xsql::ConstraintOp::Le &&
v != std::numeric_limits<int64_t>::max()) {
int64_t w = v + 1;
end = (end < 0) ? w : (w < end ? w : end);
}
}
continue;
}
switch (a.column_index) {
case kPattern: { const char* p = a.value.as_c_str(); if (p) pattern = p; break; }
case kStart: start = a.value.as_int64(); break;
case kEnd: end = a.value.as_int64(); break;
case kMaxResults: max_results = a.value.as_int64(); break;
case kAddr:
window.constrain(a.op, a.value);
break;
case kPattern: {
auto v = xsql::exact_text(a.value);
if (!v) return nullptr;
pattern = std::move(*v);
break;
}
case kStart: {
const auto v = xsql::exact_int64(a.value);
if (!v) return nullptr;
window.constrain(xsql::ConstraintOp::Ge, *v);
break;
}
case kEnd: {
const auto v = xsql::exact_int64(a.value);
if (!v) return nullptr;
window.constrain(xsql::ConstraintOp::Lt, *v);
break;
}
case kMaxResults: {
const auto v = xsql::exact_int64(a.value);
if (!v) return nullptr;
max_results = *v;
break;
}
default: break;
}
}
if (window.empty()) return nullptr;
ParsedPattern parsed = parse_pattern(pattern);
if (!parsed.error.empty()) {
xsql::set_vtab_error(parsed.error);
return std::make_unique<ByteSearchGen>(
be, std::string{}, 0, 0, 0, 0);
be, std::string{}, 0, window, 0);
}
return std::make_unique<ByteSearchGen>(
be, parsed.command_token, parsed.byte_count, start, end, max_results);
be, parsed.command_token, parsed.byte_count, window, max_results);
};
auto def = xsql::generator_table<ByteSearchRow>("byte_search")
+8 -1
View File
@@ -395,7 +395,14 @@ void register_bytes_table(Session& s) {
xsql::set_vtab_error("bytes: UPDATE value cannot be set to NULL");
return false;
}
const int64_t iv = v.as_int64();
// Exact integers only: 65.5 is refused, never truncated to 65.
const auto exact = xsql::exact_int64(v);
if (!exact) {
xsql::set_vtab_error(
"bytes: UPDATE value must be an integer between 0 and 255");
return false;
}
const int64_t iv = *exact;
if (iv < 0 || iv > 255) {
xsql::set_vtab_error(
"bytes: UPDATE value must be an integer between 0 and 255 (got " +
+3 -2
View File
@@ -29,7 +29,7 @@
//
// The WRITE path (this table's `text` UPDATE and INSERT) is ALSO now
// C-API-backed on the libr flavor, a separate, later port from both of the
// above: a private standalone lab measured a bulk workload (N=500 repeated
// above: a benchmark measured a bulk workload (N=500 repeated
// sets, simulating a bulk-annotate script) at 30-40x over the CMD leg
// (`r_core_call_str_at("CCu base64:...")`, the SAME dispatch this table
// already issues), with exact field/behavior parity once the API leg
@@ -145,7 +145,8 @@ void register_comments_table(Session& s) {
xsql::set_vtab_error("comments: INSERT requires addr and text");
return false;
}
const int64_t addr = argv[0].as_int64();
int64_t addr = 0;
if (!table_utils::exact_write_int64(argv[0], "comments: addr", addr)) return false;
const char* text = argv[2].as_c_str();
const std::string body = text ? text : "";
if (auto* api = be->direct()) {
+4 -2
View File
@@ -72,8 +72,10 @@ void register_demangle_table(Session& s) {
for (const auto& a : args) {
if (a.op != xsql::ConstraintOp::Eq) continue;
switch (a.column_index) {
case kMangled: { const char* v = a.value.as_c_str(); if (v) mangled = v; break; }
case kLang: { const char* v = a.value.as_c_str(); if (v) lang = v; break; }
// omit=1: compared like a native TEXT column (xsql::exact_text), so a
// NULL or a blob yields no rows rather than an empty-string lookup.
case kMangled: { auto v = xsql::exact_text(a.value); if (!v) return nullptr; mangled = std::move(*v); break; }
case kLang: { auto v = xsql::exact_text(a.value); if (!v) return nullptr; lang = std::move(*v); break; }
default: break;
}
}
+10 -8
View File
@@ -21,7 +21,7 @@
// A full scan is that same enumeration: one `fj` (full dump) + one
// `fs <name>; fj; fs *` PER FLAGSPACE to attribute the real space back onto
// each flag by name. On the libr backend this reads `r_flag_foreach` directly
// instead — measured in a private lab at 17-47x on a full scan across a real
// instead — measured at 17-47x on a full scan across a real
// binary, this project's own scale fixtures, and a real large DLL, with exact
// field parity (including a pointer-equality quirk in radare2's own
// `realname` field), before being ported (see `direct.hpp` for why the second
@@ -61,7 +61,7 @@
// unbuilt and is not needed here.
//
// The rename is ALSO now C-API-backed on the libr flavor, a separate, later
// port from the cmd_noeval rollout above: a private standalone lab
// port from the cmd_noeval rollout above: a benchmark
// measured a bulk workload (N up to 500 repeated renames, simulating a
// bulk-rename script) at 48-62x over the CMD leg (the SAME
// `r_core_call_str_at("fr ...")` dispatch this table already issues), with
@@ -77,6 +77,7 @@
#include <r2xsql/backend.hpp>
#include <xsql/vtable.hpp>
#include <r_util.h>
#include "tables.hpp"
#include "direct.hpp"
#include "json_utils.hpp"
@@ -127,7 +128,7 @@ void build_flags(Backend* be, std::vector<FlagRow>& rows) {
const std::string name = json_utils::str(sp, "name");
// A space name is spliced into a command, so it gets the same charset gate
// as every other name this project sends to radare2.
if (name.empty() || !json_utils::valid_flag_name(name)) continue;
if (name.empty() || !r_name_check(name.c_str())) continue;
if (json_utils::i64(sp, "count") == 0) continue; // nothing to attribute
// One command, not three: selecting a flagspace is session state, so the
@@ -163,9 +164,9 @@ void register_flags_table(Session& s) {
[](const FlagRow& r) { return r.name; },
[be = s.backend_ptr()](FlagRow& r, const char* v) -> bool {
if (!v || !*v) return false;
if (!json_utils::valid_flag_name(v)) {
if (!r_name_check(v)) {
xsql::set_vtab_error(
"flags: name must be [A-Za-z0-9._$] (no spaces or r2 metacharacters)");
"flags: name must start with a letter, '_' or ':' and use only [A-Za-z0-9._:]");
return false;
}
if (r.name == v) return true;
@@ -254,15 +255,16 @@ void register_flags_table(Session& s) {
xsql::set_vtab_error("flags: INSERT requires addr and name");
return false;
}
const int64_t addr = argv[0].as_int64();
int64_t addr = 0;
if (!table_utils::exact_write_int64(argv[0], "flags: addr", addr)) return false;
const char* name = argv[2].as_c_str();
if (!name || !*name) {
xsql::set_vtab_error("flags: INSERT requires a non-empty name");
return false;
}
if (!json_utils::valid_flag_name(name)) {
if (!r_name_check(name)) {
xsql::set_vtab_error(
"flags: name must be [A-Za-z0-9._$] (no spaces or r2 metacharacters)");
"flags: name must start with a letter, '_' or ':' and use only [A-Za-z0-9._:]");
return false;
}
std::string out = dispatch_write(be, addr, std::string("f ") + name);
+136 -42
View File
@@ -12,7 +12,7 @@
// radare2's own command handler, seeked to the target address, called
// directly without evaluating its command-chaining grammar (`;`/`@`/`|`/
// backtick) at all. This is an ADDITIONAL layer alongside this table's
// existing grammar/value validation (`valid_flag_name`, `valid_c_signature`,
// existing grammar/value validation (`r_name_check`, `valid_c_signature`,
// the `calling_convention_exists` check on `calltype`) — none replaces the
// others. On every other backend (r2pipe, mock) cmd_noeval reports
// unsupported and the exact same `cmd()` + spliced `@ <addr>` string this
@@ -20,8 +20,7 @@
// pilot of this dispatch.
//
// `name`'s rename is explicitly REFUSED a C-API port, unlike the sibling
// rename/set writes on `comments`/`flags`/`locals` (measured together in a
// private standalone lab). Not a speed question — `afn`'s bulk-rename
// rename/set writes on `comments`/`flags`/`locals` (measured together). Not a speed question — `afn`'s bulk-rename
// cost clears the same 3x bar the others did. The refusal is PARITY: reading
// `afn`'s actual handler (`__setFunctionName`, `cmd_anal.inc.c`) found it
// does substantially more than `r_anal_function_rename` alone —
@@ -32,12 +31,12 @@
// reads back through `afcf`;
// * it fires the `on_fcn_rename` callback.
// `r_anal_function_rename` touches ONLY the function's own name field and
// hashtable entries — none of the above. The write-path lab proved this
// live, not just from the source read: after an equal-sized bulk rename on
// hashtable entries — none of the above. Measured live, not just
// read from the source: after an equal-sized bulk rename on
// two independent cores, the CMD leg's flag was renamed on 100% of rows and
// the API leg's flag was renamed on 0% — a real, silent desync between
// `funcs.name` and its own backing flag, the exact bug class already fixed
// once this session for `flags.name` (`afn` creating a duplicate flag
// for `flags.name` (`afn` creating a duplicate flag
// instead of renaming in place). Porting this write would require
// reimplementing `__setFunctionName` in full, which is not a "call the C
// API instead" port at all — it is copying that problem in reverse. Stays
@@ -50,12 +49,14 @@
#include <cctype>
#include <string_view>
#include <r_util.h>
#include "tables.hpp"
#include "direct.hpp"
#include "json_utils.hpp"
#include "table_utils.hpp"
#include <cstdint>
#include <memory>
#include <vector>
namespace r2xsql {
@@ -70,45 +71,119 @@ using direct::FuncRow;
// when the query does not read it is worth roughly an order of magnitude.
constexpr uint64_t kColUsedAll = 1ull << 63;
constexpr uint64_t kPrototypeColBit = 1ull << 8;
// Same idea for `cost`: r_anal_function_cost decodes every instruction of
// every function. On radare2 6.2 it is ~70% of a prototype-less scan, and a
// join onto `funcs` (the `callers` view) rescans the table per outer row, so
// paying it there pushed a real binary past the query timeout.
constexpr uint64_t kCostColBit = 1ull << 12;
void build_funcs(Backend* be, std::vector<FuncRow>& rows, bool with_prototype) {
// One `aflj` / `afij` function object -> FuncRow. Both commands emit the same
// per-function object (fcn_print_json), so the scan and the single-function
// lookup share this and cannot drift.
FuncRow func_row_from_json(const nlohmann::json& j, bool with_prototype) {
FuncRow r;
r.addr = json_utils::i64_addr(j);
r.name = json_utils::str(j, "name");
r.size = json_utils::i64(j, "size");
r.nbbs = json_utils::i64(j, "nbbs");
r.edges = json_utils::i64(j, "edges");
r.cc = json_utils::i64(j, "cc");
r.type = json_utils::str(j, "type");
r.calltype = json_utils::str(j, "calltype");
// One `aflj` carries every field, so on this path the signature is already
// paid for whether or not the query wants it.
if (with_prototype) r.prototype = json_utils::str(j, "signature");
r.end_addr = json_utils::i64(j, "maxaddr");
r.ninstrs = json_utils::i64(j, "ninstrs");
r.stackframe = json_utils::i64(j, "stackframe");
r.cost = json_utils::i64(j, "cost");
r.tracecov = json_utils::i64(j, "tracecov");
r.is_noreturn = json_utils::boolean(j, "noreturn");
// `is-pure` is the odd one out twice over: the key is hyphenated, and
// radare2 prints it through r_str_bool, so it arrives as the STRING
// "true"/"false" rather than a JSON boolean. Reading it as a bool yields
// false for every function.
r.is_pure = json_utils::str(j, "is-pure") == "true";
r.is_recursive = json_utils::boolean(j, "recursive");
return r;
}
void build_funcs(Backend* be, std::vector<FuncRow>& rows, bool with_prototype,
bool with_cost) {
if (auto* api = be->direct()) {
api->funcs_scan(with_prototype, rows);
api->funcs_scan(with_prototype, with_cost, rows);
return;
}
auto arr = be->cmd_json("aflj");
if (!arr.is_array()) return;
rows.reserve(arr.size());
for (auto& j : arr) {
FuncRow r;
r.addr = json_utils::i64_addr(j);
r.name = json_utils::str(j, "name");
r.size = json_utils::i64(j, "size");
r.nbbs = json_utils::i64(j, "nbbs");
r.edges = json_utils::i64(j, "edges");
r.cc = json_utils::i64(j, "cc");
r.type = json_utils::str(j, "type");
r.calltype = json_utils::str(j, "calltype");
// One `aflj` carries every field, so on this path the signature is already
// paid for whether or not the query wants it.
if (with_prototype) r.prototype = json_utils::str(j, "signature");
r.end_addr = json_utils::i64(j, "maxaddr");
r.ninstrs = json_utils::i64(j, "ninstrs");
r.stackframe = json_utils::i64(j, "stackframe");
r.cost = json_utils::i64(j, "cost");
r.tracecov = json_utils::i64(j, "tracecov");
r.is_noreturn = json_utils::boolean(j, "noreturn");
// `is-pure` is the odd one out twice over: the key is hyphenated, and
// radare2 prints it through r_str_bool, so it arrives as the STRING
// "true"/"false" rather than a JSON boolean. Reading it as a bool yields
// false for every function.
r.is_pure = json_utils::str(j, "is-pure") == "true";
r.is_recursive = json_utils::boolean(j, "recursive");
rows.push_back(std::move(r));
}
for (auto& j : arr) rows.push_back(func_row_from_json(j, with_prototype));
}
// The function STARTING at `addr`, or false. `afij @ X` resolves the function
// CONTAINING X, so a mid-function X must be refused here: `addr = X` asks for
// an entry point, and SQLite does not re-check a pushed-down constraint.
bool func_at(Backend* be, int64_t addr, FuncRow& out) {
if (auto* api = be->direct()) {
return api->func_at(addr, /*with_prototype=*/true, /*with_cost=*/true, out);
}
auto info = be->cmd_json("afij @ " + json_utils::hex_addr(addr));
const nlohmann::json* func = nullptr;
if (info.is_array() && !info.empty() && info[0].is_object()) {
func = &info[0];
} else if (info.is_object()) {
func = &info;
}
if (!func || json_utils::i64_addr(*func) != addr) return false;
out = func_row_from_json(*func, /*with_prototype=*/true);
return true;
}
// Zero-or-one row for the `addr` equality pushdown. Column order MUST match
// the builder in register_funcs_table: a filtered row has to equal, column for
// column, the same row from a full scan.
class FuncAtIterator : public xsql::RowIterator {
FuncRow row_;
bool have_ = false;
bool started_ = false;
public:
FuncAtIterator(Backend* be, int64_t addr) { have_ = func_at(be, addr, row_); }
bool next() override {
if (started_) { have_ = false; return false; }
started_ = true;
return have_;
}
bool eof() const override { return started_ && !have_; }
int64_t rowid() const override { return row_.addr; }
void column(xsql::FunctionContext& ctx, int col) override {
const FuncRow& r = row_;
switch (col) {
case 0: ctx.result_int64(r.addr); break;
case 1: ctx.result_text(r.name); break;
case 2: ctx.result_int64(r.size); break;
case 3: ctx.result_int64(r.nbbs); break;
case 4: ctx.result_int64(r.edges); break;
case 5: ctx.result_int64(r.cc); break;
case 6: ctx.result_text(r.type); break;
case 7: ctx.result_text(r.calltype); break;
case 8: ctx.result_text(r.prototype); break;
case 9: ctx.result_int64(r.end_addr); break;
case 10: ctx.result_int64(r.ninstrs); break;
case 11: ctx.result_int64(r.stackframe); break;
case 12: ctx.result_int64(r.cost); break;
case 13: ctx.result_int64(r.tracecov); break;
case 14: ctx.result_int64(r.is_noreturn ? 1 : 0); break;
case 15: ctx.result_int64(r.is_pure ? 1 : 0); break;
case 16: ctx.result_int64(r.is_recursive ? 1 : 0); break;
default: ctx.result_null(); break;
}
}
};
std::string compact_signature(std::string_view signature) {
while (!signature.empty() &&
(std::isspace(static_cast<unsigned char>(signature.back())) ||
@@ -202,13 +277,23 @@ void register_funcs_table(Session& s) {
return table_utils::quick_count(be, direct::Collection::Functions, "afl~?", 1000);
})
.cache_builder([be = s.backend_ptr()](std::vector<FuncRow>& rows) {
build_funcs(be, rows, /*with_prototype=*/true);
build_funcs(be, rows, /*with_prototype=*/true, /*with_cost=*/true);
})
.projection_cache_builder([be = s.backend_ptr()](std::vector<FuncRow>& rows,
uint64_t colUsed) {
const bool with_prototype =
(colUsed & kColUsedAll) || (colUsed & kPrototypeColBit);
build_funcs(be, rows, with_prototype);
const bool with_cost =
(colUsed & kColUsedAll) || (colUsed & kCostColBit);
build_funcs(be, rows, with_prototype, with_cost);
})
// Identity is the entry address, not the cache position. That makes a
// multi-row UPDATE/DELETE resolve each row by its own address (through the
// key-based snapshot below) and lets `WHERE addr = X` be a real lookup.
.rowid([](const FuncRow& r) -> int64_t { return r.addr; })
.snapshot_mutations()
.row_lookup([be = s.backend_ptr()](FuncRow& r, int64_t addr) -> bool {
return func_at(be, addr, r);
})
.column_int64("addr", [](const FuncRow& r) { return r.addr; })
// Writable: UPDATE funcs SET name='x' WHERE addr=... renames the function
@@ -218,9 +303,9 @@ void register_funcs_table(Session& s) {
[](const FuncRow& r) { return r.name; },
[be = s.backend_ptr()](FuncRow& r, const char* v) -> bool {
if (!v || !*v) return false;
if (!json_utils::valid_flag_name(v)) {
if (!r_name_check(v)) {
xsql::set_vtab_error(
"funcs: name must be [A-Za-z0-9._$] (no spaces or r2 metacharacters)");
"funcs: name must start with a letter, '_' or ':' and use only [A-Za-z0-9._:]");
return false;
}
std::string out = dispatch_write(be, r.addr, std::string("afn ") + v);
@@ -337,6 +422,14 @@ void register_funcs_table(Session& s) {
.column_int64("is_noreturn", [](const FuncRow& r) -> int64_t { return r.is_noreturn ? 1 : 0; })
.column_int64("is_pure", [](const FuncRow& r) -> int64_t { return r.is_pure ? 1 : 0; })
.column_int64("is_recursive", [](const FuncRow& r) -> int64_t { return r.is_recursive ? 1 : 0; })
// `WHERE addr = X` -- a JOIN probe or a correlated subquery (`NOT EXISTS
// (SELECT 1 FROM funcs f WHERE f.addr = x.from_func)`) -- is one function
// lookup, not a scan. A correlated subquery opens a fresh cursor per
// evaluation, so no cursor-lived cache can help it; this is what does.
.filter_eq("addr",
[be = s.backend_ptr()](int64_t addr) -> std::unique_ptr<xsql::RowIterator> {
return std::make_unique<FuncAtIterator>(be, addr);
}, 1.0, 1.0)
.deletable([be = s.backend_ptr()](FuncRow& r) -> bool {
// Remove the function definition at this address (analysis only).
// `af-` takes the address as a plain trailing argument, not via `@`
@@ -362,7 +455,8 @@ void register_funcs_table(Session& s) {
xsql::set_vtab_error("funcs: INSERT requires addr");
return false;
}
const int64_t addr = argv[0].as_int64();
int64_t addr = 0;
if (!table_utils::exact_write_int64(argv[0], "funcs: addr", addr)) return false;
const std::string at = json_utils::hex_addr(addr);
if (std::string e = table_utils::command_error_text(dispatch_write(be, addr, "af")); !e.empty()) {
xsql::set_vtab_error("funcs: INSERT failed to define a function at " + at + ": " + e);
@@ -371,7 +465,7 @@ void register_funcs_table(Session& s) {
// Optional best-effort naming; a failed afn does not undo the af above.
if (argc > 1 && !argv[1].is_null()) {
const char* name = argv[1].as_c_str();
if (name && *name && json_utils::valid_flag_name(name)) {
if (name && *name && r_name_check(name)) {
(void)dispatch_write(be, addr, std::string("afn ") + name);
}
}
+1 -1
View File
@@ -69,7 +69,7 @@ void build_grep_rows(Backend* be, std::vector<GrepRow>& rows) {
parent, addr});
};
std::vector<direct::FuncRow> funcs;
api->funcs_scan(false, funcs);
api->funcs_scan(false, false, funcs);
for (const auto& r : funcs) add("func", r.addr, r.name);
std::vector<direct::FlagRow> flags;
api->flags_scan(flags, nullptr, true);
+7 -2
View File
@@ -373,8 +373,13 @@ void register_instructions_table(Session& s) {
.parametric_filter_projection({"start_addr", "count"},
[be = s.backend_ptr()](const std::vector<xsql::FunctionArg>& args, uint64_t col_used)
-> std::unique_ptr<xsql::Generator<InsnRow>> {
const int64_t start_addr = args[0].as_int64();
const int64_t count = args[1].as_int64();
// omit=1: exact integers only (xsql::exact_int64); 4096.5 or NULL is
// no rows, never a walk from a truncated address.
const auto start_opt = xsql::exact_int64(args[0]);
const auto count_opt = xsql::exact_int64(args[1]);
if (!start_opt || !count_opt) return nullptr;
const int64_t start_addr = *start_opt;
const int64_t count = *count_opt;
if (!needs_render(col_used)) {
if (auto* api = be->direct()) {
return std::make_unique<InsnsVecGen>(insns_range_api(api, start_addr, count));
+13 -5
View File
@@ -15,6 +15,7 @@
#include <r2xsql/backend.hpp>
#include <xsql/vtable.hpp>
#include <r_util.h>
#include "tables.hpp"
#include "json_utils.hpp"
#include "table_utils.hpp"
@@ -85,8 +86,11 @@ void register_io_maps_table(Session& s) {
xsql::set_vtab_error("io_maps: INSERT requires start_addr and end_addr");
return false;
}
const int64_t start = argv[0].as_int64();
const int64_t end = argv[1].as_int64();
int64_t start = 0, end = 0;
if (!table_utils::exact_write_int64(argv[0], "io_maps: start_addr", start) ||
!table_utils::exact_write_int64(argv[1], "io_maps: end_addr", end)) {
return false;
}
if (end <= start) {
xsql::set_vtab_error("io_maps: end_addr must be > start_addr");
return false;
@@ -99,12 +103,16 @@ void register_io_maps_table(Session& s) {
std::string name;
if (argc > 3 && !argv[3].is_null()) {
name = argv[3].as_c_str();
if (!name.empty() && !json_utils::valid_flag_name(name.c_str())) {
xsql::set_vtab_error("io_maps: name must be [A-Za-z0-9._$] (no spaces)");
if (!name.empty() && !r_name_check(name.c_str())) {
xsql::set_vtab_error("io_maps: name must start with a letter, '_' or ':' and use only [A-Za-z0-9._:]");
return false;
}
}
const int64_t paddr = (argc > 4 && !argv[4].is_null()) ? argv[4].as_int64() : 0;
int64_t paddr = 0;
if (argc > 4 && !argv[4].is_null() &&
!table_utils::exact_write_int64(argv[4], "io_maps: paddr", paddr)) {
return false;
}
std::string out = be->cmd(make_om_command(start, end, paddr, perm, name));
if (std::string e = table_utils::command_error_text(out); !e.empty()) {
xsql::set_vtab_error("io_maps: INSERT failed (check a file is open and "
+9 -8
View File
@@ -13,7 +13,7 @@
//
// A full scan enumerates functions (`aflj`) and pulls each one's vars
// (`afvj @ <addr>`). On the libr backend this reads `fcn->vars` directly
// instead — measured in a private lab at 50-780x on a full scan across a real
// instead — measured at 50-780x on a full scan across a real
// binary and generated fixtures, with exact field parity, before being ported
// (see `direct.hpp` for why the second producer lives in another TU). The
// command path remains the only implementation r2pipe can have, and it also
@@ -26,13 +26,13 @@
//
// The WRITE side (this table's `name` rename and `type` retype) is ALSO now
// C-API-backed on the libr flavor, a separate, later port from the read-side
// scan above: a private standalone lab measured a bulk workload (N
// scan above: a benchmark measured a bulk workload (N
// repeated renames/retypes, simulating a bulk-retype script) at 78-150x over
// the CMD leg. Reading `cmd_anal.inc.c`'s `afvn`/`afvt` handlers found each is
// nothing more than `r_anal_function_get_var_byname` + `r_anal_var_rename`/
// `r_anal_var_set_type` — the SAME calls the API leg makes directly, with no
// other side effect (unlike `funcs.name`'s `afn`, refused for exactly this
// reason on that table) — so parity is exact, proven live in the lab. The
// reason on that table) — so parity is exact, proven live. The
// single-function readback (`local_readback_matches`, below) stays
// command-based on BOTH legs, same as `funcs`'/`flags`' own per-write
// readbacks — this port only moves the WRITE call itself. On every non-libr
@@ -49,7 +49,7 @@
// `dispatch_write(be, addr, command)` helper every sibling write-capable
// table (`comments`, `funcs`, `flags`, `bookmarks`, `types`, `patches`)
// already uses. This is an ADDITIONAL layer alongside this table's existing
// grammar validation (`valid_flag_name`/`valid_type_text`), never a
// grammar validation (`r_name_check`/`valid_type_text`), never a
// replacement for it. On the r2pipe backend, `cmd_noeval` reports unsupported
// and the exact `afvn`/`afvt` command text this table always issued runs
// unchanged.
@@ -64,6 +64,7 @@
#include <r2xsql/backend.hpp>
#include <xsql/vtable.hpp>
#include <r_util.h>
#include "tables.hpp"
#include "direct.hpp"
#include "json_utils.hpp"
@@ -191,12 +192,12 @@ void register_locals_table(Session& s) {
[](const LocalRow& r) { return r.name; },
[be = s.backend_ptr()](LocalRow& r, const char* v) -> bool {
if (!v || !*v) return false;
if (!json_utils::valid_flag_name(v)) {
if (!r_name_check(v)) {
xsql::set_vtab_error(
"locals: name must be [A-Za-z0-9._$] (no spaces or r2 metacharacters)");
"locals: name must start with a letter, '_' or ':' and use only [A-Za-z0-9._:]");
return false;
}
if (!json_utils::valid_flag_name(r.name)) {
if (!r_name_check(r.name.c_str())) {
xsql::set_vtab_error(
"locals: existing name contains r2 command metacharacters");
return false;
@@ -245,7 +246,7 @@ void register_locals_table(Session& s) {
"locals: type contains an r2 metacharacter (@ ; | ` \" $ etc.)");
return false;
}
if (!json_utils::valid_flag_name(r.name)) {
if (!r_name_check(r.name.c_str())) {
xsql::set_vtab_error(
"locals: existing name contains r2 command metacharacters");
return false;
+2 -1
View File
@@ -294,7 +294,8 @@ void register_patches_table(Session& s) {
return false;
}
const int64_t addr = argv[0].as_int64();
int64_t addr = 0;
if (!table_utils::exact_write_int64(argv[0], "patches: addr", addr)) return false;
const bool have_hex = argc > 3 && !argv[3].is_null();
const bool have_asm = argc > 5 && !argv[5].is_null();
if (have_hex == have_asm) {
+2 -1
View File
@@ -11,6 +11,7 @@
#include <r2xsql/backend.hpp>
#include <xsql/vtable.hpp>
#include <r_util.h>
#include "tables.hpp"
#include "direct.hpp"
#include "json_utils.hpp"
@@ -42,7 +43,7 @@ void register_projects_table(Session& s) {
})
.column_text("name", [](const ProjectRow& r) { return r.name; })
.deletable([be = s.backend_ptr()](ProjectRow& r) -> bool {
if (r.name.empty() || !json_utils::valid_flag_name(r.name)) {
if (r.name.empty() || !r_name_check(r.name.c_str())) {
xsql::set_vtab_error("projects: refusing to delete an unsafe/empty project name");
return false;
}
+16 -2
View File
@@ -164,11 +164,25 @@ void register_read_cstr_table(Session& s) {
bool have_addr = false;
int64_t addr = 0;
int64_t max_len = kDefaultMaxLen;
// omit=1: SQLite does not re-check these values, so each is compared the
// way a native column would (xsql::exact_*): a value nothing can equal
// (10.5 for an integer, NULL, a blob) yields no rows.
for (const auto& a : args) {
if (a.op != xsql::ConstraintOp::Eq) continue;
switch (a.column_index) {
case kAddr: addr = a.value.as_int64(); have_addr = true; break;
case kMaxLen: max_len = a.value.as_int64(); break;
case kAddr: {
const auto v = xsql::exact_int64(a.value);
if (!v) return nullptr;
addr = *v;
have_addr = true;
break;
}
case kMaxLen: {
const auto v = xsql::exact_int64(a.value);
if (!v) return nullptr;
max_len = *v;
break;
}
default: break;
}
}
+34 -16
View File
@@ -401,26 +401,35 @@ void register_rop_gadgets_table(Session& s) {
const std::vector<xsql::GeneratorConstraintArg>& args)
-> std::unique_ptr<xsql::Generator<GadgetRow>> {
SearchParams p;
// omit=1: SQLite does not re-check these values, so each is compared the
// way a native column would (xsql::exact_*): a value nothing can equal
// (10.5 for an integer, NULL, a blob) yields no rows.
for (const auto& a : args) {
switch (a.column_index) {
case kGPattern: {
const char* v = a.value.as_c_str();
if (v) p.pattern = v;
auto v = xsql::exact_text(a.value);
if (!v) return nullptr;
p.pattern = std::move(*v);
break;
}
case kGTerminator: {
const char* v = a.value.as_c_str();
if (v && *v) p.terminator = v;
auto v = xsql::exact_text(a.value);
if (!v) return nullptr;
if (!v->empty()) p.terminator = std::move(*v);
break;
}
case kGKlass: {
const char* v = a.value.as_c_str();
if (v) p.klass = v;
auto v = xsql::exact_text(a.value);
if (!v) return nullptr;
p.klass = std::move(*v);
break;
}
case kGMaxResults:
p.max_results = a.value.as_int64();
case kGMaxResults: {
const auto v = xsql::exact_int64(a.value);
if (!v) return nullptr;
p.max_results = *v;
break;
}
default:
break;
}
@@ -455,26 +464,35 @@ void register_rop_gadget_instructions_table(Session& s) {
const std::vector<xsql::GeneratorConstraintArg>& args)
-> std::unique_ptr<xsql::Generator<GadgetInstrRow>> {
SearchParams p;
// omit=1: SQLite does not re-check these values, so each is compared the
// way a native column would (xsql::exact_*): a value nothing can equal
// (10.5 for an integer, NULL, a blob) yields no rows.
for (const auto& a : args) {
switch (a.column_index) {
case kGiPattern: {
const char* v = a.value.as_c_str();
if (v) p.pattern = v;
auto v = xsql::exact_text(a.value);
if (!v) return nullptr;
p.pattern = std::move(*v);
break;
}
case kGiTerminator: {
const char* v = a.value.as_c_str();
if (v && *v) p.terminator = v;
auto v = xsql::exact_text(a.value);
if (!v) return nullptr;
if (!v->empty()) p.terminator = std::move(*v);
break;
}
case kGiKlass: {
const char* v = a.value.as_c_str();
if (v) p.klass = v;
auto v = xsql::exact_text(a.value);
if (!v) return nullptr;
p.klass = std::move(*v);
break;
}
case kGiMaxResults:
p.max_results = a.value.as_int64();
case kGiMaxResults: {
const auto v = xsql::exact_int64(a.value);
if (!v) return nullptr;
p.max_results = *v;
break;
}
default:
break;
}
+3 -2
View File
@@ -55,6 +55,7 @@
#include <r2xsql/backend.hpp>
#include <xsql/vtable.hpp>
#include <r_util.h>
#include "tables.hpp"
#include "direct.hpp"
#include "json_utils.hpp"
@@ -148,7 +149,7 @@ void register_types_table(Session& s) {
// SQL creates empty struct/union/enum shells and deletes types by name.
// Full declarations use radare2's native td command.
.deletable([be = s.backend_ptr()](TypeRow& r) -> bool {
if (r.name.empty() || !json_utils::valid_flag_name(r.name)) {
if (r.name.empty() || !r_name_check(r.name.c_str())) {
xsql::set_vtab_error("types: refusing to delete an unsafe/empty type name");
return false;
}
@@ -177,7 +178,7 @@ void register_types_table(Session& s) {
const char* kind_c = argv[2].as_c_str();
const std::string name = name_c ? name_c : "";
const std::string kind = kind_c ? kind_c : "";
if (name.empty() || !json_utils::valid_flag_name(name)) {
if (name.empty() || !r_name_check(name.c_str())) {
xsql::set_vtab_error("types: refusing to create an unsafe/empty type name");
return false;
}
+3
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
+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')