mirror of
https://github.com/radareorg/r2xsql.git
synced 2026-11-07 22:42:50 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5417ccc164 | ||
|
|
9df012ee10 | ||
|
|
104599f12a | ||
|
|
c4b6114464 | ||
|
|
248bcdc83f | ||
|
|
16da52e29f | ||
|
|
70184621db | ||
|
|
46f00dd9a1 | ||
|
|
69e959d898 | ||
|
|
7eaaedc12c | ||
|
|
5836f83068 | ||
|
|
2d32ad72dc | ||
|
|
25776ca418 | ||
|
|
3f9ae4cfa0 | ||
|
|
d92f9d962d | ||
|
|
2618735bd4 | ||
|
|
c46ccb42b6 | ||
|
|
956d60a11a | ||
|
|
58cd6ebde4 | ||
|
|
eca186fa6e | ||
|
|
96f2f8a3c7 | ||
|
|
64b5e218e3 | ||
|
|
b5316d5e9d |
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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()
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -3,276 +3,97 @@
|
||||
[](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
@@ -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)
|
||||
@@ -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}")
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
+49
-108
@@ -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,18 +1204,10 @@ 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 `r_bin_demangle`.
|
||||
@@ -1342,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
|
||||
@@ -1515,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;
|
||||
@@ -1537,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.
|
||||
@@ -1636,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
|
||||
@@ -1740,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 |
|
||||
|------------|-------|--------------------------------|
|
||||
@@ -1765,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 |
|
||||
|--------|-------|--------------------------------|
|
||||
@@ -1790,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
|
||||
@@ -1821,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
|
||||
|
||||
@@ -1857,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) | — |
|
||||
@@ -1910,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
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
+27
-52
@@ -6,28 +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.
|
||||
// The demangle method also uses this boundary so arbitrary symbol text reaches
|
||||
// RBin directly, without passing through the r2 command parser.
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -139,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;
|
||||
@@ -185,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.
|
||||
@@ -215,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;
|
||||
@@ -284,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;
|
||||
@@ -304,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.
|
||||
@@ -326,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
|
||||
@@ -342,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:
|
||||
//
|
||||
@@ -376,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
|
||||
@@ -384,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 {
|
||||
@@ -515,9 +482,16 @@ class Api {
|
||||
// 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;
|
||||
@@ -564,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
|
||||
@@ -617,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.
|
||||
@@ -659,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
|
||||
@@ -719,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
|
||||
+165
-92
@@ -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) {
|
||||
@@ -144,6 +140,21 @@ class LibrApi : public Api {
|
||||
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 &&
|
||||
@@ -457,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.
|
||||
@@ -465,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);
|
||||
@@ -567,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;
|
||||
@@ -621,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>`.
|
||||
@@ -744,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) {
|
||||
@@ -837,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));
|
||||
@@ -853,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));
|
||||
@@ -862,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];
|
||||
@@ -1006,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(),
|
||||
@@ -1057,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 {
|
||||
|
||||
@@ -56,18 +56,6 @@ bool boolean(const nlohmann::json& obj, std::string_view key, bool dflt) {
|
||||
return 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;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool valid_type_text(std::string_view type) {
|
||||
if (type.empty()) return false;
|
||||
for (unsigned char c : type) {
|
||||
@@ -233,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
|
||||
@@ -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.
|
||||
|
||||
|
||||
// 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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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));
|
||||
})
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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=" +
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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,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>
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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; })
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
@@ -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)"
|
||||
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -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()))
|
||||
Reference in new issue
Block a user