20 changed files with 816 additions and 347 deletions

No files matched your search

+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
# Test the public release through r2pm using radare2's official distribution.
set -euo pipefail
mkdir -p build-r2-runtime
base="https://github.com/radareorg/radare2/releases/download/${R2_REF}"
case "$RUNNER_OS" in
Linux)
curl -fsSL -o build-r2-runtime/radare2.deb "$base/radare2_${R2_REF}_amd64.deb"
sudo apt-get update -qq
sudo apt-get install -y ./build-r2-runtime/radare2.deb
;;
Windows)
curl -fsSL -o build-r2-runtime/radare2.zip "$base/radare2-${R2_REF}-w64.zip"
unzip -q build-r2-runtime/radare2.zip -d build-r2-runtime
r2=$(find "$PWD/build-r2-runtime" -name radare2.exe -type f -print -quit)
[[ -n "$r2" ]]
export PATH="$(dirname "$r2"):$PATH"
;;
*) echo "Unsupported runner: $RUNNER_OS" >&2; exit 1 ;;
esac
r2pm -U
r2pm -bi r2xsql
python3 tests/smoke.py "$(command -v radare2)"
r2pm -u r2xsql
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# Exercise the source installation paths used outside the release archives.
set -euo pipefail
export PATH="$PWD/r2prefix/bin:$PATH"
export LD_LIBRARY_PATH="$PWD/r2prefix/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
export DYLD_LIBRARY_PATH="$PWD/r2prefix/lib${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}"
export PKG_CONFIG_PATH="$PWD/r2prefix/lib/pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}"
if [[ "$RUNNER_OS" == Windows ]]; then
cmake --build build --config Release --target user-install
else
make -j2 user-install LIBXSQL_DIR="$PWD/build/_deps/libxsql-src"
fi
python3 tests/smoke.py ./r2prefix/bin/radare2
if [[ "$RUNNER_OS" == Windows ]]; then
cmake --build build --config Release --target user-uninstall
else
make user-uninstall
fi
+16 -9
View File
@@ -2,7 +2,7 @@
# Stage the same plugins/ layout on every platform for r2pm -bi.
set -euo pipefail
# The pinned radare2 predates r2pm -H R2PM_TRIPLET.
# 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 ;;
@@ -11,17 +11,24 @@ case "${RUNNER_OS}-${RUNNER_ARCH}" in
*) echo "Unsupported runner: ${RUNNER_OS}-${RUNNER_ARCH}" >&2; exit 1 ;;
esac
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/plugins
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
# Check automatic discovery of the packaged plugin, including its suffix.
result=$(R2_LIBR_PLUGINS="$PWD/dist/plugin/plugins" \
LD_LIBRARY_PATH="$PWD/r2prefix/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" \
DYLD_LIBRARY_PATH="$PWD/r2prefix/lib${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}" \
./r2prefix/bin/radare2 -Nq -c 'sqlj SELECT 1 AS ok' -)
[[ "$result" == *'"rows":[["1"]]'* ]] || { echo "$result" >&2; exit 1; }
# 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
@@ -48,7 +55,7 @@ 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.
+36 -2
View File
@@ -18,7 +18,7 @@ concurrency:
# Build against this radare2 release without changing its version for r2pm.
env:
R2_REF: "6.2.2"
R2_REF: "6.2.6"
jobs:
# ============================================================================
@@ -26,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
@@ -63,6 +64,10 @@ 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
@@ -127,6 +132,10 @@ 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
@@ -171,6 +180,16 @@ 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
@@ -214,3 +233,18 @@ jobs:
files: |
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
+26 -4
View File
@@ -40,6 +40,26 @@ 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)
@@ -84,7 +104,7 @@ all: $(PLUGIN)
# Removing files must not fetch or rebuild dependencies via included makefiles.
ifneq ($(filter-out clean mrproper uninstall user-uninstall,$(or $(MAKECMDGOALS),all)),)
-include $(BUILD)/libxsql.mk
include $(BUILD)/libxsql.mk
-include $(OBJS:.o=.d)
endif
@@ -95,9 +115,10 @@ $(LIBXSQL_DIR)/CMakeLists.txt:
@echo "libxsql not found at ../libxsql -- cloning $(LIBXSQL_URL)"
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
@@ -123,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)"
+62 -275
View File
@@ -3,310 +3,97 @@
[![ci status](https://github.com/radareorg/r2xsql/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/radareorg/r2xsql/actions/workflows/build.yml?query=branch%3Amain)
SQL interface for [radare2](https://github.com/radareorg/radare2), powered
by [`libxsql`](https://github.com/0xeb/libxsql).
```sql
SELECT name, size FROM funcs WHERE name LIKE 'main%';
SELECT content FROM strings WHERE content LIKE '%password%';
UPDATE comments SET text = 'reviewed' WHERE addr = 0x1234;
```
`r2xsql` exposes radare2's analysis state as live SQL virtual tables: every
query reads native analysis data or the JSON form of the relevant r2 command
(`aflj`, `izj`, `axtj`, …) from an in-process core or a spawned `radare2 -q0`
pipe. Most live tables cache rows only for the duration of a query.
## It's not read-only — and your work saves
Load the plugin in radare2 and mutate analysis through ordinary SQL writes.
Use radare2's `-w` option when writing binary bytes:
```sql
UPDATE funcs SET name = 'parse_header' WHERE addr = 0x401000; -- rename
UPDATE funcs SET prototype = 'int parse_header(char *buf)' WHERE name = 'parse_header';
INSERT INTO comments(addr, text) VALUES (0x401044, 'bounds check missing');
UPDATE locals SET name = 'len', type = 'int' WHERE func_addr = 0x401000 AND name = 'var_8h';
INSERT INTO types(name, kind) VALUES ('Header', 'struct');
INSERT INTO bookmarks(addr, name) VALUES (0x401000, 'entry-of-interest');
```
Writable surfaces: **funcs** (rename, prototype), **comments**, **flags**,
**locals** (rename + retype), **bookmarks**, **types** (create struct/union/
enum), **patches** (byte edits), **io_maps**, **bytes**.
**Persist it.** Save analysis with `Ps NAME` in radare2 and reopen it with
`P NAME`. The plugin uses radare2's native project format; it does not
automatically save on exit.
Project save/open and full type declarations use native radare2 commands:
`Ps NAME`, `P NAME`, and `"td struct Header { int magic; };"`. The former SQL
functions `r2xsql_project_save`, `r2xsql_project_open`, and
`r2xsql_type_define` are removed. SQL can still list/delete projects and
create empty struct/union/enum types through the corresponding tables.
## Requirements
**You need radare2.** r2xsql does not bundle it and does not install it.
```bash
# any one of these
r2pm -ci radare2 # radare2's own package manager
git clone https://github.com/radareorg/radare2 && radare2/sys/install.sh
sudo apt install radare2 # distro package (may lag behind releases)
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
The recommended way to install the plugin is with r2pm:
Install radare2 6.2.6, then install the matching release plugin with r2pm:
```sh
r2pm -U
r2pm -ci r2xsql
```
The installed plugin loads automatically when radare2 starts:
```sh
r2pm -bi r2xsql
r2 -A ./target
```
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 \
-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).
For a local build, `make user-install` also installs the plugin for automatic
loading. To load a build without installing it, use either `-i` or `L`:
## Fully headless
With an installed plugin, start a fresh binary in a background native process:
```sh
r2 -i /absolute/path/core_r2xsql.so -A ./target
r2 -A -c 'L /absolute/path/core_r2xsql.so' ./target
```
Use the actual `.so`, `.dylib`, or `.dll` path. Inside an existing radare2
session, use `L /absolute/path/core_r2xsql.so`.
## 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
```
`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. 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 # load it in r2 and run one SQL query
# 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`).
Release assets are named `r2xsql-<radare2-version>-<R2PM_TRIPLET>.zip`, for
example `r2xsql-6.2.2-linux-x86-64.zip`. Each contains the plugin in `plugins/`.
With a recent r2pm supporting binary installs, use `r2pm -U` followed by
`r2pm -bi r2xsql`; uninstall with `r2pm -u r2xsql`. Only matching radare2
versions and targets are installed. To install manually, copy the plugin to
the directory printed by `radare2 -NN -H R2_USER_PLUGINS`.
### JSON results
Use `sqlj` for programmatic queries and scripts:
```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
With the plugin installed, start r2's web server in a persistent session:
```sh
r2 -N -q \
-e http.bind=127.0.0.1 -e http.port=9090 -c aa -c '=h' ./target
```
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 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/`.
## 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
+22 -11
View File
@@ -22,7 +22,7 @@
# Radare2::libr (INTERFACE bundle of the lot)
#
# Plus:
# Radare2_EXECUTABLE — path to radare2(.exe) (for the r2pipe backend)
# Radare2_EXECUTABLE — path to radare2(.exe)
set(_r2_search_roots "")
@@ -66,18 +66,28 @@ endif()
if(NOT Radare2_FOUND)
find_package(PkgConfig QUIET)
if(PkgConfig_FOUND)
pkg_check_modules(_R2 QUIET r_core)
pkg_check_modules(_R2 QUIET IMPORTED_TARGET r_core)
if(_R2_FOUND)
set(Radare2_FOUND TRUE)
pkg_get_variable(Radare2_ROOT r_core prefix)
set(Radare2_VERSION "${_R2_VERSION}")
set(Radare2_INCLUDE_DIRS "${_R2_INCLUDE_DIRS}")
set(Radare2_LIBRARY_DIRS "${_R2_LIBRARY_DIRS}")
set(Radare2_LIBRARIES "${_R2_LIBRARIES}")
set(Radare2_LIBRARIES PkgConfig::_R2)
# r_core.pc includes its public dependencies, including r_util. Keep
# pkg-config's link flags and paths instead of guessing a libdir.
foreach(_target libr r_util)
if(NOT TARGET Radare2::${_target})
add_library(Radare2::${_target} INTERFACE IMPORTED)
set_target_properties(Radare2::${_target} PROPERTIES
INTERFACE_LINK_LIBRARIES PkgConfig::_R2)
endif()
endforeach()
endif()
endif()
endif()
if(Radare2_FOUND AND Radare2_ROOT)
if(_r2_resolved_root)
# Normalize separators: on Windows, Git-Bash passes -DRadare2_ROOT as
# "D:\a\...\r2prefix" (backslashes), and file(GLOB) treats backslashes as escapes
# rather than separators, so the libdir glob below matches nothing and we end up
@@ -118,8 +128,8 @@ if(Radare2_FOUND AND Radare2_ROOT)
endif()
set(_r2_libs r_core r_anal r_bin r_util r_io r_cons r_main r_socket
r_reg r_arch r_search r_syscall r_config r_lang r_egg r_debug
r_bp r_crypto r_flag r_fs r_magic r_parse)
r_reg r_arch r_asm r_esil r_search r_syscall r_config r_lang r_egg r_debug
r_bp r_crypto r_muta r_flag r_fs r_magic r_parse)
set(Radare2_LIBRARIES "")
foreach(_lib IN LISTS _r2_libs)
set(_full "")
@@ -148,10 +158,8 @@ if(Radare2_FOUND AND Radare2_ROOT)
# r_core_* symbols. Surface it at configure time instead.
list(LENGTH Radare2_LIBRARIES _r2_nlibs)
message(STATUS "FindRadare2: libdir='${_r2_libdir}', resolved ${_r2_nlibs} libraries")
if(_r2_nlibs EQUAL 0)
message(WARNING "FindRadare2: found radare2 at '${Radare2_ROOT}' but located ZERO "
"import libraries under '${_r2_libdir}'. Full/plugin links will fail "
"with undefined r_core_* symbols.")
if(NOT TARGET Radare2::r_core OR NOT TARGET Radare2::r_util)
set(Radare2_LIBRARIES "")
endif()
if(NOT TARGET Radare2::libr)
@@ -161,6 +169,9 @@ if(Radare2_FOUND AND Radare2_ROOT)
INTERFACE_LINK_LIBRARIES "${Radare2_LIBRARIES}")
endif()
endif()
if(Radare2_FOUND)
find_program(Radare2_EXECUTABLE
NAMES radare2 r2
PATHS "${Radare2_ROOT}/bin"
@@ -172,7 +183,7 @@ endif()
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(Radare2
REQUIRED_VARS Radare2_ROOT Radare2_INCLUDE_DIRS
REQUIRED_VARS Radare2_INCLUDE_DIRS Radare2_LIBRARIES
VERSION_VAR Radare2_VERSION)
mark_as_advanced(Radare2_INCLUDE_DIRS Radare2_LIBRARIES Radare2_EXECUTABLE)
+20
View File
@@ -0,0 +1,20 @@
# SPDX-License-Identifier: LicenseRef-Human-Origin-Source-1.0
# Query at install time so cross builds can still configure without running r2.
if(NOT PLUGIN_DIR)
execute_process(COMMAND "${RADARE2}" -NN -H R2_USER_PLUGINS
RESULT_VARIABLE _status OUTPUT_VARIABLE PLUGIN_DIR
OUTPUT_STRIP_TRAILING_WHITESPACE)
if(NOT _status STREQUAL "0" OR NOT PLUGIN_DIR)
message(FATAL_ERROR "Cannot determine radare2 user plugin directory; set R2XSQL_USER_PLUGINS.")
endif()
endif()
file(TO_CMAKE_PATH "${PLUGIN_DIR}" PLUGIN_DIR)
get_filename_component(_name "${PLUGIN}" NAME)
if(ACTION STREQUAL "user-install")
file(INSTALL "${PLUGIN}" DESTINATION "${PLUGIN_DIR}" TYPE FILE)
elseif(ACTION STREQUAL "user-uninstall")
file(REMOVE "${PLUGIN_DIR}/${_name}")
else()
message(FATAL_ERROR "Unknown install action: ${ACTION}")
endif()
message(STATUS "${ACTION}: ${PLUGIN_DIR}/${_name}")
+150
View File
@@ -0,0 +1,150 @@
# Building and testing r2xsql
## Build
Linux / macOS:
```sh
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DRadare2_ROOT=/path/to/radare2/prefix
cmake --build build
```
Windows (MSVC):
```powershell
cmake -S . -B build -G "Visual Studio 17 2022" -A x64 `
-DRadare2_ROOT=C:\path\to\radare2\prefix
cmake --build build --config Release
cmake --build build --config Release --target user-install
```
`Radare2_ROOT` is a radare2 **install prefix** containing `include/libr/`
and `lib/`. CMake requires radare2 and builds the libr backend and plugin by
default; a missing radare2 installation is a configure error. The plugin is in
`build/bin/` (`build/bin/Release/` for the MSVC Release configuration).
CMake's `user-install` target installs the plugin in the directory reported by
the matching radare2's `-NN -H R2_USER_PLUGINS`, without administrator access.
`user-uninstall` removes it. Override the destination at configure time with
`-DR2XSQL_USER_PLUGINS=/path/to/plugins` if needed. Both targets also work on
Linux/macOS; omit `--config Release` with a single-configuration generator.
`R2XSQL_BUILD_PLUGIN=OFF` disables the plugin target and
`R2XSQL_BUILD_EXAMPLES=OFF` disables examples. C++ consumers can embed the
library with `open_libr` and the `r2xsql_libr` target. The project also supports
being added as a CMake subdirectory.
### Building the in-r2 plugin the radare2 way (Makefile / meson)
CMake above builds the library, examples, and plugin, including on Windows. If you
only want the in-r2 plugin — the `core_r2xsql` module that adds the `sql`
command inside radare2 — there is also a radare2-native build that needs no
CMake, matching how radare2 plugins are usually built (plugin paths from
`r2 -H`, radare2 from `pkg-config r_core`):
```bash
make # -> build-make/core_r2xsql.$(r2 -H R2_LIBEXT)
make user-install # install into the per-user plugin dir (no root)
make install # install system-wide (R2_LIBR_PLUGINS)
make user-uninstall # remove the per-user plugin without building
make uninstall # remove the system-wide plugin without building
make test # run the command and HTTP regression suite
# only `radare2` (not `r2`) on PATH? -> make R2=radare2
```
`make install` and `make uninstall` accept `PLUGDIR` and `DESTDIR` for a
custom plugin directory or staged install. User targets use `R2_USER_PLUGINS`;
override it on the make command line when needed.
Or with meson (what radare2 itself uses):
```bash
meson setup build-meson
meson compile -C build-meson
meson install -C build-meson # into R2_LIBR_PLUGINS
meson setup build-meson -Dr2_user_plugins=true # ... or the per-user dir
```
Both build the same plugin: the `sql` command. Both
consume [`libxsql`](https://github.com/0xeb/libxsql): the Makefile uses a
sibling `../libxsql` checkout (else clones it), and meson pulls it as a
subproject. All three builds pin the same libxsql release tag (`v1.0.21`).
When published, binary release assets are named `r2xsql-<radare2-version>-<R2PM_TRIPLET>.zip`, for
example `r2xsql-6.2.6-linux-x86-64.zip`. The first release, `v0.0.1`, targets
radare2 **6.2.6** on Linux x86-64, Windows x86-64 and macOS ARM64. The r2pm
package uses `R2PM_VERSION_MAP "6.2.6:v0.0.1"` to select this release.
Each archive contains the plugin in `plugins/` and dependency license notices.
With r2pm 6.2.6 or newer, use `r2pm -U` followed by
`r2pm -bi r2xsql`; uninstall with `r2pm -u r2xsql`. Only matching radare2
versions and targets are installed. To install manually, copy the plugin to
the directory printed by `radare2 -NN -H R2_USER_PLUGINS`.
Windows binaries use the dynamic MSVC runtime, matching radare2's MSVC build.
Use the matching radare2 distribution and its runtime dependencies; the plugin
archive does not bundle another copy of radare2's DLLs.
Tag the release as `v0.0.1` after the build workflow passes. Tag builds publish
the three ZIPs only after every platform succeeds. CI tests automatic plugin
discovery, JSON/CSV output, comment writes, error recovery, and normal/quick
process exit on all three platforms, plus the r2r suite on Linux and macOS.
CI also builds and checks `make user-install` on Linux/macOS and CMake's
`user-install` target on Windows before publishing.
After publication, CI installs the public binaries with `r2pm -bi r2xsql`
against the official Linux and Windows radare2 distributions and runs the
same smoke checks.
## Tests
Run the regression suite with radare2, `r2r`, a C compiler and Python 3 on
`PATH` (Python's standard library is used only for the HTTP transport test):
```sh
make test
```
This builds the plugin and runs the r2r database in `tests/db`. Each test loads
the exact build explicitly, ignoring installed user plugins, and checks its
output; SQL exceptions fail the suite even when radare2 exits successfully.
The fixture is compiled locally, with no downloaded test binaries or pytest.
To test an existing CMake/Meson build, or select matching radare2 tools:
```sh
make -C tests R2XSQL_PLUGIN=/absolute/path/core_r2xsql.so R2=/path/to/r2 R2R=/path/to/r2r
# Only command tests (no Python needed):
make -C tests TESTS=db/cmd
```
`R2R_ARGS` accepts r2r options (default `-L`); for example, `R2R_ARGS=-i` enables
interactive failure review. One small standard-library helper retains real
HTTP GET/POST coverage, including server startup and shutdown; r2r checks its
result alongside the command tests.
The Make build tracks header dependencies. This prevents stale adapters after
C++ interface changes, which could previously cause `std::bad_alloc` during
`make test`. After updating this Makefile, existing objects are rebuilt once
to establish dependency files. For older checkouts, `make clean && make test`
clears those stale objects.
## Backends (internal detail)
The plugin uses the host RCore; embedding and tests use these backends:
| Backend | Used by | Notes |
|---|---|---|
| `LibrBackend` | `core_r2xsql` and C++ consumers (`open_libr`) | Uses radare2 in-process; the plugin wraps its host core. |
| `MockBackend` | embedding/tests | Canned `{command → response}` map, records issued commands — no live radare2. |
The in-process backend reads analysis, flags, symbols, binary metadata, IO maps,
write caches, types, and runtime catalogs through native libr APIs. The command
fallbacks support mock responses where needed. Instruction/operand text
rendering and byte/ROP searches still use radare2's command handlers to preserve
their rendering, search configuration, and hit behavior.
The internal r2pipe backend was for the removed standalone CLI and servers.
It duplicated process management and command-based access without serving the
native plugin, so it has been removed. External r2pipe clients can still send
`sql` commands to radare2 with this plugin loaded; radare2 also provides HTTP
access through `/cmd/`.
+73
View File
@@ -0,0 +1,73 @@
# SQL access
## Queries
For direct console use or programmatic clients:
```sql
SELECT name, size FROM funcs WHERE name LIKE 'main%';
SELECT content FROM strings WHERE content LIKE '%password%';
UPDATE comments SET text = 'reviewed' WHERE addr = 0x1234;
```
Tables read native analysis data or the JSON form of the relevant r2 command
(`aflj`, `izj`, `axtj`, …) from the host's in-process core.
## It's not read-only — and your work saves
Load the plugin in radare2 and mutate analysis through ordinary SQL writes.
Use radare2's `-w` option when writing binary bytes:
```sql
UPDATE funcs SET name = 'parse_header' WHERE addr = 0x401000; -- rename
UPDATE funcs SET prototype = 'int parse_header(char *buf)' WHERE name = 'parse_header';
INSERT INTO comments(addr, text) VALUES (0x401044, 'bounds check missing');
UPDATE locals SET name = 'len', type = 'int' WHERE func_addr = 0x401000 AND name = 'var_8h';
INSERT INTO types(name, kind) VALUES ('Header', 'struct');
INSERT INTO bookmarks(addr, name) VALUES (0x401000, 'entry-of-interest');
```
Writable surfaces: **funcs** (rename, prototype), **comments**, **flags**,
**locals** (rename + retype), **bookmarks**, **types** (create struct/union/
enum), **patches** (byte edits), **io_maps**, **bytes**.
**Persist it.** Save analysis with `Ps NAME` in radare2 and reopen it with
`r2 -p NAME` without a separate binary or reanalysis. The plugin uses radare2's native project format; it does not
automatically save on exit.
Project save/open and full type declarations use native radare2 commands:
`Ps NAME`, `P NAME`, and `"td struct Header { int magic; };"`. The former SQL
functions `r2xsql_project_save`, `r2xsql_project_open`, and
`r2xsql_type_define` are removed. SQL can still list/delete projects and
create empty struct/union/enum types through the corresponding tables.
## Output formats
`sql` executes one statement using radare2's table renderer. It honours
`cfg.table.format`, `cfg.table.maxcol` and `scr.utf8`. Put a table query
immediately after `sql,`, before the SQL:
```text
sql,:csv SELECT name,size FROM funcs
sql,:tsv SELECT name,size FROM funcs
sql,?
```
Use SQL `WHERE`, `ORDER BY` and `LIMIT` for filtering. Table JSON (`sql,:json`)
omits NULL and empty-string cells; use `sqlj` for programmatic access.
`sql.tables` / `sql.views` discover the current schema; `sql.?` lists
subcommands. The `sql.` namespace does not carry SQL statements.
## JSON results
Use `sqlj` for programmatic queries and scripts:
```text
"sqlj SELECT NULL AS missing, '' AS empty; SELECT count(*) AS n FROM funcs"
```
It returns libxsql's JSON envelope with per-statement columns, rows, errors,
and timings. SQL NULL is JSON `null`, distinct from the empty string. Quoting
the entire r2 command also preserves statement separators and SQL operators.
Read [saving and reopening](usage.md#saving-and-reopening) for persistence.
The same command works through r2pipe and radare2's HTTP `/cmd/` endpoint.
+236
View File
@@ -0,0 +1,236 @@
# Using r2xsql with radare2 and an agent
The plugin supplies SQL over a live radare2 core. The skills teach an agent
how to choose tables, query and annotate that core, and manage its lifetime.
The user can ask in natural language in either tandem or headless mode.
## Installation
Install radare2, then install a matching r2xsql plugin:
```sh
r2pm -U
r2pm -bi r2xsql
```
Release `v0.0.1` targets radare2 6.2.6. For a source build on Linux/macOS,
use `r2pm -ci r2xsql` or `make user-install`. Windows source builds use
`cmake --build build --config Release --target user-install` after
[configuring CMake](development.md).
Installed plugins load automatically on normal startup. `make user-install`
does the same for a local build. Manual installation uses the directory
reported by `r2 -NN -H R2_USER_PLUGINS`. Restart a host after installation.
The plugin's radare2 ABI must match the engine loading it; iaito can use a
different engine from the standalone r2 on PATH. Binary installation with
`r2pm -bi r2xsql` requires an available asset matching the host and platform.
See [building and packaging](development.md).
For a build that is not installed:
```sh
r2 -i ./core_r2xsql.so -A ./target
```
Use the actual `.so`, `.dylib` or `.dll` file. `-i` also accepts native r2
command scripts. Inside an existing console, load a plugin with
`L ./core_r2xsql.so`; `L r2xsql` is not a package-name loader.
`-N` skips startup scripts while retaining plugins; `-NN` disables automatic
plugin loading too. For a deterministic local-build check, use `-NN -i FILE`.
Verify `r2xsql` in `Lcj`, then try `sql?` and `sqlj SELECT 1`.
Install the [r2xsql skills](https://github.com/radareorg/radare2-skills/tree/main/r2xsql#install)
in the coding agent's harness. Skills and the binary plugin are separate
installations. The harness selects the `r2xsql` entry skill and routes to
connection and domain instructions; explicit skill selection can be used
when the harness does not select it automatically.
## Tandem mode
Tandem shares an existing analysis in an interactive r2 CLI, its visual/panels
text interface, or [iaito](https://github.com/radareorg/iaito). For a fresh
binary run `r2 -A ./target`, or open it in iaito and let analysis finish.
For a saved project run `r2 -p PROJECT` without `-A`.
At the r2 prompt or iaito's **View > Console**, verify the plugin with `Lcj`,
then start background HTTP:
```text
e http.bind=127.0.0.1
e http.sandbox=false
e http.port=9090
=h&
```
Choose a free port. `http.port` selects it; `=h&` starts the background
server and leaves the console/UI available. It is an alias for `& =h`,
using r2's background-task facility. `=h` serves in the foreground instead.
Start HTTP at the prompt before entering the text interface.
Wait for `open http://127.0.0.1:9090/`, then tell the agent:
> Use r2xsql at http://127.0.0.1:9090 on the binary already open here.
> Show me the largest functions.
The agent checks the host, plugin and binary identity, authors SQL and reuses
the endpoint on later questions. It should not open another UI or rerun
analysis merely to attach. Renames and comments affect the human's analysis
too. Take turns with the agent when running analysis-changing native commands.
Use `=h-` at the interactive console to stop background HTTP while keeping
the human's host open.
For several hosts, select a distinct port per process and provide a mapping:
**application = http://127.0.0.1:9090, library = http://127.0.0.1:9091**.
The agent verifies each target and labels findings by source. SQL executes
inside one host; comparisons across hosts combine results in the client.
## Fully headless
With the plugin installed, an agent can start the native r2 process without
a human console or GUI. For a fresh binary, POSIX shell:
```sh
r2 -N -q -A -e scr.html=false -e http.bind=127.0.0.1 \
-e http.sandbox=false -e http.port=9090 -c '=h' ./target \
>r2.stdout.log 2>r2.stderr.log &
r2_pid=$!
```
Windows PowerShell:
```powershell
$engine = Get-Command r2,radare2 -CommandType Application -ErrorAction SilentlyContinue |
Select-Object -First 1
if (-not $engine) { throw 'Install radare2 or supply its executable location.' }
$target = (Resolve-Path -LiteralPath '.\target.exe').Path
$port = 9090 # choose a free port
$logDir = Join-Path $env:TEMP ('r2-session-' + [guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Path $logDir | Out-Null
$arguments = '-N -q -A -e scr.html=false -e http.bind=127.0.0.1 -e http.sandbox=false -e http.port={0} -c "=h" "{1}"' -f $port,$target
$process = Start-Process -FilePath $engine.Source -ArgumentList $arguments -WindowStyle Hidden `
-RedirectStandardOutput (Join-Path $logDir 'stdout.log') `
-RedirectStandardError (Join-Path $logDir 'stderr.log') -PassThru
$endpoint = 'http://127.0.0.1:{0}/cmd/' -f $port
```
These launch radare2 directly. No Python session/query wrapper or build
orchestrator is needed. `-A` runs analysis once; foreground `=h` keeps the
background OS process alive. Queries use that same core until shutdown.
For an uninstalled plugin, substitute `-NN -i PLUGIN_FILE` for `-N`.
Wait for the server banner on stderr, then verify the endpoint with curl.
A bind failure is not readiness. Retain the PID, endpoint, binary/project
and log locations; subsequent prompts reuse this session.
## HTTP queries
For radare2 **6.2.4**, POST the complete r2 command to `/cmd/`:
```sh
curl -d '?V' http://127.0.0.1:9090/cmd/
curl -d 'Lcj' http://127.0.0.1:9090/cmd/
curl -d '"sqlj SELECT * FROM binary"' http://127.0.0.1:9090/cmd/
curl -d '"sqlj SELECT name,size FROM funcs WHERE size > 100 LIMIT 5"' \
http://127.0.0.1:9090/cmd/
```
In PowerShell use `curl.exe` to avoid the shell alias:
```powershell
$command = '"sqlj SELECT name,size FROM funcs ORDER BY size DESC LIMIT 10"'
curl.exe --silent --show-error --max-time 30 -d $command $endpoint
```
The body is an r2 command. The **double quotes are part of that body** and
protect SQL operators such as `>` and `|` from r2's parser. Use `sqlj`
for the JSON envelope; `sql,:json` is a table format with different NULL and
empty-string behavior. Inspect top-level and per-statement success/error
fields; HTTP 200 alone does not establish success. See [SQL access](sql.md).
Native commands such as `pdf` and `Ps` travel over the same endpoint.
The command-prefix-plus-body form is implemented in development radare2
and is planned for **6.2.6**:
```sh
curl -d 'SELECT name,size FROM funcs ORDER BY size DESC LIMIT 5' \
http://127.0.0.1:9090/cmd/sqlj
```
Here the URL selects `sqlj` and the body contains only SQL. This is a
version-dependent alternative, not a 6.2.4 example. Use the full-command
form until the host is known to support it. An agent should verify the JSON
response to a harmless query before choosing this form for mutations.
The combined command still passes through r2's parser. For SQL containing
operators such as `>` or `|`, keep the full double-quoted command form above.
## Saving and reopening
Analysis lives in memory until saved. Ask the agent to save the project beside
the binary, or select another location. The save operation is native radare2;
there is no SQL `save_database()` in r2xsql.
```sh
curl -d 'Ps /absolute/path/target.exe.r2' http://127.0.0.1:9090/cmd/
```
PowerShell, using the absolute target path from the launch:
```powershell
$project = ($target + '.r2').Replace('\','/')
curl.exe --silent --show-error --max-time 30 -d ('Ps ' + $project) $endpoint
Get-Item -LiteralPath $project
```
Use forward slashes in native r2 commands on Windows. Verify the actual saved
artifact, host log and requested annotations; a refused save can return an
empty HTTP 200. Keep the original binary and any companion `.r2.d` directory.
Saving analysis does not patch the executable or shut down the host.
Reopen the saved project with `-p`, without a separate target or reanalysis:
```sh
r2 -N -q -p /absolute/path/target.exe.r2 \
-e scr.html=false -e http.bind=127.0.0.1 \
-e http.sandbox=false -e http.port=9090 -c '=h' \
>r2.stdout.log 2>r2.stderr.log &
```
The project stores the binary path. On Windows replace `-A` and the target
in the launch arguments with `-p "PROJECT"`; keep the same `-e` options
and `-c "=h"`. Verify binary identity and saved annotations after reopening,
rather than checking only function counts. Preserve the checkpoint and logs
if verification fails.
### Projects and command scripts
Native projects support binary serialization and script compatibility; do not
infer their format from the filename alone. `Ps PROJECT` and `-p PROJECT`
are the normal save/reopen interface. A bare project name uses `dir.projects`;
an absolute filename selects a sidecar. `Pl` lists named projects, not every
sidecar elsewhere.
Where available, `prj?` documents the binary-container operations, including
`prj save FILE` and `prj open FILE`. Select the matching native loader for
an existing container; a standalone container is not interchangeable with a
named `-p` project on every host. Explicit command-script exports use `PS`
or `PS*` and can be interpreted with `-i SCRIPT`; they are not a reason to
describe all projects as scripts. Check `P?` and `prj?` on the chosen host.
## Shutdown and session ownership
On a save-and-shutdown request, save and verify first, then stop an agent-owned
foreground server:
```sh
curl 'http://127.0.0.1:9090/cmd/=h--'
```
`=h--` is intercepted in the GET path; putting it in a POST body does not
stop that accept loop. With the headless `-q` launch and no later commands,
r2 exits when serving ends. Verify the intended PID and listener stopped.
Do not close the user's tandem UI; stop its background server at the console
only when requested. Saving alone leaves the host available for follow-ups.
The server provides local command access, including writes when
`http.sandbox=false`. Bind to `127.0.0.1` and do not expose it as a public
service.
+8 -5
View File
@@ -112,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.
# 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()
+19 -3
View File
@@ -38,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) {
@@ -818,7 +830,11 @@ class LibrApi : public Api {
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);
@@ -898,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));
@@ -914,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));
@@ -1063,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(),
+4 -1
View File
@@ -140,7 +140,10 @@ void build_data_items(Backend* be, std::vector<DataItemRow>& rows) {
r.has_value = !r.value_repr.empty();
r.segment_name = j.section;
r.has_segment = !r.segment_name.empty();
if (!r.has_size) { r.size = j.size; r.has_size = true; }
// Mach-O symbols and ELF mapping symbols (e.g. ARM's $d) can have
// unknown/zero size. Preserve a known symbol size, otherwise use the
// string's byte size, including its terminator.
if (!r.has_size || r.size <= 0) { r.size = j.size; r.has_size = true; }
}
rows.reserve(by_addr.size());
+15
View File
@@ -51,3 +51,18 @@ xsql_add_versioninfo(core_r2xsql
FILETYPE VFT_DLL
DESCRIPTION "r2xsql in-r2 plugin (sql command)"
ORIGINAL_FILENAME "core_r2xsql.dll")
if(R2XSQL_STANDALONE)
set(R2XSQL_USER_PLUGINS "" CACHE PATH "Override the per-user radare2 plugin directory")
foreach(_action user-install user-uninstall)
add_custom_target(${_action}
COMMAND ${CMAKE_COMMAND}
"-DRADARE2=${Radare2_EXECUTABLE}"
"-DPLUGIN=$<TARGET_FILE:core_r2xsql>"
"-DPLUGIN_DIR=${R2XSQL_USER_PLUGINS}"
"-DACTION=${_action}"
-P "${PROJECT_SOURCE_DIR}/cmake/user-install.cmake"
VERBATIM)
endforeach()
add_dependencies(user-install core_r2xsql)
endif()
+37 -33
View File
@@ -17,8 +17,8 @@
// sql.views list available views
//
// The plugin wraps the host RCore* with libr2xsql's LibrBackend (no
// ownership) and shares a single Session across calls so SQLite caches
// stay warm across queries.
// ownership) and keeps one Session per core across calls so SQLite caches
// stay warm across queries. radare2 owns its lifetime through ctx->data.
#include <r2xsql/r2xsql.hpp>
#include <xsql/query_script.hpp>
@@ -30,6 +30,7 @@
#include <cstring>
#include <memory>
#include <mutex>
#include <new>
#include <string>
// r2xsql::make_libr_backend_from_core (declared in <r2xsql/backend.hpp>, included
@@ -39,18 +40,16 @@ namespace {
struct PluginState {
std::unique_ptr<r2xsql::Session> session;
RCore* bound_core = nullptr;
std::mutex mu;
};
PluginState g_state;
void ensure_session(RCore* core) {
std::lock_guard<std::mutex> lock(g_state.mu);
if (g_state.session && g_state.bound_core == core) return;
// Called with state.mu held. Do not keep a namespace-scope owning Session:
// quick exit skips plugin_fini, and static destruction can close SQLite after
// libxsql's cache registry has already been destroyed.
void ensure_session(PluginState& state, RCore* core) {
if (state.session) return;
auto backend = r2xsql::make_libr_backend_from_core(core);
g_state.session = std::make_unique<r2xsql::Session>(std::move(backend));
g_state.bound_core = core;
state.session = std::make_unique<r2xsql::Session>(std::move(backend));
}
static RCoreHelpMessage help_msg_sql = {
@@ -106,7 +105,7 @@ std::string render_table(RCore* core, const xsql::Result& res,
return out ? out.get() : "";
}
bool dispatch(RCore* core, const char* input) {
bool dispatch(PluginState& state, RCore* core, const char* input) {
if (!input) return false;
std::string table_query;
@@ -151,15 +150,12 @@ bool dispatch(RCore* core, const char* input) {
return true;
}
ensure_session(core);
std::string out;
{
// Serialize against concurrent callers (e.g. from other plugins or tools)
// which also touch g_state.session / SQLite under this same mutex.
std::lock_guard<std::mutex> lock(g_state.mu);
std::lock_guard<std::mutex> lock(state.mu);
try {
auto res = g_state.session->query(input);
ensure_session(state, core);
auto res = state.session->query(input);
out = render_table(core, res, table_query.c_str());
} catch (const std::exception& e) {
out = std::string("EXCEPTION: ") + e.what() + "\n";
@@ -171,7 +167,7 @@ bool dispatch(RCore* core, const char* input) {
return true;
}
bool dispatch_json(RCore* core, const char* input) {
bool dispatch_json(PluginState& state, RCore* core, const char* input) {
input = r_str_trim_head_ro(input);
if (!*input || std::strcmp(input, "?") == 0) {
print_help(core);
@@ -179,10 +175,10 @@ bool dispatch_json(RCore* core, const char* input) {
}
std::string out;
try {
ensure_session(core);
std::lock_guard<std::mutex> lock(g_state.mu);
auto exec = [](const std::string& sql, xsql::ScriptStatementResult& result) {
auto res = g_state.session->query(sql);
std::lock_guard<std::mutex> lock(state.mu);
ensure_session(state, core);
auto exec = [&state](const std::string& sql, xsql::ScriptStatementResult& result) {
auto res = state.session->query(sql);
result.success = res.ok();
result.error = res.error;
result.warnings = std::move(res.warnings);
@@ -205,11 +201,12 @@ bool dispatch_json(RCore* core, const char* input) {
}
bool plugin_call(RCorePluginSession* ctx, const char* input) {
if (!ctx || !input) return false;
if (!ctx || !ctx->data || !input) return false;
auto& state = *static_cast<PluginState*>(ctx->data);
if (std::strncmp(input, "sqlj", 4) == 0) {
const char* sql = input + 4;
if (*sql && *sql != ' ' && *sql != '\t' && *sql != '?') return false;
return dispatch_json(ctx->core, sql);
return dispatch_json(state, ctx->core, sql);
}
// r2 calls every registered plugin's call() for every command; only
// claim the ones that start with our prefix.
@@ -219,17 +216,24 @@ bool plugin_call(RCorePluginSession* ctx, const char* input) {
if (*rest != '\0' && *rest != ' ' && *rest != '?' && *rest != '.' && *rest != ',') {
return false;
}
return dispatch(ctx->core, rest);
return dispatch(state, ctx->core, rest);
}
bool plugin_init(RCorePluginSession* /*ctx*/) {
return true;
bool plugin_init(RCorePluginSession* ctx) {
ctx->data = new (std::nothrow) PluginState;
return ctx->data != nullptr;
}
bool plugin_fini(RCorePluginSession* /*ctx*/) {
std::lock_guard<std::mutex> lock(g_state.mu);
g_state.session.reset();
g_state.bound_core = nullptr;
bool plugin_fini(RCorePluginSession* ctx) {
auto* state = static_cast<PluginState*>(ctx->data);
if (state) {
{
std::lock_guard<std::mutex> lock(state->mu);
state->session.reset();
}
delete state;
ctx->data = nullptr;
}
return true;
}
@@ -242,10 +246,10 @@ static RCorePlugin r_core_plugin_r2xsql = {
/*.name =*/ const_cast<char*>("r2xsql"),
/*.desc =*/ const_cast<char*>("SQL interface for radare2 (libr2xsql)"),
/*.author =*/ const_cast<char*>("0xeb"),
/*.version =*/ const_cast<char*>("0.0.1"),
/*.version =*/ const_cast<char*>(R2XSQL_VERSION_STRING),
/*.license =*/ const_cast<char*>("LicenseRef-Human-Origin-Source-1.0"),
/*.contact =*/ const_cast<char*>("[email protected]"),
/*.copyright =*/ const_cast<char*>("Copyright (c) 2024-2026 Elias Bachaalany"),
/*.copyright =*/ const_cast<char*>(R2XSQL_COPYRIGHT_STRING),
/*.status =*/ R_PLUGIN_STATUS_BASIC,
},
/*.init =*/ plugin_init,
+2 -2
View File
@@ -1,9 +1,9 @@
NAME=native IO maps keep metacharacters in names as data
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='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
+1 -1
View File
@@ -3,7 +3,7 @@ FILE=build/fixture.bin
ARGS=-e log.level=0 -i build/load.r2
CMDS=<<CMDS
aaa
"sql,:quiet SELECT count(*)=1 FROM data_items WHERE name='r2xsql_marker' AND size>0 AND is_string=0 AND is_initialized=1"
"sql,:quiet SELECT count(*)=1 FROM data_items WHERE name IN ('r2xsql_marker','_r2xsql_marker') AND is_string=0 AND is_initialized=1"
"sql,:quiet SELECT count(*)>0 FROM data_items WHERE is_string=1 AND value_repr='r2xsql_test_marker_string' AND size>length(value_repr)"
"sql,:quiet SELECT (SELECT count(*) FROM grep WHERE kind='func')=(SELECT count(*) FROM funcs)"
"sql,:quiet SELECT (SELECT count(*) FROM grep WHERE kind='import')=(SELECT count(*) FROM imports)"
+1 -1
View File
@@ -31,7 +31,7 @@ def test_r2_http(r2_bin, r2xsql_plugin, fixture_binary, tmp_path, post):
with log_path.open("w") as log:
proc = subprocess.Popen([
r2_bin, "-NN", "-q", "-e", "http.bind=127.0.0.1", "-e", f"http.port={port}",
"-e", "http.log=false", "-e", "scr.color=0",
"-e", "http.log=false", "-e", "http.sandbox=false", "-e", "scr.color=0",
"-c", f"L {r2xsql_plugin}", "-c", "aa", "-c", "=h", str(fixture_binary),
], stdout=log, stderr=log, cwd=tmp_path)
try:
+45
View File
@@ -0,0 +1,45 @@
# SPDX-License-Identifier: LicenseRef-Human-Origin-Source-1.0
"""Check an automatically discovered release plugin on every supported OS."""
import json
from pathlib import Path
import subprocess
import sys
def check(r2):
# -Q skips plugin finalizers; process teardown must still be safe.
for quiet in ("-q", "-Q"):
commands = [
'"sqlj SELECT NULL AS missing, 7 > 3 AS ok; SELECT 2 AS n"',
'"sqlj INSERT INTO comments(addr,text) VALUES(4, \'release smoke\'); '
"SELECT text FROM comments WHERE addr=4; "
"DELETE FROM comments WHERE addr=4; "
'SELECT count(*) FROM comments WHERE addr=4"',
'"sqlj SELECT * FROM missing_table"',
'"sqlj SELECT 42 AS alive"',
'"sql,:csv SELECT 1 AS n, \'a,b\' AS name"',
]
args = [r2, "-N", quiet, "-e", "scr.color=0", "-e", "scr.utf8=false"]
for command in commands:
args.extend(["-c", command])
result = subprocess.run(args + ["malloc://64"], capture_output=True,
text=True, timeout=30)
assert result.returncode == 0, (quiet, result.returncode, result.stderr)
lines = result.stdout.splitlines()
assert len(lines) == 6, (result.stdout, result.stderr)
multi, write, error, alive = map(json.loads, lines[:4])
assert multi["success"] and multi["statement_count"] == 2, multi
assert multi["results"][0]["rows"] == [[None, "1"]], multi
assert multi["results"][1]["rows"] == [["2"]], multi
assert write["success"] and write["statement_count"] == 4, write
assert write["results"][1]["rows"] == [["release smoke"]], write
assert write["results"][3]["rows"] == [["0"]], write
assert not error["success"], error
assert alive["success"] and alive["results"][0]["rows"] == [["42"]], alive
assert lines[4:] == ["n,name", '1,"a,b"'], result.stdout
print("Packaged plugin: JSON, CRUD, errors, CSV and process cleanup OK")
if __name__ == "__main__":
check(str(Path(sys.argv[1]).resolve()))