r2pharo

Description

Pharo/Smalltalk bindings for Frida

Details

Source
GitHub
Dialect
pharo (25% confidence)
License
MIT
Stars
3
Created
July 19, 2026
Updated
July 20, 2026

README excerpt

# 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.
← Back to results