mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-21 20:18:06 +02:00
[pitboss/grind] deferred session-0008 (20260516T052512Z-20f8)
This commit is contained in:
parent
f053665a83
commit
1ef650dc48
5 changed files with 292 additions and 11 deletions
24
tests/dynamic_fixtures/c/free_fn/setup_fault.c
Normal file
24
tests/dynamic_fixtures/c/free_fn/setup_fault.c
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
/* Phase 08 (b) acceptance fixture — crash outside the sink.
|
||||
*
|
||||
* Cap: FMT_STRING. A global constructor (`__attribute__((constructor))`)
|
||||
* runs before `main`, so the abort fires BEFORE the harness reaches
|
||||
* `__nyx_install_crash_guard`. No Crash probe is written, the
|
||||
* `Oracle::SinkCrash` predicate sees `process_crashed &&
|
||||
* !has_sink_crash_probe`, and the verifier routes to
|
||||
* `Inconclusive(UnrelatedCrash)` instead of `Confirmed`.
|
||||
*
|
||||
* The `run` body is unreachable but must compile so the entry symbol
|
||||
* resolves at link time. */
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
__attribute__((constructor)) static void nyx_fixture_crash_in_setup(void) {
|
||||
abort();
|
||||
}
|
||||
|
||||
void run(const char *payload, size_t len) {
|
||||
(void)payload;
|
||||
(void)len;
|
||||
printf("__NYX_SINK_HIT__\n");
|
||||
}
|
||||
25
tests/dynamic_fixtures/c/free_fn/sink_fault.c
Normal file
25
tests/dynamic_fixtures/c/free_fn/sink_fault.c
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/* Phase 08 (a) acceptance fixture — crash at the sink.
|
||||
*
|
||||
* Cap: FMT_STRING. Prints the `__NYX_SINK_HIT__` sentinel so the runner
|
||||
* sees the in-harness sink-hit, then NULL-dereferences when handed the
|
||||
* vuln payload. The harness's `__nyx_install_crash_guard` was installed
|
||||
* earlier in `main`, so SIGSEGV writes a Crash probe to `NYX_PROBE_PATH`,
|
||||
* which lifts the `Oracle::SinkCrash` predicate to `Confirmed`.
|
||||
*
|
||||
* Differential confirmation: the paired benign payload carries the
|
||||
* `NYX_BENIGN` marker. The short-circuit below returns cleanly on the
|
||||
* benign run so `benign_fired = false`, satisfying the §4.1 rule. */
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
void run(const char *payload, size_t len) {
|
||||
(void)len;
|
||||
printf("__NYX_SINK_HIT__\n");
|
||||
fflush(stdout);
|
||||
if (payload && strstr(payload, "NYX_BENIGN")) {
|
||||
return;
|
||||
}
|
||||
volatile char *p = NULL;
|
||||
*p = 1;
|
||||
}
|
||||
|
|
@ -11,9 +11,16 @@
|
|||
//! - (a) sink-site crash → `Confirmed`
|
||||
//! - (b) crash outside sink → `Inconclusive(UnrelatedCrash)`
|
||||
//! - (c) bounded witness capture for known payloads
|
||||
//!
|
||||
//! End-to-end fixtures at the bottom of this file drive the full
|
||||
//! [`run_spec`] pipeline against compiled C harnesses, locking in that
|
||||
//! the `__nyx_install_crash_guard` ordering inside the emitted `main.c`
|
||||
//! routes setup-fault and sink-fault crashes to the right verdicts.
|
||||
|
||||
#![cfg(feature = "dynamic")]
|
||||
|
||||
mod common;
|
||||
|
||||
use nyx_scanner::dynamic::oracle::{
|
||||
oracle_fired, probe_crash_signal, Oracle, Signal, SignalSet,
|
||||
};
|
||||
|
|
@ -279,3 +286,143 @@ fn signal_set_const_construction_is_order_independent() {
|
|||
assert!(B.contains(Signal::Sigabrt));
|
||||
assert!(!A.contains(Signal::Sigfpe));
|
||||
}
|
||||
|
||||
// ── End-to-end Phase 08 acceptance via compiled C harnesses ───────────────────
|
||||
//
|
||||
// These tests drive the full `run_spec` pipeline against the FMT_STRING
|
||||
// curated payload + paired benign control, against two purpose-built
|
||||
// fixtures under `tests/dynamic_fixtures/c/free_fn/`. Both pin the
|
||||
// install ordering inside the emitted `main.c`:
|
||||
//
|
||||
// nyx_payload() <- harness setup
|
||||
// __nyx_install_crash_guard(callee) <- install
|
||||
// run(payload, len) <- entry
|
||||
//
|
||||
// `setup_fault.c` aborts in a global constructor (before `main` runs),
|
||||
// so the handler never installs and `Oracle::SinkCrash` cannot fire —
|
||||
// the verifier downgrades to `Inconclusive(UnrelatedCrash)`.
|
||||
//
|
||||
// `sink_fault.c` prints the in-harness sink-hit sentinel and then
|
||||
// NULL-dereferences on the vuln payload only. The handler is installed
|
||||
// by the time the deref happens, a Crash probe lands in `NYX_PROBE_PATH`,
|
||||
// and the differential rule (§4.1) confirms because the benign payload
|
||||
// short-circuits without crashing.
|
||||
|
||||
mod e2e_phase_08 {
|
||||
use crate::common::fixture_harness::FIXTURE_LOCK;
|
||||
use nyx_scanner::dynamic::runner::{run_spec, RunOutcome};
|
||||
use nyx_scanner::dynamic::sandbox::SandboxOptions;
|
||||
use nyx_scanner::dynamic::spec::{
|
||||
default_toolchain_id, EntryKind, HarnessSpec, PayloadSlot, SpecDerivationStrategy,
|
||||
};
|
||||
use nyx_scanner::labels::Cap;
|
||||
use nyx_scanner::symbol::Lang;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn cc_available() -> bool {
|
||||
let bin = std::env::var("NYX_CC_BIN").unwrap_or_else(|_| "cc".to_owned());
|
||||
std::process::Command::new(&bin)
|
||||
.arg("--version")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Stage `tests/dynamic_fixtures/c/free_fn/<file>` into a fresh
|
||||
/// tempdir and synthesise a [`HarnessSpec`] pointing at the copy.
|
||||
/// Returns the spec plus the tempdir guard (caller drops it after
|
||||
/// `run_spec` completes so the workdir survives the test).
|
||||
fn build_spec(file: &str) -> (HarnessSpec, tempfile::TempDir) {
|
||||
let fixture_src = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests/dynamic_fixtures/c/free_fn")
|
||||
.join(file);
|
||||
let tmp = tempfile::TempDir::new().expect("create tempdir");
|
||||
let dst = tmp.path().join(file);
|
||||
std::fs::copy(&fixture_src, &dst).expect("copy fixture");
|
||||
|
||||
let entry_file = dst.to_string_lossy().into_owned();
|
||||
let mut digest = blake3::Hasher::new();
|
||||
digest.update(b"phase08-c-e2e|");
|
||||
digest.update(file.as_bytes());
|
||||
let spec_hash = format!("{:016x}", {
|
||||
let bytes = digest.finalize();
|
||||
u64::from_le_bytes(bytes.as_bytes()[..8].try_into().unwrap())
|
||||
});
|
||||
|
||||
let spec = HarnessSpec {
|
||||
finding_id: spec_hash.clone(),
|
||||
entry_file: entry_file.clone(),
|
||||
entry_name: "run".to_owned(),
|
||||
entry_kind: EntryKind::Function,
|
||||
lang: Lang::C,
|
||||
toolchain_id: default_toolchain_id(Lang::C).into(),
|
||||
payload_slot: PayloadSlot::Param(0),
|
||||
expected_cap: Cap::FMT_STRING,
|
||||
constraint_hints: vec![],
|
||||
sink_file: entry_file,
|
||||
sink_line: 22,
|
||||
spec_hash: spec_hash.clone(),
|
||||
derivation: SpecDerivationStrategy::FromFlowSteps,
|
||||
stubs_required: vec![],
|
||||
};
|
||||
|
||||
(spec, tmp)
|
||||
}
|
||||
|
||||
fn run(file: &str) -> Option<RunOutcome> {
|
||||
if !cc_available() {
|
||||
eprintln!("SKIP {file}: cc not available");
|
||||
return None;
|
||||
}
|
||||
let _guard = FIXTURE_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let (spec, _tmp) = build_spec(file);
|
||||
let opts = SandboxOptions::default();
|
||||
match run_spec(&spec, &opts) {
|
||||
Ok(outcome) => Some(outcome),
|
||||
Err(e) => panic!("run_spec({file}) errored: {e:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn setup_fault_routes_to_unrelated_crash() {
|
||||
let Some(outcome) = run("setup_fault.c") else { return };
|
||||
assert!(
|
||||
outcome.triggered_by.is_none(),
|
||||
"setup_fault must not Confirm — handler is never installed: {outcome:?}",
|
||||
);
|
||||
assert!(
|
||||
outcome.unrelated_crash,
|
||||
"setup_fault must set unrelated_crash so verifier downgrades to Inconclusive(UnrelatedCrash): {outcome:?}",
|
||||
);
|
||||
let any_attempt_crashed = outcome
|
||||
.attempts
|
||||
.iter()
|
||||
.any(|a| a.outcome.exit_code.is_none() && !a.outcome.timed_out);
|
||||
assert!(
|
||||
any_attempt_crashed,
|
||||
"setup_fault constructor must abort the process at least once across attempts",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sink_fault_confirms_via_sink_crash_probe() {
|
||||
let Some(outcome) = run("sink_fault.c") else { return };
|
||||
assert!(
|
||||
outcome.triggered_by.is_some(),
|
||||
"sink_fault must Confirm via SinkCrash + differential: {outcome:?}",
|
||||
);
|
||||
let label = outcome
|
||||
.triggered_by
|
||||
.and_then(|i| outcome.attempts.get(i))
|
||||
.map(|a| a.payload_label);
|
||||
assert_eq!(
|
||||
label,
|
||||
Some("fmt-string-percent-n-crash"),
|
||||
"triggering payload must be the FMT_STRING vuln entry"
|
||||
);
|
||||
assert!(
|
||||
!outcome.unrelated_crash,
|
||||
"sink_fault attempt should NOT set unrelated_crash — probe was written: {outcome:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue