mirror of
https://github.com/radareorg/r2xsql.git
synced 2026-11-07 14:48:16 -10:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5417ccc164 | ||
|
|
9df012ee10 | ||
|
|
104599f12a | ||
|
|
c4b6114464 | ||
|
|
248bcdc83f |
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
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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)"
|
||||
|
||||
@@ -3,310 +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 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
@@ -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.
|
||||
@@ -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()
|
||||
@@ -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(),
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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