mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-12 19:55:14 +02:00
[pitboss] sweep after phase 19: 3 deferred items resolved
This commit is contained in:
parent
7ca0c053f5
commit
1d9b4c688f
12 changed files with 204 additions and 180 deletions
|
|
@ -402,6 +402,7 @@ mod tests {
|
|||
oob_callback_seen: false,
|
||||
sink_hit: false,
|
||||
duration: Duration::from_millis(1),
|
||||
hardening_outcome: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -406,6 +406,7 @@ mod tests {
|
|||
oob_callback_seen: false,
|
||||
sink_hit: true,
|
||||
duration: Duration::from_millis(250),
|
||||
hardening_outcome: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ mod repro_determinism_tests {
|
|||
oob_callback_seen: false,
|
||||
sink_hit: true,
|
||||
duration: Duration::from_millis(150),
|
||||
hardening_outcome: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ fn empty_outcome() -> SandboxOutcome {
|
|||
oob_callback_seen: false,
|
||||
sink_hit: true,
|
||||
duration: Duration::from_millis(1),
|
||||
hardening_outcome: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue