Skip to content

rsandbox: RPython-style compile-time sandbox for pyre (#285) - #304

Merged
youknowone merged 2 commits into
mainfrom
rsandbox
Jul 4, 2026
Merged

rsandbox: RPython-style compile-time sandbox for pyre (#285)#304
youknowone merged 2 commits into
mainfrom
rsandbox

Conversation

@youknowone

@youknowone youknowone commented Jun 28, 2026

Copy link
Copy Markdown
Owner

Closes #285.

Implements an RPython/PyPy-style sandbox for pyre. pyre's interpreter is never
translated to a standalone binary (there is no genc backend), so the faithful
analog of RPython's translate.py --sandbox is rustc + cargo as the
"translation", and #[cfg(feature = "sandbox")] as the compile-time rewrite
:
the sandbox feature compiles out the real-syscall implementations and
compiles in marshalling trampolines, reproducing RPython's security property
— syscall code unreachable from untrusted Python — at compile time.

Untrusted Python runs as pyre --features sandbox, driven by a trusted
pyre interact controller over the rmarshal wire protocol and a virtual
filesystem. All OS access is mediated; the host is never touched directly.

Architecture

  • pyre-sandbox crate — the single live home shared by the untrusted client
    and the trusted controller: byte-exact rmarshal wire codec, protocol,
    trait-based vfs, client trampoline half, and the sandlib/controller
    ports of sandlib.py + pypy_interact.py. pyre interact (in pyrex) drives
    it. Deps: libc + std only.
  • host_seam (interpreter) — the single indirection to the OS. Two
    #[cfg]-selected impls: RealHost (today's libc/std bodies, off sandbox) and
    TrampolineHost (marshals to the controller, on sandbox). Builtins are
    non-capturing fn pointers, so the seam is reached by module path through
    host_seam::ops::*.

Defense layers

  1. Reroute — file-object fd I/O, stdio, and the posix/time OS surface go
    through host_seam::ops::* (marshalled to the controller).
  2. Stubs + module omission — host-mutating/process/fd/privilege builtins
    raise; _socket/_ctypes/_posixsubprocess/_signal/fcntl/… modules are
    compiled out entirely. Import resolution is routed through a seam-backed
    SourceProvider (no std::fs), so import cannot read arbitrary host files.
  3. Fails-closed host_seam::sys facade — under sandbox, sys re-exports
    only libc TYPES, CONSTANTS and curated PURE functions; the mediated modules
    name libc as crate::host_seam::sys, so any direct syscall call left outside
    the seam is a compile error. A green cargo build --features sandbox is
    the proof.
  4. seccomp-bpf backstop (Linux) — the child installs a hand-rolled classic-BPF
    filter after startup and before untrusted code: a curated allowlist of
    host-neutral runtime syscalls (memory, signals, time, I/O on the open
    marshalling fds), everything else SECCOMP_RET_KILL_PROCESS. This is the
    analog of RPython's os_level_sandboxing and covers what the source-level
    seam cannot: the linked host_env crate, std, or a syscall reached by a
    memory-safety exploit.

A 2026-06-27 escape audit (workflow, adversarially verified) found 40 reachable
escapes in an earlier import-free state (import spawned python3, read host
files via HostFsProvider, sendfile/readlink/scandir/… unstubbed). All are
closed here and locked by the e2e regression guard.

Verification

  • pyre/check.py --backend dynasm: 160/160 ×2 (default feature set, sandbox
    OFF — all changes are sandbox-cfg or not(sandbox), so the default build is
    byte-identical).
  • cargo build --release -p pyrex --bin pyre --features sandbox: green (the
    fails-closed facade proof).
  • e2e (pyre-sandbox/tests/e2e_interact.rs, port of test_pypy_interact.py):
    2/2 — virtual-FS read, /etc/passwd + write attempts blocked, and a
    regression guard asserting the audited escape surface stays closed.
  • New CI job sandbox-build (ubuntu): builds the sandbox binary and runs the
    e2e — and, on Linux, validates the seccomp allowlist (a SIGSYS-killed child
    surfaces as a failed run).

Caveats

  • The seccomp allowlist is compile-validated (cargo check --target x86_64-unknown-linux-gnu) but not runtime-validated locally (dev is
    macOS, where seccomp is cfg-d out); the Linux CI e2e is the runtime check. A
    missing runtime syscall fails safe (over-restrictive: the child is killed, not
    escaped). It is default-on under sandbox on Linux; PYRE_SANDBOX_NO_SECCOMP
    bypasses it for direct-run debugging (the controller env_clears the child, so
    it cannot be disabled through the real path).
  • sandbox = ["host_env"] is deliberate: the host_env crate stays linked but
    is unreachable from untrusted Python (and killed by seccomp if reached). The
    majit-translate sandbox shells + rtyper.rs:734 hook stay as inert
    RPython-parity mirrors (there is no genc backend to attach to).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added pyre interact to run an external program inside a controlled sandbox with optional virtual filesystem roots, optional library root, and a timeout.
    • Introduced sandbox-mediated OS/file operations (including fd I/O, seek, and virtual sys.executable) with a virtualized environment and console behavior.
  • Bug Fixes
    • Fixed sandbox output handling by routing stdout/stderr through the sandbox channel to avoid pipe corruption.
    • Tightened sandbox hardening: many host-access modules and locale/time functions are now stubbed or unavailable under sandbox.
  • Tests / CI
    • Added sandbox build + end-to-end interaction CI coverage, including ignored escape-probe e2e checks.

Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Research if we can properly implement rsandbox

1 participant