[pitboss] sweep after phase 19: 3 deferred items resolved

This commit is contained in:
pitboss 2026-05-15 11:28:47 -05:00
parent 7ca0c053f5
commit 1d9b4c688f
12 changed files with 204 additions and 180 deletions

View file

@ -402,6 +402,7 @@ mod tests {
oob_callback_seen: false,
sink_hit: false,
duration: Duration::from_millis(1),
hardening_outcome: None,
}
}

View file

@ -406,6 +406,7 @@ mod tests {
oob_callback_seen: false,
sink_hit: true,
duration: Duration::from_millis(250),
hardening_outcome: None,
}
}

View file

@ -84,11 +84,27 @@ pub fn ensure_image_pulled(image: &str) -> bool {
if let Some(entry) = cache.get(image) {
return *entry;
}
let ok = docker_pull(image);
// Fast path: a prior `docker pull` (often by an earlier nextest binary in
// the same machine) may already have the image locally. `docker image
// inspect` is a no-network lookup against the local daemon — when it
// succeeds we can skip the network pull entirely. When it fails we fall
// through to `docker pull` so registry-side rotations / first-time runs
// still settle.
let ok = if docker_image_present(image) { true } else { docker_pull(image) };
cache.insert(image.to_owned(), ok);
ok
}
fn docker_image_present(image: &str) -> bool {
Command::new(docker_bin())
.args(["image", "inspect", image])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
fn docker_pull(image: &str) -> bool {
Command::new(docker_bin())
.args(["pull", image])

View file

@ -40,6 +40,29 @@ pub use process_linux::{HardeningLevel, HardeningOutcome};
#[cfg(target_os = "macos")]
pub mod process_macos;
/// Phase 17 (Track E.1) + Phase 18 (Track E.2) per-run hardening outcome.
///
/// Returned by [`run_process`] on the [`SandboxOutcome`] so callers (tests +
/// telemetry) can inspect the per-primitive status without consulting a
/// process-global singleton. The previous Phase 17/18 implementation kept
/// the outcome in `process_linux::LAST_OUTCOME` / `process_macos::LAST_OUTCOME`
/// statics; that worked under nextest's per-test process isolation but would
/// race the moment `verify_finding` ran under `rayon::par_iter`.
///
/// The enum is platform-cfg'd because the Linux and macOS backends record
/// different shapes: Linux captures per-primitive `PrimitiveStatus` for
/// `prctl` / `rlimit` / `unshare` / `chroot` / `seccomp`; macOS captures a
/// coarser `level + profile` pair after the `sandbox-exec` wrap decision.
/// On other targets the enum has no constructible variants, so
/// `Option<HardeningRecord>` is always `None`.
#[derive(Debug, Clone)]
pub enum HardeningRecord {
#[cfg(target_os = "linux")]
Linux(process_linux::HardeningOutcome),
#[cfg(target_os = "macos")]
Macos(process_macos::HardeningOutcome),
}
/// Phase 19 (Track E.3) — pinned-digest docker backend helpers.
///
/// The functions in this module resolve [`crate::dynamic::toolchain::
@ -140,6 +163,11 @@ pub struct SandboxOutcome {
pub sink_hit: bool,
/// Wall-clock duration of the run.
pub duration: Duration,
/// Phase 17/18 hardening outcome captured by the process backend.
/// `None` when the run did not exercise a hardening path (docker
/// backend, non-Linux/non-macOS host, or `ProcessHardeningProfile`
/// of `Standard` with no primitive outcome to record).
pub hardening_outcome: Option<HardeningRecord>,
}
#[derive(Debug, Clone)]
@ -1001,6 +1029,7 @@ fn exec_in_container(
oob_callback_seen: false,
sink_hit,
duration,
hardening_outcome: None,
})
}
@ -1218,6 +1247,7 @@ fn exec_native_binary_in_container(
oob_callback_seen: false,
sink_hit,
duration,
hardening_outcome: None,
})
}
@ -1260,21 +1290,22 @@ fn run_process(
// Phase 18 (Track E.2): on macOS, wrap the command with
// `sandbox-exec -f <profile> -D WORKDIR=<workdir> ...` so per-cap
// policies confine the harness. When `sandbox-exec` is missing or
// the wrap setup fails, `wrap_plan` returns `None` and we fall
// back to the unwrapped command; the verifier reads back the
// recorded [`process_macos::HardeningLevel::Trusted`] outcome and
// downgrades filesystem-oracle verdicts to
// the wrap setup fails, `wrap_plan` returns `plan = None` and we
// fall back to the unwrapped command; the verifier reads back the
// returned [`process_macos::HardeningLevel::Trusted`] outcome via
// [`SandboxOutcome::hardening_outcome`] and downgrades filesystem-
// oracle verdicts to
// [`crate::evidence::InconclusiveReason::BackendInsufficient`].
#[cfg(target_os = "macos")]
let macos_wrap = {
if matches!(opts.process_hardening, ProcessHardeningProfile::Strict) {
process_macos::wrap_plan(&process_macos::WrapInput {
Some(process_macos::wrap_plan(&process_macos::WrapInput {
cmd_path: &resolved_cmd_path,
cmd_args: &harness.command[1..],
workdir: &harness.workdir,
caps: opts.seccomp_caps,
profile_override: None,
})
}))
} else {
None
}
@ -1282,7 +1313,7 @@ fn run_process(
#[cfg(target_os = "macos")]
let (effective_cmd_path, effective_cmd_args): (std::path::PathBuf, Vec<String>) =
match &macos_wrap {
match macos_wrap.as_ref().and_then(|w| w.plan.as_ref()) {
Some(plan) => (plan.binary.clone(), plan.args.clone()),
None => (resolved_cmd_path.clone(), harness.command[1..].to_vec()),
};
@ -1405,13 +1436,12 @@ fn run_process(
let status = child.wait().map_err(SandboxError::Io)?;
// Phase 17 (Track E.1): wait for the per-primitive HardeningOutcome
// drain thread before returning so callers (tests + telemetry) read
// a settled value via `process_linux::last_hardening_outcome()`.
// Phase 17 (Track E.1): drain the per-primitive HardeningOutcome
// off the pre_exec status pipe before returning so the caller sees
// the settled value on `SandboxOutcome::hardening_outcome` instead
// of consulting a process-global singleton.
#[cfg(target_os = "linux")]
if let Some(joiner) = outcome_joiner {
joiner.await_outcome();
}
let linux_outcome = outcome_joiner.and_then(|j| j.await_outcome());
let stdout_buf = stdout_handle
.and_then(|h| h.join().ok())
@ -1431,6 +1461,13 @@ fn run_process(
let sink_hit = contains_subslice(&stdout_buf, SINK_HIT_SENTINEL)
|| contains_subslice(&stderr_buf, SINK_HIT_SENTINEL);
#[cfg(target_os = "linux")]
let hardening_outcome = linux_outcome.map(HardeningRecord::Linux);
#[cfg(target_os = "macos")]
let hardening_outcome = macos_wrap.map(|w| HardeningRecord::Macos(w.outcome));
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
let hardening_outcome: Option<HardeningRecord> = None;
Ok(SandboxOutcome {
exit_code,
stdout: stdout_buf,
@ -1439,6 +1476,7 @@ fn run_process(
oob_callback_seen: false,
sink_hit,
duration,
hardening_outcome,
})
}
@ -1570,6 +1608,7 @@ mod tests {
oob_callback_seen: false,
sink_hit: false,
duration: Duration::from_millis(10),
hardening_outcome: None,
};
const SENTINEL: &[u8] = b"__NYX_SINK_HIT__";
outcome.sink_hit = contains_subslice(&outcome.stdout, SENTINEL);
@ -1586,6 +1625,7 @@ mod tests {
oob_callback_seen: false,
sink_hit: false,
duration: Duration::from_millis(10),
hardening_outcome: None,
};
assert!(!outcome.sink_hit);
}

View file

@ -37,7 +37,7 @@ use std::os::unix::io::{FromRawFd, RawFd};
use std::os::unix::process::CommandExt;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::{Arc, Mutex, OnceLock};
use std::sync::Arc;
// ── HardeningLevel reporting ─────────────────────────────────────────────────
@ -129,36 +129,6 @@ impl HardeningOutcome {
}
}
// ── Last outcome registry (read back by tests + telemetry) ───────────────────
static LAST_OUTCOME: OnceLock<Mutex<Option<HardeningOutcome>>> = OnceLock::new();
fn outcome_cell() -> &'static Mutex<Option<HardeningOutcome>> {
LAST_OUTCOME.get_or_init(|| Mutex::new(None))
}
fn record_outcome(outcome: HardeningOutcome) {
if let Ok(mut g) = outcome_cell().lock() {
*g = Some(outcome);
}
}
/// Snapshot of the most-recent hardening outcome. Returns `None` until
/// at least one [`install_pre_exec`] child has been spawned and waited
/// on. Tests + telemetry read this after `wait_for_outcome` to get the
/// per-primitive status table.
pub fn last_hardening_outcome() -> Option<HardeningOutcome> {
outcome_cell().lock().ok().and_then(|g| *g)
}
/// Reset the last-outcome slot. Tests use this between cases so a stale
/// value from a prior spawn cannot leak into the assertion under test.
pub fn reset_last_hardening_outcome() {
if let Ok(mut g) = outcome_cell().lock() {
*g = None;
}
}
// ── Status pipe between parent and child ─────────────────────────────────────
struct StatusPipe {
@ -389,20 +359,23 @@ pub struct OutcomeCollector {
}
/// Background-drain handle returned by [`OutcomeCollector::after_spawn`].
/// `run_process` awaits this after `child.wait()` so the outcome is
/// guaranteed to be in the registry before the function returns; tests
/// that bypass `run_process` can call [`OutcomeJoiner::await_outcome`]
/// themselves.
/// `run_process` awaits this after `child.wait()`, receiving the per-
/// primitive [`HardeningOutcome`] the drain thread parsed off the
/// status pipe. Each spawn gets its own joiner, so the outcome flows
/// back to exactly the caller that spawned it — no process-global
/// singleton, no race when `verify_finding` runs under
/// `rayon::par_iter`.
pub struct OutcomeJoiner {
handle: Option<std::thread::JoinHandle<()>>,
handle: Option<std::thread::JoinHandle<Option<HardeningOutcome>>>,
}
impl OutcomeJoiner {
/// Block until the drain thread finishes recording the outcome.
pub fn await_outcome(mut self) {
if let Some(h) = self.handle.take() {
let _ = h.join();
}
/// Block until the drain thread finishes, returning the per-
/// primitive outcome it parsed. `None` when the status pipe was
/// drained but the wire record was truncated (rare: child died
/// before `pre_exec` could write).
pub fn await_outcome(mut self) -> Option<HardeningOutcome> {
self.handle.take().and_then(|h| h.join().ok().flatten())
}
}
@ -419,16 +392,12 @@ impl OutcomeCollector {
/// of the write fd so the kernel ref-count drops to whatever the
/// child is still holding; once execve(2) closes the child's
/// O_CLOEXEC copy too, the read end sees EOF and the drain thread
/// records the outcome via [`record_outcome`]. Returns a join
/// handle the caller can await to know the outcome is settled.
/// parses the outcome off the pipe and ships it back via the
/// returned [`OutcomeJoiner`].
pub fn after_spawn(self) -> OutcomeJoiner {
close_fd(self.write_fd);
let read_fd = self.read_fd;
let handle = std::thread::spawn(move || {
if let Some(outcome) = drain_outcome(read_fd) {
record_outcome(outcome);
}
});
let handle = std::thread::spawn(move || drain_outcome(read_fd));
OutcomeJoiner { handle: Some(handle) }
}
@ -638,20 +607,4 @@ mod tests {
assert!(decode_outcome(&[0_u8; OUTCOME_LEN - 1]).is_none());
}
#[test]
fn record_and_reset_round_trip() {
let original = last_hardening_outcome();
let probe = HardeningOutcome {
no_new_privs: PrimitiveStatus::Applied,
profile: ProcessHardeningProfileTag::Strict,
..HardeningOutcome::default()
};
record_outcome(probe);
assert_eq!(last_hardening_outcome(), Some(probe));
reset_last_hardening_outcome();
assert!(last_hardening_outcome().is_none());
if let Some(prev) = original {
record_outcome(prev);
}
}
}

View file

@ -28,8 +28,8 @@
//! `sandbox-exec` is shipped on every supported macOS release but the
//! binary path can be missing in stripped CI images. When
//! [`sandbox_exec_available`] returns `false`, the wrapper is a no-op
//! and [`record_outcome`] tags the run as
//! [`HardeningLevel::Trusted`] — the verifier reads this back via
//! and [`wrap_plan`] tags the run as [`HardeningLevel::Trusted`] on the
//! returned [`WrapResult`] — the verifier reads this back via
//! `VerifyOptions::refuse_filesystem_confirm` and downgrades filesystem-
//! oracle verdicts to
//! [`crate::evidence::InconclusiveReason::BackendInsufficient`].
@ -44,6 +44,15 @@ use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::sync::{Mutex, OnceLock};
// ── HardeningOutcome flow ─────────────────────────────────────────────────────
//
// Phase 18 originally recorded the outcome to a process-global
// `LAST_OUTCOME` singleton. Phase 17/18 sweep dropped that singleton
// because `verify_finding` runs under `rayon::par_iter` in `scan.rs`, so
// concurrent wraps would overwrite each other. [`wrap_plan`] now
// returns the outcome via [`WrapResult`] and `run_process` stashes it on
// the returned `SandboxOutcome`.
// ── HardeningLevel reporting ─────────────────────────────────────────────────
/// Coarse summary of the macOS sandbox-exec wrap outcome.
@ -64,7 +73,9 @@ pub enum HardeningLevel {
Failed,
}
/// Per-run summary read back by [`last_hardening_outcome`].
/// Per-run summary returned by [`wrap_plan`]. Threaded back to the
/// caller through [`WrapResult`] so `run_process` can stash it on the
/// [`crate::dynamic::sandbox::SandboxOutcome`] for the run.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HardeningOutcome {
pub level: HardeningLevel,
@ -73,33 +84,6 @@ pub struct HardeningOutcome {
pub profile: String,
}
static LAST_OUTCOME: OnceLock<Mutex<Option<HardeningOutcome>>> = OnceLock::new();
fn outcome_cell() -> &'static Mutex<Option<HardeningOutcome>> {
LAST_OUTCOME.get_or_init(|| Mutex::new(None))
}
pub(crate) fn record_outcome(outcome: HardeningOutcome) {
if let Ok(mut g) = outcome_cell().lock() {
*g = Some(outcome);
}
}
/// Snapshot of the most-recent hardening outcome on macOS. Tests +
/// telemetry read this after `sandbox::run` returns. Returns `None`
/// until at least one wrap attempt has been recorded.
pub fn last_hardening_outcome() -> Option<HardeningOutcome> {
outcome_cell().lock().ok().and_then(|g| g.clone())
}
/// Clear the last-outcome slot. Tests use this between cases so a stale
/// value from a prior spawn cannot leak into the assertion under test.
pub fn reset_last_hardening_outcome() {
if let Ok(mut g) = outcome_cell().lock() {
*g = None;
}
}
// ── sandbox-exec availability + binary path ──────────────────────────────────
/// Env override consulted by [`sandbox_exec_bin`]; tests set this to
@ -233,24 +217,35 @@ pub struct WrapPlan {
pub profile: &'static str,
}
/// Result of [`wrap_plan`]. Always carries a [`HardeningOutcome`] so
/// the caller can stash it on the `SandboxOutcome` even when wrapping
/// itself was a no-op (`plan = None` + `outcome.level = Trusted`).
pub struct WrapResult {
/// Wrap plan when `sandbox-exec` was applied; `None` when the
/// harness should run unwrapped. The verifier's
/// `refuse_filesystem_confirm` flag keeps the verdict honest in the
/// `None` case.
pub plan: Option<WrapPlan>,
pub outcome: HardeningOutcome,
}
/// Build the `sandbox-exec -f <profile> -D WORKDIR=<workdir> -- <cmd>`
/// argv for `cmd_path + cmd_args`. Returns `None` when:
/// argv for `cmd_path + cmd_args`. The returned [`WrapResult`]
/// `plan` is `None` when:
///
/// - `sandbox-exec` is not on the host (records [`HardeningLevel::Trusted`]),
/// - the profile name is unknown (records [`HardeningLevel::Trusted`]), or
/// - `sandbox-exec` is not on the host (`outcome.level = Trusted`),
/// - the profile name is unknown (`outcome.level = Trusted`), or
/// - the profile file could not be materialised in `/tmp`
/// (records [`HardeningLevel::Failed`]).
///
/// Callers use the returned `None` as a signal to fall back to the
/// unwrapped command; the verifier's `refuse_filesystem_confirm` flag
/// keeps the verdict honest in that case.
pub fn wrap_plan(input: &WrapInput<'_>) -> Option<WrapPlan> {
/// (`outcome.level = Failed`).
pub fn wrap_plan(input: &WrapInput<'_>) -> WrapResult {
if !sandbox_exec_available() {
record_outcome(HardeningOutcome {
level: HardeningLevel::Trusted,
profile: String::new(),
});
return None;
return WrapResult {
plan: None,
outcome: HardeningOutcome {
level: HardeningLevel::Trusted,
profile: String::new(),
},
};
}
let profile = input.profile_override.unwrap_or_else(|| profile_for_caps(input.caps));
// Profile keys must be `&'static str` (from `PROFILE_SOURCES`); reject
@ -263,21 +258,25 @@ pub fn wrap_plan(input: &WrapInput<'_>) -> Option<WrapPlan> {
let resolved_key = match resolved_key {
Some(k) => k,
None => {
record_outcome(HardeningOutcome {
level: HardeningLevel::Trusted,
profile: String::new(),
});
return None;
return WrapResult {
plan: None,
outcome: HardeningOutcome {
level: HardeningLevel::Trusted,
profile: String::new(),
},
};
}
};
let profile_file = match profile_path(resolved_key) {
Some(p) => p,
None => {
record_outcome(HardeningOutcome {
level: HardeningLevel::Failed,
profile: resolved_key.to_owned(),
});
return None;
return WrapResult {
plan: None,
outcome: HardeningOutcome {
level: HardeningLevel::Failed,
profile: resolved_key.to_owned(),
},
};
}
};
@ -293,16 +292,17 @@ pub fn wrap_plan(input: &WrapInput<'_>) -> Option<WrapPlan> {
args.push(a.clone());
}
record_outcome(HardeningOutcome {
level: HardeningLevel::Sandboxed,
profile: resolved_key.to_owned(),
});
Some(WrapPlan {
binary: sandbox_exec_bin(),
args,
profile: resolved_key,
})
WrapResult {
plan: Some(WrapPlan {
binary: sandbox_exec_bin(),
args,
profile: resolved_key,
}),
outcome: HardeningOutcome {
level: HardeningLevel::Sandboxed,
profile: resolved_key.to_owned(),
},
}
}
// ── Tests ────────────────────────────────────────────────────────────────────
@ -356,7 +356,6 @@ mod tests {
#[test]
fn wrap_plan_returns_none_when_sandbox_exec_missing() {
unsafe { std::env::set_var(SANDBOX_EXEC_BIN_ENV, "/nonexistent/sandbox-exec") };
reset_last_hardening_outcome();
let input = WrapInput {
cmd_path: Path::new("/usr/bin/true"),
cmd_args: &[],
@ -364,9 +363,9 @@ mod tests {
caps: 0,
profile_override: None,
};
assert!(wrap_plan(&input).is_none());
let outcome = last_hardening_outcome().expect("outcome recorded");
assert_eq!(outcome.level, HardeningLevel::Trusted);
let result = wrap_plan(&input);
assert!(result.plan.is_none());
assert_eq!(result.outcome.level, HardeningLevel::Trusted);
unsafe { std::env::remove_var(SANDBOX_EXEC_BIN_ENV) };
}
@ -380,7 +379,6 @@ mod tests {
eprintln!("SKIP: /usr/bin/sandbox-exec missing on this host");
return;
}
reset_last_hardening_outcome();
let input = WrapInput {
cmd_path: Path::new("/usr/bin/true"),
cmd_args: &[],
@ -388,13 +386,13 @@ mod tests {
caps: 1 << 5, // FILE_IO
profile_override: None,
};
let plan = wrap_plan(&input).expect("plan");
let result = wrap_plan(&input);
let plan = result.plan.expect("plan");
assert_eq!(plan.profile, "path_traversal");
assert_eq!(plan.binary, PathBuf::from("/usr/bin/sandbox-exec"));
assert!(plan.args.iter().any(|a| a == "-f"));
assert!(plan.args.iter().any(|a| a.starts_with("WORKDIR=")));
let outcome = last_hardening_outcome().expect("outcome");
assert_eq!(outcome.level, HardeningLevel::Sandboxed);
assert_eq!(outcome.profile, "path_traversal");
assert_eq!(result.outcome.level, HardeningLevel::Sandboxed);
assert_eq!(result.outcome.profile, "path_traversal");
}
}

View file

@ -36,6 +36,7 @@ fn crashed_outcome() -> SandboxOutcome {
oob_callback_seen: false,
sink_hit: false,
duration: Duration::from_millis(1),
hardening_outcome: None,
}
}
@ -48,6 +49,7 @@ fn clean_outcome() -> SandboxOutcome {
oob_callback_seen: false,
sink_hit: false,
duration: Duration::from_millis(1),
hardening_outcome: None,
}
}

View file

@ -37,6 +37,7 @@ fn dummy_outcome() -> nyx_scanner::dynamic::sandbox::SandboxOutcome {
oob_callback_seen: false,
sink_hit: true,
duration: Duration::from_millis(1),
hardening_outcome: None,
}
}

View file

@ -47,6 +47,7 @@ mod repro_determinism_tests {
oob_callback_seen: false,
sink_hit: true,
duration: Duration::from_millis(150),
hardening_outcome: None,
}
}

View file

@ -21,14 +21,22 @@ mod hardening_tests {
use std::time::Duration;
use nyx_scanner::dynamic::harness::BuiltHarness;
use nyx_scanner::dynamic::sandbox::process_linux::{
last_hardening_outcome, reset_last_hardening_outcome, HardeningLevel, PrimitiveStatus,
};
use nyx_scanner::dynamic::sandbox::process_linux::{HardeningLevel, PrimitiveStatus};
use nyx_scanner::dynamic::sandbox::seccomp;
use nyx_scanner::dynamic::sandbox::{
self, ProcessHardeningProfile, SandboxBackend, SandboxOptions,
self, HardeningRecord, ProcessHardeningProfile, SandboxBackend, SandboxOptions,
};
fn linux_outcome(out: &sandbox::SandboxOutcome)
-> Option<nyx_scanner::dynamic::sandbox::process_linux::HardeningOutcome>
{
match out.hardening_outcome.as_ref()? {
HardeningRecord::Linux(o) => Some(*o),
#[allow(unreachable_patterns)]
_ => None,
}
}
// ── Probe build ───────────────────────────────────────────────────────────
/// Path to the freshly-built probe binary, shared across every test.
@ -161,7 +169,6 @@ mod hardening_tests {
let tmp = workdir();
let harness = build_harness_with_probe(tmp.path(), &[]);
let opts = strict_opts();
reset_last_hardening_outcome();
let result = sandbox::run(&harness, b"", &opts).expect("sandbox::run");
let stdout = stdout_string(&result);
eprintln!("probe stdout under strict:\n{stdout}");
@ -260,10 +267,9 @@ mod hardening_tests {
let tmp = workdir();
let harness = build_harness_with_probe(tmp.path(), &[]);
let opts = strict_opts();
reset_last_hardening_outcome();
let result = sandbox::run(&harness, b"", &opts).expect("sandbox::run");
let stdout = stdout_string(&result);
let outcome = last_hardening_outcome().expect("hardening outcome recorded");
let outcome = linux_outcome(&result).expect("hardening outcome recorded");
// Parent's user-ns inode for comparison.
let parent_user_ns =
@ -310,10 +316,9 @@ mod hardening_tests {
let tmp = workdir();
let harness = build_harness_with_probe(tmp.path(), &[]);
let opts = strict_opts();
reset_last_hardening_outcome();
let result = sandbox::run(&harness, b"", &opts).expect("sandbox::run");
let stdout = stdout_string(&result);
let outcome = last_hardening_outcome().expect("hardening outcome recorded");
let outcome = linux_outcome(&result).expect("hardening outcome recorded");
match outcome.chroot {
PrimitiveStatus::Applied => {
@ -349,10 +354,9 @@ mod hardening_tests {
let tmp = workdir();
let harness = build_harness_with_probe(tmp.path(), &["traverse"]);
let opts = strict_opts();
reset_last_hardening_outcome();
let result = sandbox::run(&harness, b"", &opts).expect("sandbox::run");
let stdout = stdout_string(&result);
let outcome = last_hardening_outcome().expect("hardening outcome recorded");
let outcome = linux_outcome(&result).expect("hardening outcome recorded");
if matches!(outcome.chroot, PrimitiveStatus::Applied) {
// NotConfirmed shape: the verifier maps a non-zero exit + no
@ -390,10 +394,9 @@ mod hardening_tests {
let tmp = workdir();
let harness = build_harness_with_probe(tmp.path(), &[]);
let opts = strict_opts();
reset_last_hardening_outcome();
let result = sandbox::run(&harness, b"", &opts).expect("sandbox::run");
let stdout = stdout_string(&result);
let outcome = last_hardening_outcome().expect("hardening outcome recorded");
let outcome = linux_outcome(&result).expect("hardening outcome recorded");
match outcome.seccomp {
PrimitiveStatus::Applied => {
@ -422,10 +425,9 @@ mod hardening_tests {
let tmp = workdir();
let harness = build_harness_with_probe(tmp.path(), &[]);
let opts = standard_opts();
reset_last_hardening_outcome();
let result = sandbox::run(&harness, b"", &opts).expect("sandbox::run");
let stdout = stdout_string(&result);
let outcome = last_hardening_outcome().expect("hardening outcome recorded");
let outcome = linux_outcome(&result).expect("hardening outcome recorded");
assert_eq!(outcome.level(), HardeningLevel::Baseline);
assert!(matches!(outcome.no_new_privs, PrimitiveStatus::Applied));

View file

@ -21,13 +21,22 @@ mod hardening_tests {
use nyx_scanner::dynamic::harness::BuiltHarness;
use nyx_scanner::dynamic::sandbox::process_macos::{
last_hardening_outcome, profile_for_caps, reset_last_hardening_outcome,
sandbox_exec_available, HardeningLevel, SANDBOX_EXEC_BIN_ENV,
profile_for_caps, sandbox_exec_available, HardeningLevel, SANDBOX_EXEC_BIN_ENV,
};
use nyx_scanner::dynamic::sandbox::{
self, ProcessHardeningProfile, SandboxBackend, SandboxOptions,
self, HardeningRecord, ProcessHardeningProfile, SandboxBackend, SandboxOptions,
};
fn macos_outcome(out: &sandbox::SandboxOutcome)
-> Option<&nyx_scanner::dynamic::sandbox::process_macos::HardeningOutcome>
{
match out.hardening_outcome.as_ref()? {
HardeningRecord::Macos(o) => Some(o),
#[allow(unreachable_patterns)]
_ => None,
}
}
// ── Probe source + harness helpers ────────────────────────────────────────
/// Python source that tries to read `/etc/passwd`. Exits 0 when the
@ -145,11 +154,10 @@ except Exception as exc:
let tmp = workdir();
let harness = build_harness(tmp.path());
let opts = strict_opts(FILE_IO);
reset_last_hardening_outcome();
let result = sandbox::run(&harness, b"", &opts).expect("sandbox::run");
let stdout = stdout_string(&result);
eprintln!("stdout under path_traversal:\n{stdout}");
let outcome = last_hardening_outcome().expect("hardening outcome recorded");
let outcome = macos_outcome(&result).expect("hardening outcome recorded");
assert_eq!(outcome.level, HardeningLevel::Sandboxed);
assert_eq!(outcome.profile, "path_traversal");
assert!(
@ -173,14 +181,16 @@ except Exception as exc:
let tmp = workdir();
let harness = build_harness(tmp.path());
let opts = standard_opts();
reset_last_hardening_outcome();
let result = sandbox::run(&harness, b"", &opts).expect("sandbox::run");
let stdout = stdout_string(&result);
eprintln!("stdout under standard:\n{stdout}");
// Standard profile means the macOS wrap was never attempted; the
// outcome registry stays at `None` (no prior strict run in this
// test) or carries the prior strict run's outcome. We don't
// assert on the registry — we assert on the probe's exit.
// Standard profile means the macOS wrap was never attempted —
// `hardening_outcome` stays `None` because `wrap_plan` was not
// called. Assert on the probe's marker only.
assert!(
result.hardening_outcome.is_none(),
"standard profile should not produce a hardening outcome",
);
assert!(
stdout.contains("escape:escaped") || stdout.contains("escape:blocked"),
"probe should at least print its marker; stdout:\n{stdout}"
@ -188,7 +198,7 @@ except Exception as exc:
}
/// When `sandbox-exec` is unavailable the wrap is a no-op and the
/// outcome registry records `Trusted`. Tests force the missing
/// returned outcome records `Trusted`. Tests force the missing
/// binary path via the [`SANDBOX_EXEC_BIN_ENV`] override.
#[test]
fn sandbox_exec_missing_records_trusted_outcome() {
@ -197,14 +207,12 @@ except Exception as exc:
let tmp = workdir();
let harness = build_harness(tmp.path());
let opts = strict_opts(FILE_IO);
reset_last_hardening_outcome();
let result = sandbox::run(&harness, b"", &opts).expect("sandbox::run");
let stdout = stdout_string(&result);
let outcome = last_hardening_outcome().expect("hardening outcome recorded");
let outcome = macos_outcome(&result).expect("hardening outcome recorded");
assert_eq!(outcome.level, HardeningLevel::Trusted);
eprintln!("stdout when sandbox-exec missing:\n{stdout}");
unsafe { std::env::remove_var(SANDBOX_EXEC_BIN_ENV) };
let _ = result;
}
/// Phase 18 acceptance (b): when sandbox-exec is missing the

View file

@ -64,6 +64,7 @@ fn empty_outcome() -> SandboxOutcome {
oob_callback_seen: false,
sink_hit: true,
duration: Duration::from_millis(1),
hardening_outcome: None,
}
}