sandbox: an S1 executor — a native program confined by the operating system, earning the rung by probe #281

Merged
dcharlot merged 1 commit from feat/sandbox-exec-os into main 2026-10-17 04:02:09 -04:00
8 changed files with 773 additions and 19 deletions

View file

@ -508,6 +508,23 @@ jobs:
# process tree, spawned suspended and read before it is reaped. Its
# real-engine tests are gated to Apple Silicon macOS; each run they
# meter prints this line.
# sandbox-exec-os earns S1 only where its probe shows the operating
# system's sandbox confining a program on this host, and that branch
# exists only on macOS: every other runner takes the S0 branch. The step
# fails unless the test printed the confinement it found, the only proof
# the S1 branch ran rather than the S0 one passing in its place.
- name: sandbox-exec-os (Seatbelt confinement at S1)
shell: bash
working-directory: sandbox/sandbox-rs
env:
SANDBOX_EXEC_OS_EXPECT_S1: "1"
run: |
set -o pipefail
log=$(mktemp)
cargo test -p sandbox-exec-os -- --nocapture 2>&1 | tee "$log"
grep -q 'sandbox-exec-os: confinement seatbelt/' "$log"
grep -q 'denied a read outside the grants' "$log"
- name: sandbox-proof (local engine process tree)
shell: bash
working-directory: sandbox/sandbox-rs

View file

