2026-05-05 03:43:45 -04:00
|
|
|
//! Orchestration: spec -> harness -> sandbox -> oracle -> verdict.
|
|
|
|
|
//!
|
2026-05-11 22:56:43 -04:00
|
|
|
//! The runner is the only place that knows about all four submodules at once.
|
|
|
|
|
//! Everything below it (corpus, harness, sandbox) is independent; everything
|
|
|
|
|
//! above it ([`crate::dynamic::verify`]) just calls [`run_spec`] and turns
|
|
|
|
|
//! the result into a [`crate::dynamic::report::VerifyResult`].
|
2026-05-05 03:43:45 -04:00
|
|
|
|
2026-05-11 22:56:43 -04:00
|
|
|
use crate::dynamic::corpus::{benign_payload_for, payloads_for, Oracle, Payload};
|
|
|
|
|
use crate::dynamic::harness::{self, HarnessError};
|
2026-05-05 03:43:45 -04:00
|
|
|
use crate::dynamic::sandbox::{self, SandboxError, SandboxOptions, SandboxOutcome};
|
|
|
|
|
use crate::dynamic::spec::HarnessSpec;
|
|
|
|
|
|
2026-05-11 22:56:43 -04:00
|
|
|
/// Max harness-build attempts before giving up.
|
|
|
|
|
const MAX_BUILD_ATTEMPTS: u32 = 2;
|
|
|
|
|
|
2026-05-05 03:43:45 -04:00
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub struct RunOutcome {
|
|
|
|
|
pub spec: HarnessSpec,
|
|
|
|
|
pub attempts: Vec<Attempt>,
|
2026-05-11 22:56:43 -04:00
|
|
|
/// First attempt that fired the sink with `oracle_fired && sink_hit`.
|
2026-05-05 03:43:45 -04:00
|
|
|
pub triggered_by: Option<usize>,
|
2026-05-11 22:56:43 -04:00
|
|
|
/// Whether the oracle fired but the sink probe did not (oracle collision).
|
|
|
|
|
pub oracle_collision: bool,
|
|
|
|
|
/// Number of build attempts consumed.
|
|
|
|
|
pub build_attempts: u32,
|
|
|
|
|
/// Harness sources for repro artifacts.
|
|
|
|
|
pub harness_source: String,
|
|
|
|
|
pub entry_source: String,
|
2026-05-05 03:43:45 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub struct Attempt {
|
|
|
|
|
pub payload_label: &'static str,
|
|
|
|
|
pub outcome: SandboxOutcome,
|
2026-05-11 22:56:43 -04:00
|
|
|
pub oracle_fired: bool,
|
2026-05-05 03:43:45 -04:00
|
|
|
pub triggered: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub enum RunError {
|
|
|
|
|
NoPayloadsForCap,
|
|
|
|
|
Harness(HarnessError),
|
|
|
|
|
Sandbox(SandboxError),
|
2026-05-11 22:56:43 -04:00
|
|
|
BuildFailed { stderr: String, attempts: u32 },
|
2026-05-05 03:43:45 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<SandboxError> for RunError {
|
|
|
|
|
fn from(e: SandboxError) -> Self {
|
|
|
|
|
RunError::Sandbox(e)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-11 22:56:43 -04:00
|
|
|
/// Build harness (with retry), run every payload, stop at first confirmed trigger.
|
|
|
|
|
///
|
|
|
|
|
/// "Confirmed trigger" = `oracle_fired && sink_hit` (§4.1).
|
|
|
|
|
///
|
|
|
|
|
/// If the oracle fires but the sink probe does not, sets `oracle_collision = true`
|
|
|
|
|
/// and continues (no `triggered_by` is set).
|
2026-05-05 03:43:45 -04:00
|
|
|
pub fn run_spec(spec: &HarnessSpec, opts: &SandboxOptions) -> Result<RunOutcome, RunError> {
|
|
|
|
|
let payloads = payloads_for(spec.expected_cap);
|
|
|
|
|
if payloads.is_empty() {
|
|
|
|
|
return Err(RunError::NoPayloadsForCap);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-11 22:56:43 -04:00
|
|
|
// Build harness with retry.
|
|
|
|
|
const BACKOFF: [u64; 1] = [1];
|
|
|
|
|
let mut build_attempts = 0u32;
|
|
|
|
|
let harness = loop {
|
|
|
|
|
build_attempts += 1;
|
|
|
|
|
match harness::build(spec) {
|
|
|
|
|
Ok(h) => break h,
|
|
|
|
|
Err(HarnessError::BuildFailed(msg)) if build_attempts < MAX_BUILD_ATTEMPTS => {
|
|
|
|
|
std::thread::sleep(std::time::Duration::from_secs(
|
|
|
|
|
BACKOFF[(build_attempts as usize - 1).min(BACKOFF.len() - 1)],
|
|
|
|
|
));
|
|
|
|
|
let _ = msg; // log would go here
|
|
|
|
|
}
|
|
|
|
|
Err(HarnessError::BuildFailed(msg)) => {
|
|
|
|
|
return Err(RunError::BuildFailed {
|
|
|
|
|
stderr: msg,
|
|
|
|
|
attempts: build_attempts,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
Err(e) => return Err(RunError::Harness(e)),
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-05-05 03:43:45 -04:00
|
|
|
|
2026-05-11 22:56:43 -04:00
|
|
|
let harness_source = harness.source.clone();
|
|
|
|
|
let entry_source = harness.entry_source.clone();
|
|
|
|
|
|
|
|
|
|
// Run only vuln (non-benign) payloads in the main loop.
|
|
|
|
|
let vuln_payloads: Vec<&Payload> = payloads.iter().filter(|p| !p.is_benign).collect();
|
|
|
|
|
let benign_payload = benign_payload_for(spec.expected_cap);
|
|
|
|
|
|
|
|
|
|
let mut attempts = Vec::with_capacity(vuln_payloads.len());
|
2026-05-05 03:43:45 -04:00
|
|
|
let mut triggered_by = None;
|
2026-05-11 22:56:43 -04:00
|
|
|
let mut oracle_collision = false;
|
2026-05-05 03:43:45 -04:00
|
|
|
|
2026-05-11 22:56:43 -04:00
|
|
|
for (i, payload) in vuln_payloads.iter().enumerate() {
|
2026-05-05 03:43:45 -04:00
|
|
|
let outcome = sandbox::run(&harness, payload, opts)?;
|
2026-05-11 22:56:43 -04:00
|
|
|
let fired = oracle_fired(&payload.oracle, &outcome);
|
|
|
|
|
let sink_hit = outcome.sink_hit;
|
|
|
|
|
|
|
|
|
|
let triggered = if fired && sink_hit {
|
|
|
|
|
// Full confirmation: oracle + probe both fired.
|
|
|
|
|
// Check differential: if benign payload also triggers oracle, downgrade.
|
|
|
|
|
if let Some(benign) = benign_payload {
|
|
|
|
|
let benign_outcome = sandbox::run(&harness, benign, opts)?;
|
|
|
|
|
let benign_fired = oracle_fired(&benign.oracle, &benign_outcome);
|
|
|
|
|
!benign_fired
|
|
|
|
|
} else {
|
|
|
|
|
true
|
|
|
|
|
}
|
|
|
|
|
} else if fired && !sink_hit {
|
|
|
|
|
// Oracle fired but probe didn't — likely collision.
|
|
|
|
|
oracle_collision = true;
|
|
|
|
|
false
|
|
|
|
|
} else {
|
|
|
|
|
false
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-05 03:43:45 -04:00
|
|
|
attempts.push(Attempt {
|
|
|
|
|
payload_label: payload.label,
|
|
|
|
|
outcome,
|
2026-05-11 22:56:43 -04:00
|
|
|
oracle_fired: fired,
|
2026-05-05 03:43:45 -04:00
|
|
|
triggered,
|
|
|
|
|
});
|
2026-05-11 22:56:43 -04:00
|
|
|
|
2026-05-05 03:43:45 -04:00
|
|
|
if triggered {
|
|
|
|
|
triggered_by = Some(i);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(RunOutcome {
|
|
|
|
|
spec: spec.clone(),
|
|
|
|
|
attempts,
|
|
|
|
|
triggered_by,
|
2026-05-11 22:56:43 -04:00
|
|
|
oracle_collision,
|
|
|
|
|
build_attempts,
|
|
|
|
|
harness_source,
|
|
|
|
|
entry_source,
|
2026-05-05 03:43:45 -04:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn oracle_fired(oracle: &Oracle, outcome: &SandboxOutcome) -> bool {
|
|
|
|
|
match oracle {
|
|
|
|
|
Oracle::OutputContains(needle) => {
|
|
|
|
|
let nb = needle.as_bytes();
|
|
|
|
|
contains_subslice(&outcome.stdout, nb) || contains_subslice(&outcome.stderr, nb)
|
|
|
|
|
}
|
|
|
|
|
Oracle::Crash => matches!(outcome.exit_code, None) && !outcome.timed_out,
|
|
|
|
|
Oracle::OobCallback { .. } => outcome.oob_callback_seen,
|
2026-05-11 22:56:43 -04:00
|
|
|
Oracle::FileEscape => false,
|
2026-05-05 03:43:45 -04:00
|
|
|
Oracle::ExitStatus(code) => outcome.exit_code == Some(*code),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn contains_subslice(hay: &[u8], needle: &[u8]) -> bool {
|
|
|
|
|
if needle.is_empty() || needle.len() > hay.len() {
|
|
|
|
|
return needle.is_empty();
|
|
|
|
|
}
|
|
|
|
|
hay.windows(needle.len()).any(|w| w == needle)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-11 22:56:43 -04:00
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn contains_subslice_empty_needle() {
|
|
|
|
|
assert!(contains_subslice(b"hello", b""));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn contains_subslice_finds_match() {
|
|
|
|
|
assert!(contains_subslice(b"hello world", b"world"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn contains_subslice_no_match() {
|
|
|
|
|
assert!(!contains_subslice(b"hello", b"xyz"));
|
|
|
|
|
}
|
|
|
|
|
}
|