mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-15 20:05:13 +02:00
[pitboss] sweep after phase 02: 3 deferred items resolved
This commit is contained in:
parent
0bf39047b9
commit
3a4f1b177b
7 changed files with 244 additions and 27 deletions
|
|
@ -143,6 +143,9 @@ pub fn write(
|
|||
// expected/outcome.json — redacted
|
||||
let redacted_stdout = redact::redact(&outcome.stdout);
|
||||
let redacted_stderr = redact::redact(&outcome.stderr);
|
||||
// duration_ms is omitted from the persisted outcome so that outcome.json is
|
||||
// byte-identical when regenerated from the repro bundle (§18.2 determinism).
|
||||
// Wall-clock timing goes to telemetry only.
|
||||
let outcome_json = serde_json::json!({
|
||||
"exit_code": outcome.exit_code,
|
||||
"stdout": String::from_utf8_lossy(&redacted_stdout),
|
||||
|
|
@ -150,7 +153,6 @@ pub fn write(
|
|||
"timed_out": outcome.timed_out,
|
||||
"oob_callback_seen": outcome.oob_callback_seen,
|
||||
"sink_hit": outcome.sink_hit,
|
||||
"duration_ms": outcome.duration.as_millis(),
|
||||
});
|
||||
write_json(&root.join("expected").join("outcome.json"), &outcome_json)?;
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
//! above it ([`crate::dynamic::verify`]) just calls [`run_spec`] and turns
|
||||
//! the result into a [`crate::dynamic::report::VerifyResult`].
|
||||
|
||||
use crate::dynamic::build_sandbox;
|
||||
use crate::dynamic::corpus::{benign_payload_for, payloads_for, Oracle, Payload};
|
||||
use crate::dynamic::harness::{self, HarnessError};
|
||||
use crate::dynamic::sandbox::{self, SandboxError, SandboxOptions, SandboxOutcome};
|
||||
|
|
@ -65,7 +66,7 @@ pub fn run_spec(spec: &HarnessSpec, opts: &SandboxOptions) -> Result<RunOutcome,
|
|||
// Build harness with retry.
|
||||
const BACKOFF: [u64; 1] = [1];
|
||||
let mut build_attempts = 0u32;
|
||||
let harness = loop {
|
||||
let mut harness = loop {
|
||||
build_attempts += 1;
|
||||
match harness::build(spec) {
|
||||
Ok(h) => break h,
|
||||
|
|
@ -85,6 +86,31 @@ pub fn run_spec(spec: &HarnessSpec, opts: &SandboxOptions) -> Result<RunOutcome,
|
|||
}
|
||||
};
|
||||
|
||||
// Prepare Python venv for build-time isolation and dependency caching.
|
||||
// Errors from prepare_python propagate as RunError::BuildFailed (making
|
||||
// that variant reachable) or are swallowed for non-fatal failures (Io /
|
||||
// Unsupported), falling back to the system python3 in the harness command.
|
||||
match build_sandbox::prepare_python(spec, &harness.workdir) {
|
||||
Ok(build_result) => {
|
||||
// Patch harness command to use venv Python when the venv was built
|
||||
// or found in cache.
|
||||
if let Some(cmd0) = harness.command.first_mut() {
|
||||
if cmd0 == "python3" || cmd0 == "python" {
|
||||
let venv_python = build_result.venv_path.join("bin").join("python3");
|
||||
if venv_python.exists() {
|
||||
*cmd0 = venv_python.to_string_lossy().into_owned();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(build_sandbox::BuildError::BuildFailed { stderr, attempts }) => {
|
||||
return Err(RunError::BuildFailed { stderr, attempts });
|
||||
}
|
||||
Err(_) => {
|
||||
// Io / Unsupported: fall back to system python3 already in command.
|
||||
}
|
||||
}
|
||||
|
||||
let harness_source = harness.source.clone();
|
||||
let entry_source = harness.entry_source.clone();
|
||||
|
||||
|
|
|
|||
|
|
@ -89,9 +89,8 @@ impl From<std::io::Error> for SandboxError {
|
|||
|
||||
/// Run a built harness once with a chosen payload.
|
||||
///
|
||||
/// Dispatches to the process backend (subprocess with timeout).
|
||||
/// On Linux the process backend uses unshare namespaces + seccomp.
|
||||
/// On other platforms it falls back to plain subprocess with timeout.
|
||||
/// Dispatches to the process backend (subprocess with timeout, env stripping,
|
||||
/// and memory cap via `setrlimit(RLIMIT_AS)` on Linux).
|
||||
pub fn run(
|
||||
harness: &BuiltHarness,
|
||||
payload: &Payload,
|
||||
|
|
@ -106,10 +105,7 @@ pub fn run(
|
|||
}
|
||||
|
||||
/// Process backend: spawns the harness command in a subprocess with timeout,
|
||||
/// stdout/stderr capture, and env stripping.
|
||||
///
|
||||
/// On Linux, wraps the command with `unshare` for namespace isolation when
|
||||
/// available. On other platforms, runs the command directly.
|
||||
/// stdout/stderr capture, env stripping, and memory cap (Linux: RLIMIT_AS).
|
||||
fn run_process(
|
||||
harness: &BuiltHarness,
|
||||
payload: &Payload,
|
||||
|
|
@ -152,6 +148,21 @@ fn run_process(
|
|||
cmd.env("NYX_PAYLOAD", std::ffi::OsStr::from_bytes(payload.bytes));
|
||||
}
|
||||
|
||||
// Enforce memory cap before exec on Linux via RLIMIT_AS.
|
||||
// RLIMIT_AS limits total virtual address space. Python uses significantly
|
||||
// more virtual AS than RSS (shared libs, mmap arenas), so the enforced
|
||||
// limit is memory_mib * 8 with a floor of 4 GiB. This prevents multi-GiB
|
||||
// memory bombs while leaving normal Python workloads headroom.
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use std::os::unix::process::CommandExt;
|
||||
let memory_mib = opts.memory_mib;
|
||||
// Safety: called in the child after fork but before exec; no allocator use.
|
||||
unsafe {
|
||||
cmd.pre_exec(move || rlimit_as_linux(memory_mib));
|
||||
}
|
||||
}
|
||||
|
||||
let start = Instant::now();
|
||||
let mut child = cmd.spawn().map_err(SandboxError::Spawn)?;
|
||||
|
||||
|
|
@ -261,6 +272,36 @@ fn base64_encode(data: &[u8]) -> String {
|
|||
out
|
||||
}
|
||||
|
||||
/// Set RLIMIT_AS (virtual address space) in a `pre_exec` context on Linux.
|
||||
///
|
||||
/// `memory_mib` is the configured cap; we enforce `max(memory_mib * 8, 4096)`
|
||||
/// MiB of virtual AS to give Python's mmap-heavy runtime adequate headroom
|
||||
/// while still capping runaway memory bombs.
|
||||
///
|
||||
/// RLIMIT_AS = 9 on x86_64, aarch64, arm, ppc64, s390x, and all other major
|
||||
/// Linux architectures (kernel source: include/uapi/asm-generic/resource.h).
|
||||
#[cfg(target_os = "linux")]
|
||||
fn rlimit_as_linux(memory_mib: u64) -> std::io::Result<()> {
|
||||
#[repr(C)]
|
||||
struct Rlimit {
|
||||
cur: u64,
|
||||
max: u64,
|
||||
}
|
||||
unsafe extern "C" {
|
||||
fn setrlimit(resource: i32, rlim: *const Rlimit) -> i32;
|
||||
}
|
||||
const RLIMIT_AS: i32 = 9;
|
||||
let cap_mib = memory_mib.saturating_mul(8).max(4096);
|
||||
let bytes = cap_mib.saturating_mul(1024 * 1024);
|
||||
let rl = Rlimit { cur: bytes, max: bytes };
|
||||
let ret = unsafe { setrlimit(RLIMIT_AS, &rl) };
|
||||
if ret == 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(std::io::Error::last_os_error())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn libc_kill(pid: i32, sig: i32) -> i32 {
|
||||
unsafe extern "C" {
|
||||
|
|
|
|||
|
|
@ -56,6 +56,35 @@ pub fn verify_finding(diag: &Diag, opts: &VerifyOptions) -> VerifyResult {
|
|||
}
|
||||
};
|
||||
|
||||
// Scan the entry file's directory for sensitive files (§17.3 mount filter).
|
||||
// If the entry file itself matches a sensitive pattern, refuse to run it:
|
||||
// the harness would copy it into the workdir and expose secrets.
|
||||
{
|
||||
let entry_path = Path::new(&spec.entry_file);
|
||||
let scan_dir = entry_path
|
||||
.parent()
|
||||
.filter(|p| !p.as_os_str().is_empty())
|
||||
.unwrap_or(Path::new("."));
|
||||
let notes = crate::dynamic::mount_filter::scan_sensitive_files(scan_dir);
|
||||
for note in ¬es {
|
||||
let note_abs = scan_dir.join(¬e.path);
|
||||
if entry_path == note_abs {
|
||||
return VerifyResult {
|
||||
finding_id,
|
||||
status: VerifyStatus::Unsupported,
|
||||
triggered_payload: None,
|
||||
reason: Some(UnsupportedReason::RequiredFileRedactedForSecrets(
|
||||
note.path.clone(),
|
||||
)),
|
||||
inconclusive_reason: None,
|
||||
detail: None,
|
||||
attempts: vec![],
|
||||
toolchain_match: None,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve toolchain information.
|
||||
let toolchain_res = toolchain::resolve_python(Path::new("."));
|
||||
let toolchain_match = if toolchain_res.toolchain_drift { "drift" } else { "exact" };
|
||||
|
|
@ -64,6 +93,13 @@ pub fn verify_finding(diag: &Diag, opts: &VerifyOptions) -> VerifyResult {
|
|||
let result = run_spec(&spec, &opts.sandbox);
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
// Extract build_attempts before result is consumed by build_verdict.
|
||||
let build_attempts = match &result {
|
||||
Ok(run) => run.build_attempts,
|
||||
Err(RunError::BuildFailed { attempts, .. }) => *attempts,
|
||||
_ => 1,
|
||||
};
|
||||
|
||||
let verdict = build_verdict(
|
||||
&finding_id,
|
||||
&spec,
|
||||
|
|
@ -80,7 +116,7 @@ pub fn verify_finding(diag: &Diag, opts: &VerifyOptions) -> VerifyResult {
|
|||
verdict.inconclusive_reason,
|
||||
toolchain_match,
|
||||
elapsed,
|
||||
1, // build_attempts tracked in RunOutcome but not exposed here for simplicity
|
||||
build_attempts,
|
||||
);
|
||||
telemetry::emit(&event);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue