[pitboss/grind] deferred session-0023 (20260516T052512Z-20f8)

This commit is contained in:
pitboss 2026-05-16 13:05:27 -05:00
parent 1d1975a2ea
commit 6189c4a4c5
20 changed files with 297 additions and 1 deletions

View file

@ -448,6 +448,7 @@ mod tests {
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
}),
..Default::default()
});

View file

@ -110,6 +110,7 @@ mod tests {
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
}
}

View file

@ -131,6 +131,7 @@ impl CompositeReverifier for DefaultCompositeReverifier {
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
}
}
}
@ -256,6 +257,7 @@ mod tests {
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
}
}

View file

@ -687,6 +687,7 @@ mod tests {
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
}
}

View file

@ -14,7 +14,9 @@ use crate::dynamic::spec::{HarnessSpec, SPEC_FORMAT_VERSION};
use crate::dynamic::stubs::StubHarness;
use crate::dynamic::telemetry::{self, SamplingPolicy, TelemetryEvent};
use crate::dynamic::toolchain;
use crate::evidence::{InconclusiveReason, SpecDerivationStrategy, UnsupportedReason};
use crate::evidence::{HardeningSummary, InconclusiveReason, SpecDerivationStrategy, UnsupportedReason};
#[cfg(target_os = "linux")]
use crate::evidence::HardeningPrimitive;
use crate::summary::GlobalSummaries;
use crate::utils::config::Config;
use std::path::Path;
@ -305,6 +307,7 @@ fn entry_kind_unsupported_verdict(
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
}
}
@ -349,6 +352,7 @@ fn spec_derivation_failed_verdict(
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
};
}
@ -367,6 +371,7 @@ fn spec_derivation_failed_verdict(
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
}
}
@ -474,6 +479,7 @@ pub fn verify_finding(diag: &Diag, opts: &VerifyOptions) -> VerifyResult {
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
};
}
@ -558,6 +564,7 @@ pub fn verify_finding(diag: &Diag, opts: &VerifyOptions) -> VerifyResult {
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
};
}
@ -588,6 +595,7 @@ pub fn verify_finding(diag: &Diag, opts: &VerifyOptions) -> VerifyResult {
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
};
}
}
@ -732,6 +740,91 @@ pub fn verify_finding(diag: &Diag, opts: &VerifyOptions) -> VerifyResult {
}
/// Project the platform-cfg'd [`crate::dynamic::sandbox::HardeningRecord`]
/// into the portable [`HardeningSummary`] that lands on
/// [`VerifyResult::hardening_outcome`]. Returns `None` when the run did
/// not record a hardening outcome (docker backend, non-Linux/non-macOS
/// host, or `Standard` profile on a host whose backend skipped the wrap).
///
/// Exposed for tests so a `sandbox::run`-driven probe can assert that the
/// projection lands the same record `build_verdict` would stamp on a
/// `Confirmed` `VerifyResult` from the same triggering attempt.
pub fn summarize_hardening(
outcome: &crate::dynamic::sandbox::SandboxOutcome,
) -> Option<HardeningSummary> {
use crate::dynamic::sandbox::HardeningRecord;
let record = outcome.hardening_outcome.as_ref()?;
match record {
#[cfg(target_os = "linux")]
HardeningRecord::Linux(o) => {
use crate::dynamic::sandbox::process_linux::{
HardeningLevel, PrimitiveStatus, ProcessHardeningProfileTag,
};
fn status_str(s: PrimitiveStatus) -> (String, Option<i32>) {
match s {
PrimitiveStatus::Skipped => ("skipped".to_owned(), None),
PrimitiveStatus::Applied => ("applied".to_owned(), None),
PrimitiveStatus::Failed(errno) => ("failed".to_owned(), Some(errno)),
}
}
let primitives = [
("no_new_privs", o.no_new_privs),
("rlimit_cpu", o.rlimit_cpu),
("rlimit_nofile", o.rlimit_nofile),
("rlimit_as", o.rlimit_as),
("unshare", o.unshare),
("chroot", o.chroot),
("seccomp", o.seccomp),
]
.into_iter()
.map(|(name, st)| {
let (status, errno) = status_str(st);
HardeningPrimitive {
name: name.to_owned(),
status,
errno,
}
})
.collect();
let level = match o.level() {
HardeningLevel::Baseline => "baseline",
HardeningLevel::Full => "full",
HardeningLevel::Partial => "partial",
HardeningLevel::None => "none",
};
// The Linux backend uses the same `.sb`-style profile name
// surface (Standard / Strict) as macOS via the profile tag.
let profile = match o.profile {
ProcessHardeningProfileTag::Standard => String::new(),
ProcessHardeningProfileTag::Strict => "strict".to_owned(),
};
Some(HardeningSummary {
backend: "linux-process".to_owned(),
level: level.to_owned(),
profile,
primitives,
})
}
#[cfg(target_os = "macos")]
HardeningRecord::Macos(o) => {
use crate::dynamic::sandbox::process_macos::HardeningLevel;
let level = match o.level {
HardeningLevel::Trusted => "trusted",
HardeningLevel::Sandboxed => "sandboxed",
HardeningLevel::Failed => "failed",
};
Some(HardeningSummary {
backend: "macos-process".to_owned(),
level: level.to_owned(),
profile: o.profile.clone(),
primitives: Vec::new(),
})
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
_ => None,
}
}
fn build_verdict(
finding_id: &str,
spec: &HarnessSpec,
@ -762,6 +855,7 @@ fn build_verdict(
.get(i)
.map(|p| p.bytes)
.unwrap_or(b"");
let hardening_outcome = summarize_hardening(&run.attempts[i].outcome);
// Emit repro artifact.
let repro_result = crate::dynamic::repro::write(
@ -780,6 +874,7 @@ fn build_verdict(
differential: run.differential.clone(),
replay_stable: None,
wrong: None,
hardening_outcome: hardening_outcome.clone(),
},
&run.harness_source,
&run.entry_source,
@ -802,6 +897,7 @@ fn build_verdict(
differential: run.differential,
replay_stable: None,
wrong: None,
hardening_outcome,
};
}
@ -817,6 +913,7 @@ fn build_verdict(
differential: run.differential,
replay_stable: None,
wrong: None,
hardening_outcome,
}
} else if run.unrelated_crash {
// Phase 08 §C.4: the harness crashed but the death
@ -838,6 +935,7 @@ fn build_verdict(
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
}
} else if run.no_benign_control {
// Phase 07 §4.1: vuln oracle + sink-hit fired but the
@ -858,6 +956,7 @@ fn build_verdict(
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
}
} else if let Some(d) = run.differential.as_ref() {
// Differential ran but didn't produce `Confirmed`. Map
@ -881,6 +980,7 @@ fn build_verdict(
differential: run.differential,
replay_stable: None,
wrong: None,
hardening_outcome: None,
}
}
crate::evidence::DifferentialVerdict::ReversedDifferential => {
@ -900,6 +1000,7 @@ fn build_verdict(
differential: run.differential,
replay_stable: None,
wrong: None,
hardening_outcome: None,
}
}
crate::evidence::DifferentialVerdict::Confirmed
@ -915,6 +1016,7 @@ fn build_verdict(
differential: run.differential,
replay_stable: None,
wrong: None,
hardening_outcome: None,
},
}
} else if run.oracle_collision {
@ -933,6 +1035,7 @@ fn build_verdict(
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
}
} else {
VerifyResult {
@ -947,6 +1050,7 @@ fn build_verdict(
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
}
}
}
@ -962,6 +1066,7 @@ fn build_verdict(
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
},
Err(RunError::Harness(e)) => {
// Defence-in-depth residual for `EntryKindUnsupported` from the
@ -1007,6 +1112,7 @@ fn build_verdict(
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
}
}
Err(RunError::BuildFailed { stderr, attempts: build_att }) => VerifyResult {
@ -1021,6 +1127,7 @@ fn build_verdict(
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
},
Err(RunError::Sandbox(e)) => VerifyResult {
finding_id: finding_id.to_owned(),
@ -1034,6 +1141,7 @@ fn build_verdict(
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
},
}
}
@ -1142,6 +1250,7 @@ mod tests {
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
};
// Insert.
@ -1193,6 +1302,7 @@ mod tests {
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
};
insert_verdict_cache(&db_path, "spec_aaa", "hash_xyz", "", "python-3.11", &result);
@ -1230,6 +1340,7 @@ mod tests {
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
};
insert_verdict_cache(db_path, "spec", "hash", "", "python-3", &result);
assert!(!db_path.exists(), "insert must not create a new DB");
@ -1286,6 +1397,7 @@ mod tests {
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
};
// Insert directly with the old corpus_version bypassing the helper.

View file

@ -506,6 +506,42 @@ pub struct DifferentialProbeRecord {
pub payload_id: String,
}
/// Per-primitive entry inside [`HardeningSummary::primitives`].
///
/// Mirrors the Linux process backend's `PrimitiveStatus`-per-primitive
/// table without depending on the `dynamic` feature. `status` is one of
/// `"applied"`, `"failed"`, or `"skipped"`; `errno` is populated when
/// `status == "failed"`.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct HardeningPrimitive {
pub name: String,
pub status: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub errno: Option<i32>,
}
/// Portable, JSON-serialisable projection of the per-run hardening
/// outcome the process backend stamps on `SandboxOutcome`.
///
/// Stored on [`VerifyResult::hardening_outcome`] so callers (eval-corpus
/// tabulator, repro round-trips, end-to-end acceptance tests) can assert
/// on the matched profile and per-primitive status without depending on
/// the platform-cfg'd `HardeningRecord` enum. `backend` is one of
/// `"linux-process"` or `"macos-process"`; `level` is the coarse outcome
/// (`"trusted"` / `"sandboxed"` / `"failed"` on macOS;
/// `"baseline"` / `"full"` / `"partial"` / `"none"` on Linux); `profile`
/// is the matched `.sb` name on macOS and empty on Linux; `primitives`
/// is empty on macOS and one entry per primitive on Linux.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct HardeningSummary {
pub backend: String,
pub level: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub profile: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub primitives: Vec<HardeningPrimitive>,
}
/// Full record of a Phase 07 differential confirmation run.
///
/// Captures the rule's verdict plus the raw probe traces from both the
@ -584,6 +620,14 @@ pub struct VerifyResult {
/// `wrong_confirmed` column in `tests/eval_corpus/tabulate.py`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub wrong: Option<bool>,
/// Phase 17/18 per-run hardening outcome, projected from the
/// triggering attempt's [`crate::dynamic::sandbox::SandboxOutcome`].
/// Populated only when a payload actually ran under the process
/// backend on Linux or macOS and the run captured a primitive
/// outcome; `None` for docker-backend runs, host platforms with no
/// hardening primitives, or verdicts that never executed a payload.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hardening_outcome: Option<HardeningSummary>,
}
// ─────────────────────────────────────────────────────────────────────────────

View file

@ -1159,6 +1159,7 @@ mod tests {
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
}
}
@ -1181,6 +1182,7 @@ mod tests {
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
}
}
@ -1197,6 +1199,7 @@ mod tests {
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
}
}
@ -1213,6 +1216,7 @@ mod tests {
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
}
}
@ -1229,6 +1233,7 @@ mod tests {
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
}
}

View file

@ -76,6 +76,7 @@ fn verdict(status: VerifyStatus, reason: Option<InconclusiveReason>) -> VerifyRe
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
}
}

View file

@ -584,6 +584,7 @@ pub fn run_shape_fixture_lang(
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
}
}
Err(RunError::NoPayloadsForCap) => VerifyResult {
@ -598,6 +599,7 @@ pub fn run_shape_fixture_lang(
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
},
Err(e) => VerifyResult {
finding_id: spec.finding_id.clone(),
@ -611,6 +613,7 @@ pub fn run_shape_fixture_lang(
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
},
}
}

View file

@ -74,6 +74,7 @@ fn diag_with_verdict(status: VerifyStatus) -> Diag {
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
},
VerifyStatus::NotConfirmed => VerifyResult {
finding_id: "abc123".into(),
@ -93,6 +94,7 @@ fn diag_with_verdict(status: VerifyStatus) -> Diag {
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
},
VerifyStatus::Unsupported => VerifyResult {
finding_id: "abc123".into(),
@ -106,6 +108,7 @@ fn diag_with_verdict(status: VerifyStatus) -> Diag {
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
},
VerifyStatus::Inconclusive => VerifyResult {
finding_id: "abc123".into(),
@ -119,6 +122,7 @@ fn diag_with_verdict(status: VerifyStatus) -> Diag {
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
},
};

View file

@ -55,6 +55,7 @@ fn set_verdict(
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
});
}
}
@ -170,6 +171,7 @@ fn new_confirmed_fails_no_new_confirmed_gate() {
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
});
}
}

View file

@ -61,6 +61,7 @@ mod go_fixture_tests {
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
};
}

View file

@ -69,6 +69,7 @@ mod java_fixture_tests {
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
};
}

View file

@ -62,6 +62,7 @@ mod js_fixture_tests {
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
};
}

View file

@ -60,6 +60,7 @@ fn json_dynamic_verdict_confirmed_serialises_correctly() {
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
}),
..Default::default()
});
@ -100,6 +101,7 @@ fn json_dynamic_verdict_not_confirmed_serialises_correctly() {
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
}),
..Default::default()
});
@ -165,6 +167,7 @@ fn json_unsupported_verdict_has_reason() {
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
}),
..Default::default()
});

View file

@ -61,6 +61,7 @@ mod php_fixture_tests {
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
};
}

View file

@ -70,6 +70,7 @@ mod repro_determinism_tests {
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
}
}

View file

@ -89,6 +89,7 @@ mod repro_hermetic_tests {
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
}
}

View file

@ -350,6 +350,111 @@ except Exception as exc:
"refuse_filesystem_confirm should be false when sandbox-exec is reachable"
);
}
/// Phase 18 verifier-side projection: when a real strict run lands a
/// macOS `HardeningRecord`, `summarize_hardening` collapses it into
/// the portable [`crate::evidence::HardeningSummary`] that
/// `build_verdict` stamps on a `Confirmed` `VerifyResult`. Drives
/// the same `sandbox::run` path the existing
/// `path_traversal_payload_blocked_under_strict` test uses, then
/// asserts on the projection that would land on
/// `VerifyResult::hardening_outcome` if this run had triggered the
/// finding's oracle.
#[test]
fn summarize_hardening_lands_path_traversal_on_strict_file_io_run() {
unsafe { std::env::remove_var(SANDBOX_EXEC_BIN_ENV) };
if !sandbox_exec_available() {
eprintln!("SKIP: /usr/bin/sandbox-exec missing — cannot exercise wrap");
return;
}
const FILE_IO: u32 = 1 << 5;
let tmp = workdir();
let harness = build_harness(tmp.path());
let opts = strict_opts(FILE_IO);
let result = sandbox::run(&harness, b"", &opts).expect("sandbox::run");
let summary = nyx_scanner::dynamic::verify::summarize_hardening(&result)
.expect("hardening summary should populate after a strict macOS run");
assert_eq!(summary.backend, "macos-process");
assert_eq!(summary.level, "sandboxed");
assert_eq!(
summary.profile, "path_traversal",
"FILE_IO-cap strict run should select the path_traversal profile"
);
assert!(
summary.primitives.is_empty(),
"macOS backend records no per-primitive entries"
);
}
/// Standard-profile runs leave `SandboxOutcome::hardening_outcome`
/// unset, so `summarize_hardening` returns `None` and
/// `VerifyResult::hardening_outcome` stays `None`. Companion to
/// `standard_profile_does_not_wrap_with_sandbox_exec`.
#[test]
fn summarize_hardening_returns_none_for_standard_profile_run() {
unsafe { std::env::remove_var(SANDBOX_EXEC_BIN_ENV) };
let tmp = workdir();
let harness = build_harness(tmp.path());
let opts = standard_opts();
let result = sandbox::run(&harness, b"", &opts).expect("sandbox::run");
assert!(
nyx_scanner::dynamic::verify::summarize_hardening(&result).is_none(),
"standard profile should leave hardening_outcome unset"
);
}
/// Round-trip the portable summary through JSON to lock in the
/// repro-bundle wire shape: `VerifyResult::hardening_outcome` lands
/// on `expected/verdict.json` so the eval-corpus tabulator and any
/// downstream replay reads the same fields back.
#[test]
fn hardening_summary_round_trips_through_json() {
use nyx_scanner::evidence::{HardeningSummary, HardeningPrimitive};
let summary = HardeningSummary {
backend: "macos-process".into(),
level: "sandboxed".into(),
profile: "path_traversal".into(),
primitives: vec![],
};
let json = serde_json::to_string(&summary).expect("serialize");
let parsed: HardeningSummary = serde_json::from_str(&json).expect("deserialize");
assert_eq!(parsed, summary);
// Defaults: missing `profile` and `primitives` must decode as
// empty so older `verdict.json` payloads keep round-tripping.
let minimal: HardeningSummary =
serde_json::from_str(r#"{"backend":"linux-process","level":"full"}"#)
.expect("minimal decode");
assert_eq!(minimal.profile, "");
assert!(minimal.primitives.is_empty());
// Linux-shape: per-primitive entries decode + re-encode with
// their `errno` field intact when populated.
let with_primitives = HardeningSummary {
backend: "linux-process".into(),
level: "partial".into(),
profile: "strict".into(),
primitives: vec![
HardeningPrimitive {
name: "no_new_privs".into(),
status: "applied".into(),
errno: None,
},
HardeningPrimitive {
name: "seccomp".into(),
status: "failed".into(),
errno: Some(1),
},
],
};
let json = serde_json::to_string(&with_primitives).expect("serialize primitives");
assert!(
json.contains("\"errno\":1"),
"errno field should survive JSON round-trip; got: {json}"
);
let parsed: HardeningSummary = serde_json::from_str(&json).expect("decode primitives");
assert_eq!(parsed, with_primitives);
}
}
// Non-macOS placeholder so `cargo nextest run --test sandbox_hardening_macos`

View file

@ -76,6 +76,7 @@ fn sarif_confirmed_verdict_sets_partial_fingerprint() {
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
};
let result = sarif_result(diag_with_verdict(verdict));
@ -111,6 +112,7 @@ fn sarif_not_confirmed_verdict_sets_partial_fingerprint() {
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
};
let result = sarif_result(diag_with_verdict(verdict));
@ -140,6 +142,7 @@ fn sarif_unsupported_verdict_sets_partial_fingerprint() {
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
};
let result = sarif_result(diag_with_verdict(verdict));
@ -174,6 +177,7 @@ fn sarif_inconclusive_verdict_sets_partial_fingerprint() {
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
};
let result = sarif_result(diag_with_verdict(verdict));
@ -224,6 +228,7 @@ fn sarif_confirmed_verdict_nyx_dynamic_verdict_contains_triggered_payload() {
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
};
let result = sarif_result(diag_with_verdict(verdict));
@ -257,6 +262,7 @@ fn sarif_all_four_statuses_produce_partial_fingerprint() {
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
};
let result = sarif_result(diag_with_verdict(verdict));