All checks were successful
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 4s
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 58s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 1m54s
standards workspaces build + test / joulecontract-rs (joulecontract/joulecontract-rs, joulecontract-rs, linux-arm64, true) (pull_request) Successful in 57s
standards workspaces build + test / jouleclaw-rs (-nodebug, jouleclaw/jouleclaw-rs, -p jouleclaw-energy --features rapl,nvml, jouleclaw-rs, linux-arm64, -C target-feature=+fp16) (pull_request) Successful in 3m1s
standards workspaces build + test / aac-rs (aac-conformance, aac/aac-rs, aac-rs, linux-arm64) (pull_request) Successful in 3m15s
standards workspaces build + test / map-rs (map-conformance, map/map-rs, map-rs, linux-arm64) (pull_request) Successful in 1m1s
standards workspaces build + test / mesh-rs (mesh/mesh-rs, -p mesh-engine-gguf -p mesh-openai --features mesh-engine-gguf/gguf,mesh-openai/server, mesh-rs, linux-arm64) (pull_request) Successful in 57s
standards workspaces build + test / openpay (openpay, openpay, linux-arm64) (pull_request) Successful in 3m28s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 5m25s
standards workspaces build + test / jouledesk-rs (jouledesk/jouledesk-rs, jouledesk-rs, macos) (pull_request) Successful in 52s
standards workspaces build + test / arl-rs (arl/arl-rs, arl-rs, linux) (pull_request) Successful in 1m13s
standards workspaces build + test / jcp-rs (jcp-conformance, jcp/jcp-rs, -p jcp-energy --features nvml, jcp-rs, linux) (pull_request) Successful in 1m46s
standards workspaces build + test / eoc-rs (eoc/eoc-rs, -p eoc-meter --features cuda, eoc-rs, linux) (pull_request) Successful in 2m7s
standards workspaces build + test / joule-ui-rs (joule-ui/joule-ui-rs, joule-ui-rs, linux) (pull_request) Successful in 1m12s
standards workspaces build + test / joulehook-rs (joulehook/joulehook-rs, joulehook-rs, linux) (pull_request) Successful in 1m10s
standards workspaces build + test / proof-rs (proof-conformance, proof/proof-rs, proof-rs, linux, true) (pull_request) Successful in 1m32s
standards workspaces build + test / sandbox-rs (sandbox-conformance, sandbox/sandbox-rs, sandbox-rs, linux) (pull_request) Successful in 2m23s
standards workspaces build + test / sense-rs (sense/sense-rs, sense-rs, linux) (pull_request) Successful in 1m10s
standards workspaces build + test / smart-byte-rs (smart-byte/smart-byte-rs, smart-byte-rs, linux) (pull_request) Successful in 4m12s
The ladder had no executor between a bare child process (S0) and a managed runtime (S2). `sandbox-exec-os` runs a native program under the operating system's sandbox, with a deny-by-default policy derived from the unit's grants: - the program may read and write exactly the granted storage paths; - it has no network; - it cannot spawn a process; - it reaches no device beyond the host CPU. Why S1 and no more. S1's guarantee is that no resource is reachable without a capability, and that holds for every resource grants govern. A native process holds a stated residue: the system libraries read-only, the root directory's listing, /dev/null, the clock and randomness. Because the clock and randomness cannot be withheld, a grant that withholds either is refused, never run with a capability it denies. So are network grants (egress cannot be limited to named endpoints) and everything that needs S2: subprocesses, browser, display, accelerators, key-value store. S2 itself needs fuel and memory caps, which a native process cannot have. The rung is earned per host. A probe, run once per process, runs a program under a policy that grants nothing. It must succeed at a control run, and be denied a file outside the policy, a write, a subprocess and a loopback connection to a listener the probe opened. Only then is the tier S1, with SPEC §7.9's mechanism: `seatbelt/<macOS version>` and a commitment to the exact policy applied. Anywhere else the program runs unconfined at S0 with no mechanism. Linux is S0 for now: a namespace sandbox there also needs a system-call filter to enforce "no subprocess", and the fleet's Linux hosts refuse the network namespace, so it could not be verified. Energy is the child's share of the host's CPU reading (`run_attributed`): ModelBased where that reading is a hardware one, Estimator otherwise. sandbox-build: a command step runs confined at S1 where the confinement holds and can enforce the step's grants, sealed with its mechanism. Any other step falls back to the existing unconfined child process (S0), so today's steps, whose grants withhold the clock, are unchanged. CI: the macOS energy job runs sandbox-exec-os and fails unless the S1 branch ran (the probe's confinement line and a denied read). Every other runner takes the S0 branch, which the same tests assert. Checked on the build host (macOS 27.0.1): `seatbelt/27.0.1` probes S1. A granted copy succeeds, a read outside the grants is denied, run_and_seal seals S1 with the mechanism, and a unit withholding the clock is refused before it runs. The sandbox-rs workspace passes 407 tests, conformance passes all classes, and clippy is clean on both crates. Co-Authored-By: Claude Opus 5.5 <[email protected]>
134 lines
5.9 KiB
Rust
134 lines
5.9 KiB
Rust
//! 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);
|
|
}
|