Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions .github/workflows/pyre-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -509,3 +509,65 @@ jobs:
fi
cargo build -p pyre-wasm --target wasm32-unknown-unknown \
--no-default-features --features "${{ matrix.binding }}"

sandbox-build:
name: sandbox build + e2e (ubuntu-24.04)
runs-on: ubuntu-24.04
needs: prepare-charon-llbc-linux
if: ${{ !cancelled() && needs.prepare-charon-llbc-linux.result == 'success' }}
env:
# See prepare-charon-llbc: downstream jobs download the prepared Charon
# artifact into this workspace path. The sandbox `pyre` binary still
# pulls pyre-jit, whose build needs the extracted LLBC.
PYRE_SHARED_BUILD: ${{ github.workspace }}/.pyre-build
CHARON_VERSION: nightly-2026.05.29
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
cache-bin: false
- name: Download Charon artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: charon-${{ runner.os }}-${{ runner.arch }}
path: .pyre-build/charon
- name: Download LLBC artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: llbc-${{ runner.os }}-${{ runner.arch }}
path: build/llbc
- name: Verify prepared Charon/LLBC
shell: bash
run: |
test -d .pyre-build/charon
test -s build/llbc/pyre-object.ullbc
test -s build/llbc/pyre-interpreter.ullbc
test -s build/llbc/pyre-jit.ullbc
- name: Build pyre --features sandbox
# A green sandbox build is the fails-closed proof: every mediated module
# names libc through host_seam::sys, so any direct syscall left outside
# the seam fails to compile here. On Linux `sandbox` installs the seccomp
# backstop by default, so this also compile-checks the install path.
run: cargo build --release -p pyrex --bin pyre --features sandbox
- name: Sandbox compile-out fence (clippy)
# Extends the fails-closed proof beyond libc. host_seam::sys already makes
# a stray `libc::` syscall fail to compile under sandbox; this forbids the
# non-libc host surface too (std::fs / std::env / std::io stdio /
# std::process / std::net) in the untrusted interpreter. `--no-deps` scopes
# it to pyre-interpreter's own code — the trusted `pyre-sandbox` controller
# and the host-side build script are exempt. CLIPPY_CONF_DIR points clippy
# at ci/clippy-sandbox/clippy.toml, which normal clippy never discovers, so
# a green run proves no raw host call survives in sandbox-live code.
env:
CLIPPY_CONF_DIR: ${{ github.workspace }}/ci/clippy-sandbox
run: cargo clippy -p pyre-interpreter --no-deps --features sandbox,dynasm -- -A clippy::all -D clippy::disallowed_methods -D clippy::disallowed_types
- name: Run sandbox end-to-end suite
# Exercises the compile-out sandbox (virtual FS + escape blocking) through
# the controller. On Linux the e2e binary (built `--features sandbox`)
# installs the seccomp allowlist, so this run also RUNTIME-VALIDATES it: a
# syscall the interpreter needs but the allowlist omits makes the child
# SIGSYS (exit 159) and fails the suite.
run: cargo test --release -p pyre-sandbox --test e2e_interact -- --ignored --nocapture
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ members = [
"pyre/pyre-macros",
"pyre/pyre-native",
"pyre/pyre-interpreter",
"pyre/pyre-sandbox",
"pyre/pyre-module",
"pyre/pyre-jit",
"pyre/pyre-jit-trace",
Expand All @@ -53,6 +54,7 @@ pyre-object = { version = "0.0.2", path = "pyre/pyre-object" }
pyre-macros = { version = "0.0.2", path = "pyre/pyre-macros" }
pyre-native = { version = "0.0.2", path = "pyre/pyre-native" }
pyre-interpreter = { version = "0.0.2", path = "pyre/pyre-interpreter" }
pyre-sandbox = { version = "0.0.2", path = "pyre/pyre-sandbox" }
pyre-module = { version = "0.0.2", path = "pyre/pyre-module" }
pyre-jit = { version = "0.0.2", path = "pyre/pyre-jit" }
pyre-jit-trace = { version = "0.0.2", path = "pyre/pyre-jit-trace" }
Expand Down
68 changes: 68 additions & 0 deletions ci/clippy-sandbox/clippy.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Sandbox compile-out fence.
#
# This config is applied ONLY by the dedicated sandbox clippy CI job, which
# points clippy at this directory via `CLIPPY_CONF_DIR`. Normal builds and
# normal `cargo clippy` never discover it (it is not an ancestor of any crate),
# so day-to-day development is unaffected.
#
# It forbids raw host-OS access in the `--features sandbox` build of the
# untrusted interpreter. Under sandbox every mediated OS call must go through
# `crate::host_seam::ops::*`, which marshals the request to the trusted
# controller; a direct `std::fs`/`std::env`/`std::io` stdio/`std::process`/
# `std::net` call would bypass the seam and escape the sandbox. Because clippy
# only sees code that actually compiles under the feature set, the
# `#[cfg(not(feature = "sandbox"))]` real-syscall paths are invisible here — a
# clean run therefore proves no raw host call survives in sandbox-live code.
#
# The trusted controller lives in the separate `pyre-sandbox` crate and is not
# linted by this job (it lints `-p pyre-interpreter` only).

disallowed-methods = [
# Filesystem entry points
{ path = "std::fs::read", reason = "raw host FS read; route through crate::host_seam::ops" },
{ path = "std::fs::write", reason = "raw host FS write; route through crate::host_seam::ops" },
{ path = "std::fs::read_to_string", reason = "raw host FS read; route through crate::host_seam::ops" },
{ path = "std::fs::read_link", reason = "raw host FS access; route through crate::host_seam::ops" },
{ path = "std::fs::read_dir", reason = "raw host FS listdir; route through crate::host_seam::ops" },
{ path = "std::fs::metadata", reason = "raw host FS stat; route through crate::host_seam::ops" },
{ path = "std::fs::symlink_metadata", reason = "raw host FS lstat; route through crate::host_seam::ops" },
{ path = "std::fs::canonicalize", reason = "raw host FS access; route through crate::host_seam::ops" },
{ path = "std::fs::remove_file", reason = "host FS mutation is forbidden in the sandbox" },
{ path = "std::fs::remove_dir", reason = "host FS mutation is forbidden in the sandbox" },
{ path = "std::fs::remove_dir_all", reason = "host FS mutation is forbidden in the sandbox" },
{ path = "std::fs::create_dir", reason = "host FS mutation is forbidden in the sandbox" },
{ path = "std::fs::create_dir_all", reason = "host FS mutation is forbidden in the sandbox" },
{ path = "std::fs::rename", reason = "host FS mutation is forbidden in the sandbox" },
{ path = "std::fs::copy", reason = "host FS mutation is forbidden in the sandbox" },
{ path = "std::fs::hard_link", reason = "host FS mutation is forbidden in the sandbox" },
{ path = "std::fs::set_permissions", reason = "host FS mutation is forbidden in the sandbox" },
# Process environment
{ path = "std::env::var", reason = "raw process env; route through crate::host_seam::ops::getenv" },
{ path = "std::env::var_os", reason = "raw process env; route through crate::host_seam::ops::getenv" },
{ path = "std::env::vars", reason = "raw process env; route through crate::host_seam::ops::envitems" },
{ path = "std::env::vars_os", reason = "raw process env; route through crate::host_seam::ops::envitems" },
{ path = "std::env::set_var", reason = "host env mutation is forbidden in the sandbox" },
{ path = "std::env::remove_var", reason = "host env mutation is forbidden in the sandbox" },
{ path = "std::env::current_dir", reason = "raw host cwd; route through crate::host_seam::ops::getcwd" },
{ path = "std::env::set_current_dir", reason = "host cwd mutation is forbidden in the sandbox" },
{ path = "std::env::current_exe", reason = "leaks the real executable path; forbidden in the sandbox" },
{ path = "std::env::temp_dir", reason = "leaks a real host path; forbidden in the sandbox" },
# Standard streams (the marshalling pipe is the only sanctioned raw stdio)
{ path = "std::io::stdout", reason = "raw stdout; route through crate::host_seam::ops::write" },
{ path = "std::io::stderr", reason = "raw stderr; route through crate::host_seam::ops::write" },
{ path = "std::io::stdin", reason = "raw stdin; mediated by the controller" },
{ path = "std::io::_print", reason = "print!/println! writes real stdout; route through the host_seam" },
{ path = "std::io::_eprint", reason = "eprint!/eprintln! writes real stderr; route through the host_seam" },
# Process control
{ path = "std::process::exit", reason = "raw process exit; the controller owns process lifetime" },
{ path = "std::process::abort", reason = "raw process abort; the controller owns process lifetime" },
]

disallowed-types = [
{ path = "std::fs::File", reason = "raw host file handle; route file I/O through crate::host_seam::ops" },
{ path = "std::fs::OpenOptions", reason = "raw host file open; route through crate::host_seam::ops::open" },
{ path = "std::process::Command", reason = "process spawn is forbidden in the sandbox" },
{ path = "std::net::TcpStream", reason = "raw network; use the mediated tcp:// controller path" },
{ path = "std::net::TcpListener", reason = "raw network listen is forbidden in the sandbox" },
{ path = "std::net::UdpSocket", reason = "raw network is forbidden in the sandbox" },
]
5 changes: 5 additions & 0 deletions pyre/pyre-interpreter/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ description = "Python bytecode interpreter for pyre"
[features]
default = ["host_env"]
host_env = ["dep:rustpython-host_env"]
# RPython-style sandbox: route every OS call through host_seam's marshalling
# trampoline instead of the real syscall. Implies host_env (same call surface,
# swapped bodies); the real-vs-trampoline choice lives only inside host_seam.
sandbox = ["host_env", "dep:pyre-sandbox"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Route traceback source reads through the seam

When the new sandbox feature enables host_env, read_source_line() still compiles its rustpython_host_env::fs::read_to_string(filename) branch and is not routed through the sandbox SourceProvider. A sandboxed program can set an arbitrary traceback filename, e.g. via compile(..., "/etc/passwd", "exec"), then raise an exception and have the child read and print host file lines on stderr; with seccomp enabled the same path would die on the raw open instead of producing a normal sandbox error. Please gate this host_env path out under sandbox or read traceback lines through the seam.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Return virtual thread IDs in sandbox

Because sandbox implies host_env while _thread remains registered for sandbox builds, _thread.get_ident() takes the rustpython_host_env::thread::current_thread_id() branch and _thread.get_native_id() still calls the allowlisted host gettid syscall. A sandboxed script can therefore read real pthread/kernel thread IDs even though comparable process IDs are stubbed; gate both functions to the existing single-thread sentinel under feature = "sandbox".

Useful? React with 👍 / 👎.

cranelift = ["majit-metainterp/cranelift"]
dynasm = ["majit-metainterp/dynasm"]
# Embed the pure-Python stdlib closure needed for `import re` into the binary
Expand All @@ -35,6 +39,7 @@ num-integer = { workspace = true }
pymath = { workspace = true }
sre-engine = { workspace = true }
rustpython-host_env = { workspace = true, optional = true }
pyre-sandbox = { workspace = true, optional = true }
libc = { workspace = true }
siphasher = { workspace = true }
caseless = { workspace = true }
Expand Down
5 changes: 5 additions & 0 deletions pyre/pyre-interpreter/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
//!
//! When the feature is off (every native build) this returns immediately and
//! produces nothing.
//!
//! This build script runs on the host at compile time — it is not part of the
//! sandbox binary — so the sandbox compile-out fence (`ci/clippy-sandbox`) does
//! not apply to its host filesystem/env access.
#![allow(clippy::disallowed_methods, clippy::disallowed_types)]

use std::path::Path;

Expand Down
Loading
Loading