Files

r2pipe-wasm (Python + Wasmer)

Run radare2 inside Python as a WebAssembly instance, without a native radare2 installation or a subprocess. This is a separate package (r2pipe_wasm) with an r2pipe-like open, cmd, cmdj, quit and context-manager interface.

The backend uses Wasmer's Python SDK and the wasi-api reactor build, calling r_core_new, r_core_cmd_str, r_core_file_open, r_core_bin_load and r_core_free directly. Command strings and results are allocated/freed in guest memory. Each connection owns its own instance and core.

Install

From this repository:

python3.9 -m venv .venv
. .venv/bin/activate
pip install ./python-wasm

Runtime compatibility: Wasmer's published Python SDK is 1.1.0 and its native wheels have limited Python/platform coverage. CPython 3.9 on Linux x86_64 is verified here. A newer Python may install a placeholder package that raises ImportError; installing successfully alone does not prove runtime support. Use a supported native wheel or build the Wasmer Python SDK and matching Cranelift extension yourself. This package does not fall back to a CLI/runtime subprocess. Release discovery/downloading uses only the Python standard library and can be imported without loading Wasmer.

Analyze a file

from pathlib import Path
import r2pipe_wasm as r2pipe

# Only this directory is shared. The filename is a path in the guest environment.
samples = Path("samples").resolve()
with r2pipe.open("/samples/program", version="6.1.4",
                 preopens={"/samples": samples}) as r2:
    print(r2.cmdj("ij"))
    r2.cmd("aaa")
    print(r2.cmdj("aflj"))
    print(r2.cmd("pd 10"))

By default open() downloads/caches the newest stable release with a wasi-api asset. A newer release without that asset is skipped. CLI wasi and wasi-browser builds are not substitutes. Compilation takes place when opening each connection; keep the connection open for multiple commands.

Known upstream release issue: the published 6.2.2 API module traps inside r_core_new() (ht_pp_find_kv during config initialization), reproduced with both Wasmer and Node WASI. The examples pin 6.1.4, verified end to end. The default still selects the newest stable API asset and propagates guest errors; it does not silently fall back to an older version. Use version= or wasm= to select a working release/build.

List releases, choose a version, download once

from r2pipe_wasm import DownloadManager, api_asset, open

manager = DownloadManager(cache_dir=".cache/radare2")
for release in manager.releases():  # paginates the GitHub releases API
    asset = api_asset(release)
    print(release["tag_name"], asset["name"] if asset else "no API build")

print(manager.select()["tag_name"])       # newest stable API release
wasm = manager.download("6.1.4")           # exact tag, or "latest" (default)
with open(wasm=wasm) as r2:                # local module: no network requests
    print(r2.cmd("?V"))

# Or select a release directly when opening:
with open(version="6.1.4", manager=manager) as r2:
    print(r2.cmd("?V"))

releases() includes prereleases and releases without WASM assets; select() and download() reject explicit tags without an API build. Explicit prerelease tags are allowed. GitHub HTTP errors (including rate limits) propagate to the caller. An optional token= is sent only to the GitHub API, never to the guest or release download URL. No host environment variables are inherited by WASI.

The default cache is $XDG_CACHE_HOME/r2pipe-wasm or ~/.cache/r2pipe-wasm. Downloads use temporary files and an atomic replacement, validate the WASM header, and verify GitHub's SHA-256 asset digest when provided. Archive members are never extracted by their paths. download(force=True) refreshes a cached module. Version selection still contacts GitHub on a cache hit; pass wasm= for fully offline operation.

Share files and save results

preopens maps guest absolute directory → host directory. There are no preopened host directories by default, and opening a host path does not share it automatically. Mappings expose the directory's contents with read/write access subject to host permissions; use a dedicated directory or copies for inputs. The initial binary is opened read-only, but commands can write shared files.

from pathlib import Path
import r2pipe_wasm

shared = Path("work").resolve()
shared.mkdir(exist_ok=True)
(shared / "sample.bin").write_bytes(bytes.fromhex("9090c3"))

with r2pipe_wasm.open("/work/sample.bin", wasm="radare2.wasm",
                     preopens={"/work": shared}) as r2:
    r2.cmd("e asm.arch=x86; e asm.bits=64")
    print(r2.cmd("pd 3"))
    r2.cmd("px 3 > /work/hexdump.txt")

print((shared / "hexdump.txt").read_text())

With the default network=False, the WASM guest has no network capability: WASI socket calls return ENOTSUP and optional HTTP imports return failure. No WASIX/process-spawning imports are supplied, even with networking enabled. Commands such as !sh and native debugging cannot use host services. The host-side download manager uses HTTPS independently of the guest network setting. WASI stdout/stderr remain available; this is a synchronous analysis API, not an interactive terminal. Use quit()/close() or a context manager to release it. cmdj() raises JSONDecodeError for invalid JSON; commands after closing fail.

Optional HTTP networking

Only HTTP(S) GET and POST are exposed, through radare2's existing r_socket_http_get() and r_socket_http_post() C APIs. There are no socket handles, listeners, UDP, WebSockets, or shell-curl execution. The host ABI is one r2.http_request(post, url, headers, data, code, length) import, with post=0 for GET and post=1 for POST. Update the host and WASM module together; this replaces the earlier two-import bridge.

# In a clean radare2 checkout containing the HTTP bridge:
WASI_HTTP=1 sys/wasi-api.sh
# Extract radare2.wasm from radare2-<version>-wasi-api-http.zip.
import r2pipe_wasm

with r2pipe_wasm.open("http://127.0.0.1:8000/sample.bin",
                     wasm="radare2-http.wasm", network=True) as r2:
    print(r2.cmdj("pxj 16"))
    print(r2.cmd("curl http://127.0.0.1:8000/status"))
    # JSON POST with headers via the guest's QuickJS binding to the C HTTP API:
    script = 'console.log(r2.http("POST", "http://127.0.0.1:8000/api", '
    script += '"Content-Type: application/json", "{}").body)'
    print(r2.cmd("js " + script))

network defaults to False, including for an HTTP-enabled module. When True, Python supplies HTTP(S) using urllib with normal TLS certificate verification, a 30-second socket timeout and a 16 MiB response limit. Headers, UTF-8 POST data, HTTP status/error bodies and binary responses are preserved. The C caller owns the guest-allocated response buffer. There are no persistent socket handles to manage. The host download manager's HTTPS access is independent of this guest setting.

Host opt-in never overrides the radare2 sandbox. When sandboxing is enabled, each request requires both socket and network grains, even for localhost, because the host can follow redirects. No exec grain is needed. For example:

r2.cmd("e cfg.sandbox.grain=none; e cfg.sandbox=true")
# Subsequent HTTP requests fail, even with network=True on the host.

network=True without wasm= selects/downloads a -wasi-api-http.zip asset. Use a local build until radare2 PR #27037 is merged/released. A module without the HTTP imports is rejected explicitly. The release manager also accepts select(network=True), download(version, network=True) and api_asset(release, network=True).

Tests

python -m unittest discover -s python-wasm/tests -v
# Also run the real WASI module, file sharing and isolation checks:
R2PIPE_WASM=/absolute/path/to/radare2.wasm \
    python -m unittest discover -s python-wasm/tests -v

Set R2PIPE_WASM_NETWORK=/absolute/path/to/radare2-http.wasm to additionally exercise the real C GET/POST APIs, headers, UTF-8 and binary bodies, HTTP file IO, embedded JavaScript, host opt-in and sandbox denial against a local HTTP server.

The offline runtime tests reuse the repository's small TypeScript WASM API fixture. Downloader tests do not require Wasmer or network access.