Pharo/Smalltalk bindings for Frida
# r2pharo
A Pharo/Smalltalk binding for [radare2](https://github.com/radareorg/radare2).
r2pharo links `libr` **directly**: it wraps radare2's `RCore` through Pharo uFFI
(`r_core_new`, `r_core_cmd_str`, `r_core_free`, ...). No subprocess is spawned and
**no system radare2 is required** — the binding loads a single self-contained
`libr2pharo` shared library, downloaded automatically on first use.
## Usage
```smalltalk
core := R2Core openFile: '/bin/ls'.
core analyzeAll.
functions := core functions. "cmdj 'aflj' -> collection of dictionaries"
Transcript showLn: core version. "cmd '?V' -> String"
core config set: 'asm.arch' to: 'arm'.
core close.
```
`R2Core`:
- `cmd:` — run a command, answer the output `String`.
- `cmdj:` — run a command, answer its JSON output parsed into Pharo collections.
- `config` — an `R2Config` facade with `set:to:` / `get:` over the eval
namespace, driven by `r_config_set` / `r_config_get` on the core's `RConfig`.
- Convenience wrappers: `analyzeAll`, `analyzeAllExperimental`, `seekTo:`,
`offset`, `functions`, `disassembleFunction`, `disassemble:`,
`disassemble:at:`, `fileInfo`, `sections`, `imports`, `symbols`, `strings`,
`version`.
- `close` frees the underlying `RCore` (finalization frees it too if you forget).
## I/O plugins
You can back a radare2 URI scheme with Smalltalk. Subclass `R2IOFile` to describe
some bytes (`size` and the bytes at an offset) and `R2IOPlugin` to serve them
under a `scheme`; radare2 then routes every read/seek/write on that descriptor
through your file. A minimal read-only plugin over an in-image `ByteArray`:
```smalltalk
R2IOFile << #MemoryFile
slots: { #bytes };
package: 'MyR2'.
MemoryFile class >> on: aByteArray [ ^ self new setBytes: aByteArray ]
MemoryFile >> setBytes: aByteArray [ bytes := aByteArray ]
MemoryFile >> size [ ^ bytes size ]
MemoryFile >> dataFrom: anOffset count: aCount [
^ bytes copyFrom: anOffset + 1 to: anOffset + aCount ]
R2IOPlugin << #MemoryPlugin
package: 'MyR2'.
MemoryPlugin >> scheme [ ^ 'mem' ]
MemoryPlugin >> openUri: aUri perms: aPermMask [
^ MemoryFile on: #[ 1 2 3 4 5 6 7 8 ] ]
```
Register it on a core and open its URI:
```smalltalk
core := R2Core open.
core addIOPlugin: MemoryPlugin new.
core cmd: 'o mem://x'.
core cmdj: 'pxj 8'. "-> #(1 2 3 4 5 6 7 8)"
core close.
```
`R2IOFile` tracks the read position and does the `seekTo:whence:` arithmetic, so a
subclass only describes its bytes; implement `putData:at:` too for a writable
file. The callbacks fire while radare2 services your command — they serve bytes
from the plugin's own storage and must not re-enter the same core.
### Inspecting live process memory via Frida
Because the bytes a plugin serves can come from anywhere, they can come from
another process. With [frida-pharo](https://github.com/frida/frida-pharo) loaded
alongside R2Pharo — an optional companion, not a dependency — an I/O plugin can
answer each read by asking a Frida agent to read the target's memory, so `px`,
`p8`, disassembly and search all run against a live process:
```smalltalk
R2IOFile << #FridaMemoryFile
slots: { #script. #base. #size };
package: 'MyR2'.
FridaMemoryFile >> setScript: aScript base: anAddress size: aSize [
script := aScript. base := anAddress. size := aSize ]
FridaMemoryFile >> size [ ^ size ]
FridaMemoryFile >> dataFrom: anOffset count: aCount [
^ script exports readMemory: (base + anOffset) printString size: aCount ]
FridaMemoryFile >> putData: aByteArray at: anOffset [
script exports writeMemory: (base + anOffset) printString data: aByteArray ]
R2IOPlugin << #FridaMemoryPlugin
slots: { #script };
package: 'MyR2'.
FridaMemoryPlugin >> setScript: aScript [ script := aScript ]
FridaMemoryPlugin >> scheme [ ^ 'frida' ]
FridaMemoryPlugin >> openUri: aUri perms: aPermMask [
| base |
base := Integer readFrom: ((aUri copyReplaceAll: 'frida://' with: '') allButFirst: 2) readStream base: 16.
^ FridaMemoryFile new setScript: script base: base size: 16r10000000 ]
```
The agent exposes memory reads and writes over RPC. A read returns an
`ArrayBuffer`, which frida-pharo delivers straight to Smalltalk as a `ByteArray`;
the write's trailing `ByteArray` rides the RPC's binary channel:
```smalltalk
device := Frida localDevice.
pid := device spawn: '/bin/sh'. "spawned suspended"
session := device attach: pid.
script := session createScript: 'rpc.exports = {
getBase() { return Process.mainModule.base.toString(); },
readMemory(address, size) { return ptr(address).readByteArray(size); },
writeMemory(address, data) { ptr(address).writeByteArray(data); }
};'.
script load.
```
Open the target's main module at its base address and read through radare2:
```smalltalk
base := script exports getBase. "e.g. '0x100fd0000'"
core := R2Core open.
core addIOPlugin: (FridaMemoryPlugin new setScript: script).
core cmd: 'o frida://', base.
core cmd: 's 0'.
core cmd: 'p8 4'. "-> 'cffaedfe' — the Mach-O magic at the image base"
```
Every `p8`/`px`/`pd` now reads the running process's memory over Frida. The read
callback runs on the core's pump process and blocks on the Frida RPC; the Frida
main loop delivers the reply on another process, so the VM never freezes. As
above, the callback must not re-enter the same core — but calling Frida is a
different subsystem, so it is fine.
## Threading
`RCore` is not thread-safe or reentrant, and a command can block for a long time
(a full `aaa` analysis, say). So each `R2Core` runs on its **own dedicated OS
thread**: a small C executor (`glue/r2pharo_executor.c`) owns the `RCore` and
makes every libr call on that thread, while the calling Pharo process parks until
the result is signalled back. The image stays responsive during a long command,
and separate cores analyze in parallel. Do not share one `R2Core` across
processes.
## The bundled library
radare2 ships `libr` as ~two dozen separate shared libraries, but uFFI loads one.
`tools/build-lib.sh` links a **static** radare2 build (`meson setup
--default-library=static -Dsdb_cgen=true -Duse_sys_lz4=false`) into a single
self-contained `libr2pharo.{dylib,so,dll}` that exports only the `r_core_*` /
`r_config_*` symbols the binding imports (scanned from the `ffiCall` sites) and
dead-strips the rest. `glue/*.c` is compiled into the same library: the per-core
executor thread (`r2pharo_executor.c`) and the I/O-plugin trampolines
(`r2pharo_io.c`) live there.
`R2Library` resolves that library once, in order:
1. the `R2PHARO_LIB` environment variable (a developer's local build);
2. a previously downloaded copy in the per-image cache;
3. otherwise the prebuilt for this platform + pinned radare2 version is fetched
from the r2pharo GitHub release, checksum-verified against `SHA256SUMS`, and
cached.
So a first-time user just loads the baseline and calls `R2Core` — nothing to
compile, no radare2 to install.
## Dependencies
- **NeoJSON** — parses `cmdj` responses.
Declared in `BaselineOfR2Pharo`.
## Installing
```smalltalk
Metacello new
baseline: 'R2Pharo';
repository: 'github://frida/r2pharo:main/src';
load.
```
## Building the library and running the tests
```sh
# Build radare2 statically once:
meson setup --default-library=static -Dsdb_cgen=true -Duse_sys_lz4=false \
r2build /path/to/radare2 && ninja -C r2build
make pharo # fetch a pinned Pharo image + VM (once)
make lib R2_BUILD=r2build # link libr2pharo from that build
make image # load the baseline into a fresh image
make test # run R2Pharo-Tests headless against the lib
```
`make test` needs no system radare2: it uses the locally built `libr2pharo` when
`R2_BUILD` was set, and otherwise lets `R2Library` download the prebuilt.
`R2CoreTest` exercises a live session (`?e`, `?V`, `pxj`, config round-trip); the
`malloc://` URIs keep every test self-contained, needing no sample binary.
`/path/to/radare2` can be any checkout — a release tag or a git snapshot — so
`R2_BUILD` builds and tests against whichever radare2 you point it at. The
release workflow does the same in CI: dispatch it with a radare2 tag for the
pinned prebuilts, or with a branch/commit for a snapshot build.