@ -1698,6 +1698,7 @@ dependencies = [
"sandbox-attest",
"sandbox-capability",
"sandbox-exec-microvm",
"sandbox-exec-os",
"sandbox-exec-wasm",
"sandbox-meter",
"sandbox-runtime",
@ -1761,6 +1762,18 @@ dependencies = [
"thiserror 2.0.20",
]
[[package]]
name = "sandbox-exec-os"
version = "0.1.0"
dependencies = [
"ed25519-dalek",
"sandbox-attest",
"sandbox-capability",
"sandbox-meter",
"sandbox-runtime",
"sandbox-schema",
]
[[package]]
name = "sandbox-exec-wasm"
version = "0.1.0"

View file

@ -9,6 +9,7 @@ members = [
"crates/sandbox-tee",
"crates/sandbox-exec-wasm",
"crates/sandbox-exec-microvm",
"crates/sandbox-exec-os",
"crates/sandbox-conformance",
"crates/sandbox-proof",
"crates/sandbox-proof-wasm",
@ -34,6 +35,7 @@ sandbox-runtime = { path = "crates/sandbox-runtime" }
sandbox-tee = { path = "crates/sandbox-tee" }
sandbox-exec-wasm = { path = "crates/sandbox-exec-wasm" }
sandbox-exec-microvm = { path = "crates/sandbox-exec-microvm" }
sandbox-exec-os = { path = "crates/sandbox-exec-os" }
sandbox-conformance = { path = "crates/sandbox-conformance" }
sandbox-proof = { path = "crates/sandbox-proof" }
sandbox-build = { path = "crates/sandbox-build" }

View file

@ -19,6 +19,7 @@ sandbox-attest.workspace = true
sandbox-runtime.workspace = true
sandbox-exec-wasm.workspace = true
sandbox-exec-microvm.workspace = true
sandbox-exec-os.workspace = true
ed25519-dalek.workspace = true
serde = { workspace = true }
serde_json.workspace = true

View file

@ -22,6 +22,7 @@ use sandbox_capability::Grants;
use sandbox_exec_microvm::firecracker::{FirecrackerConfig, ProcessVmm, Vmm};
use sandbox_exec_microvm::power::NoMeter;
use sandbox_exec_microvm::MicroVmExecutor;
use sandbox_exec_os::OsExecutor;
use sandbox_runtime::{
grants_content_id, unit_content_id, Executor, Host, RunError, TierRequest, Unit,
};
@ -466,11 +467,14 @@ fn resolve_candidate(
Ok((rec, true))
}
/// Resolve a **command** step: run the program in the S5 microVM (a real
/// subprocess via `ProcessVmm`; Firecracker on Linux+KVM in production), meter
/// it, and seal at S5 (an escalation above the grant floor — the host attests it
/// ran in the microVM). L0 if the identical command is cached. Returns the
/// record and whether it was a fresh execution.
/// Resolve a **command** step: run the program, meter it, and seal it at the
/// rung it actually reached. Where this host's operating-system confinement
/// holds and can enforce the step's grants, the command runs confined and
/// seals **S1** with its mechanism (`sandbox-exec-os`). Otherwise it runs as
/// an ordinary child process under `ProcessVmm` (Firecracker on Linux+KVM in
/// production) and seals the tier that reports, S0 on a host without one. L0
/// if the identical command is cached. Returns the record and whether it was a
/// fresh execution.
#[allow(clippy::too_many_arguments)]
fn resolve_command(
host: &Host,
@ -503,20 +507,34 @@ fn resolve_command(
return Ok((rec, false));
}
let vmm = ProcessVmm::new(program.to_string(), args.to_vec());
// The tier is read off the executor, never asserted here. `ProcessVmm::new`
// supervises the build command as an ordinary child process on the host, so
// on a host without hardware virtualization this is S0 — and the receipt
// will say S0. Sealing a rung the run did not reach would make the receipt
// worthless precisely where it is load-bearing.
let attained = vmm.attained_tier();
let exec = MicroVmExecutor::new(Box::new(vmm), template.clone(), Box::new(NoMeter));
let unit = Unit::new(image, grants);
let report = exec.run(&unit).map_err(|e| BuildError::Run(e.into()))?;
// The tier is read off the executor that ran the command, never asserted
// here: sealing a rung the run did not reach would make the receipt
// worthless precisely where it is load-bearing. The confined executor is
// used only where its confinement holds on this host AND it can enforce
// these grants; a step whose grants it cannot enforce (a subprocess, the
// network, a withheld clock) is never run confined with a capability its
// grant denies, and falls back to the ordinary child process.
let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("/"));
let confined = OsExecutor::new(program.to_string(), args.to_vec(), cwd);
let (attained, report, mechanism) =
if OsExecutor::confinement().is_some() && confined.policy(grants).is_ok() {
let report = confined.run(&unit).map_err(|e| BuildError::Run(e.into()))?;
(confined.tier(), report, confined.mechanism())
} else {
// `ProcessVmm::new` supervises the command as an ordinary child
// process on the host: S0 on a host without hardware virtualization.
let vmm = ProcessVmm::new(program.to_string(), args.to_vec());
let attained = vmm.attained_tier();
let exec = MicroVmExecutor::new(Box::new(vmm), template.clone(), Box::new(NoMeter));
let report = exec.run(&unit).map_err(|e| BuildError::Run(e.into()))?;
(attained, report, None)
};
let unit_id = unit_content_id(&image_id, &grants_id);
// The host attests the run at the tier the executor reports reaching.
let sealed = host.seal_run(
// The host attests the run at the tier the executor reports reaching, with
// the mechanism that executor committed to (none below S1).
let sealed = host.seal_run_with_mechanism(
unit_id,
image_id.clone(),
grants,
@ -526,6 +544,7 @@ fn resolve_command(
[0; 8],
started,
started + 1,
mechanism,
)?;
sealed
.receipt
@ -1067,6 +1086,46 @@ mod tests {
assert!(!verify_build_receipt(&bad));
}
/// A command step whose grants the operating-system confinement can
/// enforce — the clock and randomness, a granted directory to write — runs
/// confined and seals S1 with its mechanism where that confinement holds on
/// this host, and the ordinary S0 child process everywhere else.
#[test]
fn a_confinable_command_step_seals_s1_where_the_confinement_holds() {
use sandbox_capability::HostGrant;
use sandbox_exec_wasm::WasmExecutor;
let host = host();
let exec = WasmExecutor::with_defaults().unwrap();
let key = SigningKey::from_bytes(&[89u8; 32]);
let mut cache = BuildCache::new();
let dir = std::env::temp_dir().join(format!("sandbox-build-os-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let dir = dir.canonicalize().unwrap();
let mut grants = Grants {
host: HostGrant { logging: true, clock: true, random: true, inference: false },
..cpu_grants()
};
grants.storage.write = vec![dir.to_str().unwrap().into()];
let task = BuildTask {
intent: "write a build artifact".into(),
steps: vec![BuildStep::command(
"touch",
"/usr/bin/touch",
vec![dir.join("artifact").to_str().unwrap().into()],
grants,
)],
};
let signed =
run_build(&host, &exec, "steward.build", &task, &mut cache, &key, zero_id(), 1_800_000).unwrap();
let s = &signed.receipt.steps[0];
assert_eq!(s.outcome, "Completed");
assert!(dir.join("artifact").exists());
let want = if OsExecutor::confinement().is_some() { Tier::S1 } else { Tier::S0 };
assert_eq!(s.tier, want);
assert!(verify_build_receipt(&signed));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_command_step_seals_the_tier_it_actually_reached() {
use sandbox_exec_wasm::WasmExecutor;
@ -1078,9 +1137,11 @@ mod tests {
// A real command (`true`) is supervised by `ProcessVmm::new` — an
// ordinary child process on the host, not a guest under a VMM. It is
// attested and carries op provenance (the path for non-wasm source
// builds), but the rung it seals is the floor, S0. This crate never
// constructs `supervising_vmm`, so the tier here does not vary with the
// host: a build receipt from this path claims S0 everywhere.
// builds), but the rung it seals is the floor, S0. Its grants withhold
// the clock and randomness, which no operating-system confinement can
// take from a native process, so the confined executor will not run it
// either; and this crate never constructs `supervising_vmm`. So the
// tier here does not vary with the host: S0 everywhere.
let task = BuildTask {
intent: "run the test suite".into(),
steps: vec![BuildStep::command("test", "true", vec![], cpu_grants())

View file

@ -0,0 +1,18 @@
[package]
name = "sandbox-exec-os"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
description = "Sandbox — an S1 executor: a native program confined by the operating system's own sandbox, earning its rung only when a probe on this host shows the confinement denies what the grants do not name."
[dependencies]
sandbox-schema.workspace = true
sandbox-capability.workspace = true
sandbox-meter.workspace = true
sandbox-attest.workspace = true
sandbox-runtime.workspace = true
[dev-dependencies]
ed25519-dalek.workspace = true

View file

@ -0,0 +1,508 @@
//! # sandbox-exec-os — an S1 executor: a native program, confined by the operating system
//!
//! The rungs below S2 had no executor between a bare child process (S0) and
//! a managed runtime (S2, `sandbox-exec-wasm`). This crate runs a **native
//! program** under the operating system's own sandbox, with a policy derived
//! from the unit's grants, and claims **S1** only where it can show the
//! policy holds.
//!
//! ## Why S1, and only S1
//!
//! S1's guarantee (`spec/tiers.md`) is that no resource is reachable without
//! a capability for it. Here the resources are the ones grants govern —
//! storage, network, subprocesses, devices, host calls — and the policy is
//! **deny by default**: the program may read and write exactly the paths its
//! storage grant names, and nothing else; it has no network, cannot spawn a
//! process, and reaches no device beyond the host CPU.
//!
//! S2 is out of reach by construction: it needs a managed runtime with fuel
//! metering, memory caps and no raw syscall surface, and a native process has
//! none of them. So a unit whose grants need S2 — subprocesses (§5.5), a
//! browser (§5.6), a display (§5.7), an accelerator (§5.1) — is refused, never
//! run at a rung it did not reach.
//!
//! ## What the confinement leaves reachable
//!
//! A native process cannot run at all without part of its host, and that
//! part is stated rather than hidden:
//!
//! - the system libraries and frameworks, read-only, and the root directory's
//! own listing (the loader reads it);
//! - `/dev/null`;
//! - the clock, and the system's randomness: no operating-system sandbox can
//! withhold them from a native process.
//!
//! The last point decides which units can be confined honestly. A grant that
//! withholds the clock or randomness (`host.clock` or `host.random` false)
//! asks for something this executor cannot enforce, so such a unit is refused
//! ([`Unenforceable::HostCall`]) rather than run with a capability its grant
//! denies. Nor can it limit egress to named endpoints, so any network grant is
//! refused too; with no network grant the program has no network at all.
//!
//! ## The rung is earned on this host
//!
//! [`OsExecutor::tier`] is S1 only when a probe, run once per process, shows
//! the confinement working here: a program runs under a policy that grants
//! nothing, and is denied a file outside it, a write, a subprocess, and (where
//! `nc` exists) a connection to a listener the probe opened. Anywhere the
//! probe fails, or the platform has no backend, the executor runs the program
//! unconfined and says **S0**, with no mechanism: a mechanism at S0 is
//! incoherent (SPEC §7.9).
//!
//! The backend is macOS's sandbox (Seatbelt), driven through `sandbox-exec`.
//! There is no Linux backend yet. An unprivileged namespace sandbox there
//! would also need a system-call filter to enforce "no subprocess", and the
//! fleet's Linux hosts could not run one to verify it (their user-namespace
//! policy refuses the network namespace). Until one can be verified, Linux is
//! S0.
//!
//! ## What the attestation carries
//!
//! At S1, [`Executor::mechanism`] commits to the runtime (`seatbelt/<macOS
//! version>`) and the exact policy text applied to the last run (SPEC §7.9).
//! Energy is the child's attributed share of the host's CPU reading
//! ([`sandbox_meter::run_attributed`]): `ModelBased` where that reading is a
//! hardware one, `Estimator` otherwise.
use std::path::{Path, PathBuf};
use std::sync::{Mutex, OnceLock};
use sandbox_attest::{DeviceIdentity, Mechanism, Outcome};
use sandbox_capability::Grants;
use sandbox_meter::{DeviceEnergy, PowerMeter, Provenance};
use sandbox_runtime::{ExecError, Executor, RunReport, Unit};
use sandbox_schema::{DeviceClass, Tier};
/// The program that applies a Seatbelt policy and then runs the command.
const SANDBOX_EXEC: &str = "/usr/bin/sandbox-exec";
/// Why a unit's grants cannot be enforced by this executor. Each is a
/// refusal: the unit is never run with a capability its grants deny, or at a
/// rung below the one its grants need.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Unenforceable {
/// An accelerator class: reaching one needs a brokered device interface
/// (S2) or device assignment (S5).
Device(DeviceClass),
/// Network egress or ingress: the confinement cannot limit traffic to the
/// granted endpoints, only deny it all.
Network,
/// Subprocesses: spawning is untrusted-execution class (S2, SPEC §5.5).
Process,
/// Browser automation (S2, SPEC §5.6).
Browser,
/// A framebuffer or desktop (S2, SPEC §5.7).
Display,
/// A key-value store: reached through host calls this executor does not
/// broker.
Kv,
/// A host call the grant withholds and a native process holds anyway
/// (`"clock"`, `"random"`), or one it grants and this executor cannot
/// provide (`"inference"`).
HostCall(&'static str),
/// A storage path that is relative, cannot be resolved, or cannot be
/// written into a policy safely.
Path(String),
/// The program cannot be found as an executable file.
Program(String),
}
impl std::fmt::Display for Unenforceable {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Device(c) => write!(f, "device class {c:?} needs a brokered device interface"),
Self::Network => write!(f, "network grants cannot be limited to the granted endpoints"),
Self::Process => write!(f, "spawning subprocesses needs a brokered boundary (S2)"),
Self::Browser => write!(f, "browser automation needs S2"),
Self::Display => write!(f, "display access needs S2"),
Self::Kv => write!(f, "key-value access needs a brokered host call"),
Self::HostCall(c) => write!(f, "host call {c:?} cannot be enforced for a native process"),
Self::Path(p) => write!(f, "storage path {p:?} cannot be confined"),
Self::Program(p) => write!(f, "program {p:?} is not an executable file"),
}
}
}
/// The operating-system confinement this host offers, as the probe found it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Confinement {
/// The runtime, name and version: `seatbelt/<macOS version>`.
pub runtime: String,
}
/// A native program run under the operating system's sandbox (see the crate
/// docs). The program and its arguments are the executor's; the unit
/// supplies the grants the policy is derived from.
pub struct OsExecutor {
program: String,
args: Vec<String>,
cwd: PathBuf,
/// The policy applied to the last confined run, for [`Executor::mechanism`].
last_profile: Mutex<Option<String>>,
}
impl OsExecutor {
/// Run `program args` in `cwd`.
pub fn new(program: impl Into<String>, args: Vec<String>, cwd: impl Into<PathBuf>) -> Self {
OsExecutor { program: program.into(), args, cwd: cwd.into(), last_profile: Mutex::new(None) }
}
/// The confinement this host offers, when the probe showed it working;
/// `None` elsewhere. Probed once per process.
pub fn confinement() -> Option<&'static Confinement> {
static PROBED: OnceLock<Option<Confinement>> = OnceLock::new();
PROBED.get_or_init(probe).as_ref()
}
/// Whether this executor can enforce `grants`, and if so the policy it
/// would apply. Pure: it reads the file system to resolve paths but runs
/// nothing.
pub fn policy(&self, grants: &Grants) -> Result<String, Unenforceable> {
if let Some(c) = grants.compute.devices.iter().find(|d| !d.is_host_cpu()) {
return Err(Unenforceable::Device(*c));
}
if !grants.network.outbound.is_empty() || grants.network.inbound || !grants.network.credentials.is_empty() {
return Err(Unenforceable::Network);
}
if grants.process.exec || !grants.process.programs.is_empty() {
return Err(Unenforceable::Process);
}
if grants.browser.enabled {
return Err(Unenforceable::Browser);
}
if grants.display.enabled {
return Err(Unenforceable::Display);
}
if grants.kv.read || grants.kv.write {
return Err(Unenforceable::Kv);
}
if !grants.host.clock {
return Err(Unenforceable::HostCall("clock"));
}
if !grants.host.random {
return Err(Unenforceable::HostCall("random"));
}
if grants.host.inference {
return Err(Unenforceable::HostCall("inference"));
}
let program = resolve_program(&self.program).ok_or_else(|| Unenforceable::Program(self.program.clone()))?;
let mut reads = Vec::new();
for p in &grants.storage.read {
reads.push(policy_path(p)?);
}
let mut writes = Vec::new();
for p in grants.storage.write.iter().chain(&grants.storage.delete) {
writes.push(policy_path(p)?);
}
Ok(seatbelt_policy(&program, &reads, &writes))
}
/// Run `program args` with `policy` applied (or unconfined when `None`),
/// metered.
fn run_metered(&self, policy: Option<&str>) -> Result<(bool, sandbox_meter::Attribution), ExecError> {
let mut meter = PowerMeter::detect();
let mut argv: Vec<&str> = Vec::new();
let program = match policy {
Some(p) => {
argv.extend(["-p", p, self.program.as_str()]);
SANDBOX_EXEC
}
None => self.program.as_str(),
};
argv.extend(self.args.iter().map(String::as_str));
let (ok, _output, attribution) = sandbox_meter::run_attributed(&mut meter, program, &argv, &self.cwd)
.map_err(|e| ExecError::Unavailable(format!("spawn {program}: {e}")))?;
Ok((ok, attribution))
}
}
impl Executor for OsExecutor {
/// S1 where the probe showed the confinement working on this host; S0
/// everywhere else. Never asserted by a caller.
fn tier(&self) -> Tier {
if Self::confinement().is_some() {
Tier::S1
} else {
Tier::S0
}
}
fn run(&self, unit: &Unit) -> Result<RunReport, ExecError> {
let confined = Self::confinement();
let policy = match confined {
Some(_) => {
let policy = self.policy(unit.grants).map_err(|e| {
ExecError::Unavailable(format!("{e}: this executor will not run the unit at S1"))
})?;
*self.last_profile.lock().expect("profile lock") = Some(policy.clone());
Some(policy)
}
None => {
*self.last_profile.lock().expect("profile lock") = None;
None
}
};
let (ok, a) = self.run_metered(policy.as_deref())?;
// The child's attributed share of the host's CPU reading: a model
// over a hardware reading where there is one, an estimate otherwise.
let energy = DeviceEnergy {
class: DeviceClass::Cpu,
measured_uj: None,
estimated_uj: a.attributed_uj,
provenance: if a.attributed { Provenance::ModelBased } else { Provenance::Estimator },
};
Ok(RunReport {
device_energy: vec![energy],
device_identity: vec![DeviceIdentity {
class: DeviceClass::Cpu,
model: "host".into(),
driver: confined.map_or_else(|| "process".to_string(), |c| c.runtime.clone()),
}],
outcome: if ok { Outcome::Completed } else { Outcome::Failed },
journal: None,
storage: None,
})
}
/// At S1, the runtime and the policy applied to the last run; `None` at
/// S0 and before any confined run.
fn mechanism(&self) -> Option<Mechanism> {
let c = Self::confinement()?;
let policy = self.last_profile.lock().expect("profile lock").clone()?;
Some(Mechanism::commit(c.runtime.clone(), policy.as_bytes(), &[]))
}
}
/// `program` as the absolute, symlink-resolved path the policy names: as
/// given when it contains a `/`, else the first executable file of that name
/// on `PATH`.
fn resolve_program(program: &str) -> Option<String> {
let found = if program.contains('/') {
Some(PathBuf::from(program))
} else {
std::env::var_os("PATH")
.into_iter()
.flat_map(|p| std::env::split_paths(&p).collect::<Vec<_>>())
.map(|d| d.join(program))
.find(|c| c.is_file())
}?;
let real = found.canonicalize().ok()?;
let s = real.to_str()?.to_string();
policy_safe(&s).then_some(s)
}
/// A storage path as the policy names it: absolute, symlinks resolved (the
/// sandbox matches resolved paths, so `/tmp` must become `/private/tmp`). A
/// path that does not exist yet resolves through its parent.
fn policy_path(p: &str) -> Result<String, Unenforceable> {
let path = Path::new(p);
if !path.is_absolute() {
return Err(Unenforceable::Path(p.into()));
}
let resolved = match path.canonicalize() {
Ok(r) => r,
Err(_) => {
let parent = path.parent().and_then(|d| d.canonicalize().ok());
match (parent, path.file_name()) {
(Some(d), Some(name)) => d.join(name),
_ => return Err(Unenforceable::Path(p.into())),
}
}
};
match resolved.to_str() {
Some(s) if policy_safe(s) => Ok(s.to_string()),
_ => Err(Unenforceable::Path(p.into())),
}
}
/// Whether `s` can be written into a policy string literal as it is: no quote
/// or backslash to escape, no control character.
fn policy_safe(s: &str) -> bool {
!s.chars().any(|c| c == '"' || c == '\\' || c.is_control())
}
/// The Seatbelt policy for `program` reading `reads` and writing `writes`:
/// deny by default, then the substrate a native process needs (see the crate
/// docs), then exactly the granted paths.
fn seatbelt_policy(program: &str, reads: &[String], writes: &[String]) -> String {
let mut p = String::from("(version 1)\n(deny default)\n");
p.push_str(&format!("(allow process-exec (literal \"{program}\"))\n"));
p.push_str(&format!(
"(allow file-read* (literal \"/\") (literal \"/dev/null\") (subpath \"/usr/lib\") (subpath \"/System\") (literal \"{program}\"))\n"
));
p.push_str("(allow sysctl-read)\n");
for r in reads {
p.push_str(&format!("(allow file-read* (subpath \"{r}\"))\n"));
}
for w in writes {
p.push_str(&format!("(allow file-read* file-write* (subpath \"{w}\"))\n"));
}
p
}
/// Whether Seatbelt confines a program here, shown rather than assumed: under
/// a policy that grants nothing, a control run must succeed, and a read
/// outside the policy, a write, a subprocess and (where `nc` exists) a
/// connection to a listener this probe opened must each fail.
#[cfg(target_os = "macos")]
fn probe() -> Option<Confinement> {
use std::process::{Command, Stdio};
if !Path::new(SANDBOX_EXEC).is_file() {
return None;
}
let version = Command::new("/usr/bin/sw_vers").arg("-productVersion").output().ok()?;
let version = String::from_utf8(version.stdout).ok()?.trim().to_string();
if version.is_empty() {
return None;
}
let dir = std::env::temp_dir().join(format!("sandbox-exec-os-probe-{}", std::process::id()));
std::fs::create_dir_all(&dir).ok()?;
let secret = dir.join("secret");
std::fs::write(&secret, b"not granted").ok()?;
let written = dir.join("written");
let run = |program: &str, args: &[&str]| -> Option<bool> {
let policy = seatbelt_policy(&resolve_program(program)?, &[], &[]);
let status = Command::new(SANDBOX_EXEC)
.arg("-p")
.arg(&policy)
.arg(program)
.args(args)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.ok()?;
Some(status.success())
};
let secret_s = secret.to_str()?;
let written_s = written.to_str()?;
let holds = run("/bin/cat", &["/dev/null"]) == Some(true)
&& run("/bin/cat", &[secret_s]) == Some(false)
&& run("/usr/bin/touch", &[written_s]) == Some(false)
&& !written.exists()
&& run("/bin/sh", &["-c", "/usr/bin/true; /usr/bin/true"]) == Some(false)
&& network_denied(&run);
let _ = std::fs::remove_dir_all(&dir);
holds.then(|| Confinement { runtime: format!("seatbelt/{version}") })
}
/// Where `nc` exists: a connection from inside the policy to a listener on
/// this host's loopback must fail, and the listener must see nothing.
#[cfg(target_os = "macos")]
fn network_denied(run: &dyn Fn(&str, &[&str]) -> Option<bool>) -> bool {
use std::net::TcpListener;
if !Path::new("/usr/bin/nc").is_file() {
return true;
}
let Ok(listener) = TcpListener::bind("127.0.0.1:0") else { return false };
let Ok(addr) = listener.local_addr() else { return false };
if listener.set_nonblocking(true).is_err() {
return false;
}
let port = addr.port().to_string();
let refused = run("/usr/bin/nc", &["-z", "-G", "2", "127.0.0.1", &port]) == Some(false);
refused && listener.accept().is_err()
}
/// No backend off macOS: the executor is S0 (see the crate docs).
#[cfg(not(target_os = "macos"))]
fn probe() -> Option<Confinement> {
None
}
#[cfg(test)]
mod tests {
use super::*;
use sandbox_capability::{BrowserGrant, ComputeGrant, DisplayGrant, HostGrant, NetworkGrant, ProcessGrant};
/// Grants this executor can enforce: CPU, the clock and randomness, and
/// the given storage paths.
fn enforceable(read: Vec<String>, write: Vec<String>) -> Grants {
let mut g = Grants {
compute: ComputeGrant::cpu_only(5_000_000, 200, 16 * 1024 * 1024),
host: HostGrant { logging: true, clock: true, random: true, inference: false },
..Grants::default()
};
g.storage.read = read;
g.storage.write = write;
g
}
fn executor() -> OsExecutor {
OsExecutor::new("/bin/cat", vec![], std::env::temp_dir())
}
/// Every grant the executor cannot enforce is refused, by name.
#[test]
fn grants_it_cannot_enforce_are_refused() {
let ok = enforceable(vec![], vec![]);
assert!(executor().policy(&ok).is_ok());
let cases: Vec<(Grants, Unenforceable)> = vec![
(
Grants { network: NetworkGrant { outbound: vec!["example:443".into()], ..ok.network.clone() }, ..ok.clone() },
Unenforceable::Network,
),
(
Grants { process: ProcessGrant { exec: true, ..ok.process.clone() }, ..ok.clone() },
Unenforceable::Process,
),
(Grants { browser: BrowserGrant { enabled: true, ..ok.browser.clone() }, ..ok.clone() }, Unenforceable::Browser),
(Grants { display: DisplayGrant { enabled: true, ..ok.display }, ..ok.clone() }, Unenforceable::Display),
(Grants { host: HostGrant { clock: false, ..ok.host }, ..ok.clone() }, Unenforceable::HostCall("clock")),
(Grants { host: HostGrant { random: false, ..ok.host }, ..ok.clone() }, Unenforceable::HostCall("random")),
(enforceable(vec!["relative/path".into()], vec![]), Unenforceable::Path("relative/path".into())),
(enforceable(vec![], vec!["/tmp/a\"b".into()]), Unenforceable::Path("/tmp/a\"b".into())),
];
for (grants, want) in cases {
assert_eq!(executor().policy(&grants).err(), Some(want));
}
let mut gpu = ok.clone();
gpu.compute.devices.push(DeviceClass::Gpu);
assert_eq!(executor().policy(&gpu).err(), Some(Unenforceable::Device(DeviceClass::Gpu)));
assert!(matches!(
OsExecutor::new("no-such-program-anywhere", vec![], "/").policy(&ok),
Err(Unenforceable::Program(_))
));
}
/// The policy denies by default and then names exactly the program and
/// the granted paths, resolved; it never allows the network or a fork.
#[test]
fn the_policy_names_exactly_what_was_granted() {
let tmp = std::env::temp_dir().canonicalize().unwrap();
let policy = executor()
.policy(&enforceable(vec![tmp.to_str().unwrap().into()], vec![tmp.join("out").to_str().unwrap().into()]))
.unwrap();
assert!(policy.starts_with("(version 1)\n(deny default)\n"), "{policy}");
assert!(policy.contains(&format!("(allow file-read* (subpath \"{}\"))", tmp.display())), "{policy}");
assert!(policy.contains(&format!("(allow file-read* file-write* (subpath \"{}\"))", tmp.join("out").display())), "{policy}");
assert!(policy.contains("(allow process-exec (literal \""), "{policy}");
for never in ["network", "process-fork", "mach-lookup"] {
assert!(!policy.contains(never), "{never} in {policy}");
}
}
/// The rung matches the probe: S1 with a confinement here, S0 with no
/// mechanism where there is none. A host that is expected to confine
/// (`SANDBOX_EXEC_OS_EXPECT_S1`, set by the macOS CI job) fails here when
/// the probe does not hold, rather than passing on the S0 branch.
#[test]
fn the_rung_is_the_probes() {
if std::env::var_os("SANDBOX_EXEC_OS_EXPECT_S1").is_some() {
assert!(OsExecutor::confinement().is_some(), "this host is expected to confine, and the probe did not hold");
}
let e = executor();
match OsExecutor::confinement() {
Some(c) => {
eprintln!("sandbox-exec-os: confinement {} -> S1", c.runtime);
assert_eq!(e.tier(), Tier::S1);
assert!(c.runtime.starts_with("seatbelt/"), "{c:?}");
}
None => {
eprintln!("sandbox-exec-os: no confinement on this host -> S0");
assert_eq!(e.tier(), Tier::S0);
assert_eq!(e.mechanism(), None);
}
}
}
}

View file

@ -0,0 +1,134 @@
//! The executor end to end on this host: what it lets through, what it
//! denies, the mechanism it commits to, and the rung `Host::run_and_seal`
//! seals. Where the probe finds no confinement (any host but macOS, or a
//! macOS host whose sandbox did not hold), the same tests assert the S0 path
//! instead, so neither branch passes by skipping.
use ed25519_dalek::SigningKey;
use sandbox_attest::{Mechanism, Outcome};
use sandbox_capability::{ComputeGrant, Grants, HostGrant};
use sandbox_exec_os::OsExecutor;
use sandbox_runtime::{Executor, Host, RunError, TierRequest, Unit};
use sandbox_schema::Tier;
use std::path::{Path, PathBuf};
/// A scratch directory of its own for each test.
fn scratch(name: &str) -> PathBuf {
let d = std::env::temp_dir().join(format!("sandbox-exec-os-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).unwrap();
d.canonicalize().unwrap()
}
fn grants(read: &Path, write: &Path) -> Grants {
let mut g = Grants {
compute: ComputeGrant::cpu_only(5_000_000, 200, 16 * 1024 * 1024),
host: HostGrant { logging: true, clock: true, random: true, inference: false },
..Grants::default()
};
g.storage.read = vec![read.to_str().unwrap().into()];
g.storage.write = vec![write.to_str().unwrap().into()];
g
}
/// A granted read and a granted write go through; a file outside the grants
/// does not, and the run that tried says so. At S1 the mechanism commits to
/// the runtime and the very policy applied.
#[test]
fn it_reads_and_writes_what_was_granted_and_nothing_else() {
let dir = scratch("rw");
let (input, output, secret) = (dir.join("in"), dir.join("out"), dir.join("secret"));
std::fs::create_dir_all(&input).unwrap();
std::fs::create_dir_all(&output).unwrap();
std::fs::create_dir_all(&secret).unwrap();
std::fs::write(input.join("a.txt"), b"granted").unwrap();
std::fs::write(secret.join("s.txt"), b"not granted").unwrap();
let g = grants(&input, &output);
let unit = Unit::new(b"cp", &g);
let copy = OsExecutor::new(
"/bin/cp",
vec![input.join("a.txt").to_str().unwrap().into(), output.join("b.txt").to_str().unwrap().into()],
&dir,
);
let report = copy.run(&unit).expect("a run with enforceable grants");
assert_eq!(report.outcome, Outcome::Completed);
assert_eq!(std::fs::read(output.join("b.txt")).unwrap(), b"granted");
let peek = OsExecutor::new("/bin/cat", vec![secret.join("s.txt").to_str().unwrap().into()], &dir);
let report = peek.run(&unit).expect("the run itself starts");
match OsExecutor::confinement() {
Some(c) => {
eprintln!("sandbox-exec-os: {} denied a read outside the grants", c.runtime);
assert_eq!(report.outcome, Outcome::Failed, "a file outside the grants was read");
let policy = peek.policy(&g).unwrap();
let m = peek.mechanism().expect("a confined run commits to its mechanism");
assert_eq!(m, Mechanism::commit(c.runtime.clone(), policy.as_bytes(), &[]));
assert!(m.runtime.starts_with("seatbelt/"));
}
None => {
eprintln!("sandbox-exec-os: no confinement here; the run is S0 and unconfined");
assert_eq!(report.outcome, Outcome::Completed, "unconfined, the read succeeds");
assert_eq!(peek.mechanism(), None, "no mechanism at S0");
}
}
let _ = std::fs::remove_dir_all(&dir);
}
/// The canonical host path: a trusted unit that writes storage needs exactly
/// S1. Where the confinement holds, the run is sealed at S1 with its
/// mechanism; where it does not, the host refuses to seal it at all.
#[test]
fn run_and_seal_seals_s1_only_where_the_confinement_holds() {
let dir = scratch("seal");
let (input, output) = (dir.join("in"), dir.join("out"));
std::fs::create_dir_all(&input).unwrap();
std::fs::create_dir_all(&output).unwrap();
std::fs::write(input.join("a.txt"), b"granted").unwrap();
let g = grants(&input, &output);
let unit = Unit::new(b"cp", &g);
let exec = OsExecutor::new(
"/bin/cp",
vec![input.join("a.txt").to_str().unwrap().into(), output.join("b.txt").to_str().unwrap().into()],
&dir,
);
let supervisor = SigningKey::from_bytes(&[42u8; 32]);
let host = Host::new("sandbox-exec-os", "steward.os", SigningKey::from_bytes(&[41u8; 32]), supervisor.clone());
let req = TierRequest { trusted: true, ..TierRequest::minimal(&g) };
let sealed = host.run_and_seal(&exec, &unit, &req, 1, [0; 8], 1, 2);
match OsExecutor::confinement() {
Some(_) => {
let sealed = sealed.expect("sealed at S1");
assert_eq!(sealed.attestation.tier, Tier::S1);
assert!(sealed.attestation.mechanism.is_some(), "an S1 run carries its mechanism");
sealed.attestation.verify(&sealed.attestation_sig, &supervisor.verifying_key()).unwrap();
}
None => {
assert!(
matches!(sealed, Err(RunError::TierMismatch { .. })),
"an S0 executor must not be sealed for a unit that needs S1"
);
}
}
let _ = std::fs::remove_dir_all(&dir);
}
/// Where the confinement holds, a unit whose grants it cannot enforce is
/// refused before anything runs, rather than run with a capability its grant
/// denies.
#[test]
fn a_unit_it_cannot_confine_is_refused_before_it_runs() {
if OsExecutor::confinement().is_none() {
eprintln!("sandbox-exec-os: no confinement here; nothing to refuse");
return;
}
let dir = scratch("refuse");
let marker = dir.join("ran");
let mut g = grants(&dir, &dir);
g.host.clock = false;
let unit = Unit::new(b"touch", &g);
let exec = OsExecutor::new("/usr/bin/touch", vec![marker.to_str().unwrap().into()], &dir);
assert!(exec.run(&unit).is_err(), "a grant withholding the clock cannot be enforced natively");
assert!(!marker.exists(), "the refused unit ran");
let _ = std::fs::remove_dir_all(&dir);
}