cargo fmt

This commit is contained in:
elipeter 2026-05-21 14:35:42 -05:00
parent bec7bbf96c
commit 3a35cd6c8f
294 changed files with 6809 additions and 3911 deletions

View file

@ -90,7 +90,11 @@ pub fn ensure_image_pulled(image: &str) -> bool {
// 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) };
let ok = if docker_image_present(image) {
true
} else {
docker_pull(image)
};
cache.insert(image.to_owned(), ok);
ok
}
@ -249,7 +253,10 @@ mod tests {
.expect("oob listener must bind on 127.0.0.1 in tests"),
);
let args = network_args(&NetworkPolicy::OobOutbound { listener });
assert!(args.iter().any(|a| a == "--add-host=host-gateway:host-gateway"));
assert!(
args.iter()
.any(|a| a == "--add-host=host-gateway:host-gateway")
);
}
#[test]
@ -261,8 +268,8 @@ mod tests {
fn image_reference_for_toolchain_known_returns_pinned_digest() {
// The catalogue ships with hand-seeded sha256 digests for every
// catalogue entry, so known IDs resolve to `<base>@sha256:…` refs.
let r = image_reference_for_toolchain("python-3.11")
.expect("python-3.11 is in the catalogue");
let r =
image_reference_for_toolchain("python-3.11").expect("python-3.11 is in the catalogue");
assert!(r.starts_with("python:3.11-slim@sha256:"), "got {r}");
}

View file

@ -77,11 +77,15 @@ pub fn run(
_opts: &SandboxOptions,
) -> Result<SandboxOutcome, SandboxError> {
if !firecracker_available() {
return Err(SandboxError::BackendUnavailable(SandboxBackend::Firecracker));
return Err(SandboxError::BackendUnavailable(
SandboxBackend::Firecracker,
));
}
// Binary present but no VM logic yet. Surface BackendUnavailable
// explicitly so callers do not mistakenly think the run succeeded.
Err(SandboxError::BackendUnavailable(SandboxBackend::Firecracker))
Err(SandboxError::BackendUnavailable(
SandboxBackend::Firecracker,
))
}
#[cfg(test)]
@ -122,7 +126,9 @@ mod tests {
let result = run(&harness, b"", &opts);
assert!(matches!(
result,
Err(SandboxError::BackendUnavailable(SandboxBackend::Firecracker))
Err(SandboxError::BackendUnavailable(
SandboxBackend::Firecracker
))
));
}
}

View file

@ -24,7 +24,7 @@
use crate::dynamic::harness::BuiltHarness;
use crate::dynamic::oob::OobListener;
use crate::dynamic::probe::{ProbeChannel, PROBE_PATH_ENV};
use crate::dynamic::probe::{PROBE_PATH_ENV, ProbeChannel};
use std::path::{Path, PathBuf};
use std::sync::{Arc, OnceLock};
use std::time::{Duration, Instant};
@ -276,15 +276,13 @@ pub struct SandboxOptions {
/// default-deny seccomp filter scoped to [`SandboxOptions::seccomp_caps`].
/// Each primitive is best-effort; failures degrade to
/// [`HardeningLevel::Partial`] without aborting the run.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Default)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ProcessHardeningProfile {
#[default]
Standard,
Strict,
}
/// Phase 20 follow-up (Track E.4 ablation harness): selectively skip or
/// loosen individual Strict-profile primitives so the escape-fixture
/// matrix can prove the acceptance literal "removing any one Phase 17
@ -387,7 +385,10 @@ pub struct HostPort {
impl HostPort {
pub fn new(host: impl Into<String>, port: u16) -> Self {
Self { host: host.into(), port }
Self {
host: host.into(),
port,
}
}
}
@ -415,13 +416,16 @@ impl HostPort {
/// - [`NetworkPolicy::Open`] — unrestricted outbound. Docker: `bridge`
/// with no egress filter. Reserved for diagnostic / dev-only runs;
/// the verifier never sets this in production.
#[derive(Debug, Clone)]
#[derive(Default)]
#[derive(Debug, Clone, Default)]
pub enum NetworkPolicy {
#[default]
None,
StubsOnly { allow: Vec<HostPort> },
OobOutbound { listener: Arc<OobListener> },
StubsOnly {
allow: Vec<HostPort>,
},
OobOutbound {
listener: Arc<OobListener>,
},
Open,
}
@ -460,7 +464,6 @@ impl NetworkPolicy {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SandboxBackend {
Auto,
@ -590,14 +593,14 @@ fn apply_oob_egress_filter(container_name: &str, oob_port: u16) {
let rules: &[&[&str]] = &[
// Allow container → host OOB port (INPUT; docker0 bridge to host).
&["-I", "INPUT", "1", "-i", "docker0",
"-s", ip, "-p", "tcp", "--dport", &port_str, "-j", "ACCEPT"],
&[
"-I", "INPUT", "1", "-i", "docker0", "-s", ip, "-p", "tcp", "--dport", &port_str, "-j",
"ACCEPT",
],
// Drop all other container → host traffic (INPUT; position 2 fires after accept).
&["-I", "INPUT", "2", "-i", "docker0",
"-s", ip, "-j", "DROP"],
&["-I", "INPUT", "2", "-i", "docker0", "-s", ip, "-j", "DROP"],
// Drop all container egress to external internet (FORWARD / DOCKER-USER).
&["-I", "DOCKER-USER", "1",
"-s", ip, "-j", "DROP"],
&["-I", "DOCKER-USER", "1", "-s", ip, "-j", "DROP"],
];
let mut applied = 0usize;
@ -617,7 +620,10 @@ fn apply_oob_egress_filter(container_name: &str, oob_port: u16) {
if applied == rules.len() {
oob_egress_registry().insert(
container_name.to_owned(),
OobEgressState { container_ip, oob_port },
OobEgressState {
container_ip,
oob_port,
},
);
} else {
eprintln!(
@ -644,12 +650,12 @@ fn remove_oob_egress_filter(container_name: &str) {
let ip = state.container_ip.as_str();
let rules: &[&[&str]] = &[
&["-D", "INPUT", "-i", "docker0",
"-s", ip, "-p", "tcp", "--dport", &port_str, "-j", "ACCEPT"],
&["-D", "INPUT", "-i", "docker0",
"-s", ip, "-j", "DROP"],
&["-D", "DOCKER-USER",
"-s", ip, "-j", "DROP"],
&[
"-D", "INPUT", "-i", "docker0", "-s", ip, "-p", "tcp", "--dport", &port_str, "-j",
"ACCEPT",
],
&["-D", "INPUT", "-i", "docker0", "-s", ip, "-j", "DROP"],
&["-D", "DOCKER-USER", "-s", ip, "-j", "DROP"],
];
for rule in rules {
@ -680,7 +686,9 @@ fn container_registry() -> &'static dashmap::DashMap<String, String> {
/// on SIGKILL; the `sleep 300` in started containers bounds the leak window.
#[cfg(unix)]
extern "C" fn stop_all_containers() {
let Some(reg) = CONTAINER_REGISTRY.get() else { return };
let Some(reg) = CONTAINER_REGISTRY.get() else {
return;
};
let bin = std::env::var("NYX_DOCKER_BIN").unwrap_or_else(|_| "docker".to_owned());
for entry in reg.iter() {
// Remove OOB egress filter before stopping the container so stale
@ -779,10 +787,7 @@ pub fn run(
// backend in that case so the harness picks up the host
// venv / node_modules / vendor dir already prepared.
let needs_host_deps = harness_needs_host_deps(harness);
if docker_available()
&& harness_is_interpreted(&harness.command)
&& !needs_host_deps
{
if docker_available() && harness_is_interpreted(&harness.command) && !needs_host_deps {
run_docker(harness, payload_bytes, opts)
} else if docker_available() && harness_is_native_binary(&harness.command) {
run_native_binary_docker(harness, payload_bytes, opts)
@ -841,7 +846,9 @@ fn run_firecracker(
}
#[cfg(not(feature = "firecracker"))]
{
Err(SandboxError::BackendUnavailable(SandboxBackend::Firecracker))
Err(SandboxError::BackendUnavailable(
SandboxBackend::Firecracker,
))
}
}
@ -880,12 +887,9 @@ fn rewrite_extra_env_for_container(
&& let Some(idx) = fs_stub_roots
.iter()
.position(|p| p.as_os_str() == std::ffi::OsStr::new(v))
{
return (
k.clone(),
format!("{}/{idx}", docker::STUB_MOUNT_ROOT),
);
}
{
return (k.clone(), format!("{}/{idx}", docker::STUB_MOUNT_ROOT));
}
(k.clone(), v.clone())
})
.collect()
@ -930,7 +934,13 @@ fn run_docker(
registry.insert(container_name.clone(), container_name.clone());
}
exec_in_container(&container_name, harness, payload_bytes, opts, &fs_stub_roots)
exec_in_container(
&container_name,
harness,
payload_bytes,
opts,
&fs_stub_roots,
)
}
/// Returns true when `docker info` succeeds using the current `NYX_DOCKER_BIN`.
@ -998,16 +1008,20 @@ fn start_container(
"run".into(),
"-d".into(),
"--rm".into(),
"--name".into(), name.into(),
"--name".into(),
name.into(),
"--cap-drop=ALL".into(),
"--security-opt".into(), "no-new-privileges:true".into(),
"--tmpfs".into(), "/tmp:size=128m,exec".into(),
"--security-opt".into(),
"no-new-privileges:true".into(),
"--tmpfs".into(),
"/tmp:size=128m,exec".into(),
// Bind-mount the host workdir at the fixed `/work` path
// read-write so harness code can reference `/work/...` without
// threading the host tempdir through every layer. The mount
// alone is sufficient to deliver harness files into the
// container — no follow-up `docker cp` is needed.
"-v".into(), workdir_mount,
"-v".into(),
workdir_mount,
];
// Phase 10 / Phase 19 (Track D.3 + E.3): bind-mount each
// filesystem-stub root at `STUB_MOUNT_ROOT/<idx>:rw` so the
@ -1141,8 +1155,10 @@ fn exec_in_container(
// checks provide a second layer of defence on top of --cap-drop=ALL.
// The container itself starts as root for setup (mkdir, docker cp),
// but harness execution runs as nobody (uid/gid 65534).
"--user".into(), "65534:65534".into(),
"-e".into(), format!("NYX_PAYLOAD_B64={payload_b64}"),
"--user".into(),
"65534:65534".into(),
"-e".into(),
format!("NYX_PAYLOAD_B64={payload_b64}"),
];
// Mirror the process backend's `NYX_PAYLOAD` raw env var when the
// payload bytes are valid UTF-8 (most curated payloads are ASCII).
@ -1157,10 +1173,11 @@ fn exec_in_container(
// non-UTF-8 payloads (a `docker -e` argument must be valid UTF-8),
// leaving consumers to decode `NYX_PAYLOAD_B64` themselves.
if let Ok(s) = std::str::from_utf8(payload_bytes)
&& !s.contains('\0') {
cmd_args.push("-e".into());
cmd_args.push(format!("NYX_PAYLOAD={s}"));
}
&& !s.contains('\0')
{
cmd_args.push("-e".into());
cmd_args.push(format!("NYX_PAYLOAD={s}"));
}
// Forward harness-specific env vars.
for (k, v) in &harness.env {
cmd_args.push("-e".into());
@ -1276,7 +1293,11 @@ fn exec_in_container(
/// fall through to the legacy tag mapping below so behaviour on a fresh
/// catalogue stays unchanged.
fn detect_image_for_harness(harness: &BuiltHarness) -> String {
let cmd0 = harness.command.first().map(|s| s.as_str()).unwrap_or("python3");
let cmd0 = harness
.command
.first()
.map(|s| s.as_str())
.unwrap_or("python3");
let base = std::path::Path::new(cmd0)
.file_name()
.and_then(|n| n.to_str())
@ -1329,10 +1350,12 @@ fn run_native_binary_docker(
let binary_path = match harness.command.first() {
Some(p) => p.clone(),
None => return Err(SandboxError::Spawn(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"empty command for native binary",
))),
None => {
return Err(SandboxError::Spawn(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"empty command for native binary",
)));
}
};
let container_name = workdir_to_container_name(&harness.workdir);
@ -1385,7 +1408,13 @@ fn run_native_binary_docker(
registry.insert(container_name.clone(), container_name.clone());
}
exec_native_binary_in_container(&container_name, harness, payload_bytes, opts, &fs_stub_roots)
exec_native_binary_in_container(
&container_name,
harness,
payload_bytes,
opts,
&fs_stub_roots,
)
}
/// Execute a native binary already in the container at `/work/nyx_harness`.
@ -1403,8 +1432,10 @@ fn exec_native_binary_in_container(
let mut cmd_args: Vec<String> = vec![
"exec".into(),
"-i".into(),
"--user".into(), "65534:65534".into(),
"-e".into(), format!("NYX_PAYLOAD_B64={payload_b64}"),
"--user".into(),
"65534:65534".into(),
"-e".into(),
format!("NYX_PAYLOAD_B64={payload_b64}"),
];
for (k, v) in &harness.env {
cmd_args.push("-e".into());
@ -1566,10 +1597,8 @@ fn run_process(
None => (resolved_cmd_path.clone(), harness.command[1..].to_vec()),
};
#[cfg(not(target_os = "macos"))]
let (effective_cmd_path, effective_cmd_args): (std::path::PathBuf, Vec<String>) = (
resolved_cmd_path.clone(),
harness.command[1..].to_vec(),
);
let (effective_cmd_path, effective_cmd_args): (std::path::PathBuf, Vec<String>) =
(resolved_cmd_path.clone(), harness.command[1..].to_vec());
let mut cmd = Command::new(&effective_cmd_path);
cmd.args(&effective_cmd_args);
@ -1894,9 +1923,15 @@ mod tests {
#[test]
fn python_image_for_known_toolchains() {
assert_eq!(python_image_for_toolchain("python-3.11"), "python:3.11-slim");
assert_eq!(
python_image_for_toolchain("python-3.11"),
"python:3.11-slim"
);
assert_eq!(python_image_for_toolchain("python-3"), "python:3-slim");
assert_eq!(python_image_for_toolchain("python-3.12"), "python:3.12-slim");
assert_eq!(
python_image_for_toolchain("python-3.12"),
"python:3.12-slim"
);
}
#[test]
@ -1908,8 +1943,14 @@ mod tests {
#[test]
fn java_image_for_known_toolchains() {
assert_eq!(java_image_for_toolchain("java-21"), "eclipse-temurin:21-jre-jammy");
assert_eq!(java_image_for_toolchain("java-17"), "eclipse-temurin:17-jre-jammy");
assert_eq!(
java_image_for_toolchain("java-21"),
"eclipse-temurin:21-jre-jammy"
);
assert_eq!(
java_image_for_toolchain("java-17"),
"eclipse-temurin:17-jre-jammy"
);
}
#[test]
@ -1927,13 +1968,21 @@ mod tests {
#[test]
fn harness_is_interpreted_java() {
let cmd = vec!["java".to_owned(), "-cp".to_owned(), ".".to_owned(), "NyxHarness".to_owned()];
let cmd = vec![
"java".to_owned(),
"-cp".to_owned(),
".".to_owned(),
"NyxHarness".to_owned(),
];
assert!(harness_is_interpreted(&cmd));
}
#[test]
fn harness_is_interpreted_node() {
assert!(harness_is_interpreted(&["node".to_owned(), "harness.js".to_owned()]));
assert!(harness_is_interpreted(&[
"node".to_owned(),
"harness.js".to_owned()
]));
}
#[test]
@ -2076,7 +2125,10 @@ mod tests {
fn fetch_docker_image_digest_short_returns_empty_on_bad_image() {
// A non-existent image tag always returns empty (inspect fails).
let digest = fetch_docker_image_digest_short("nyx-nonexistent-image:does-not-exist-99999");
assert!(digest.is_empty(), "non-existent image must return empty digest");
assert!(
digest.is_empty(),
"non-existent image must return empty digest"
);
}
#[test]
@ -2174,7 +2226,10 @@ mod tests {
fn rewrite_extra_env_passes_unrelated_pairs_through() {
let extra = vec![
("NYX_SQL_ENDPOINT".to_owned(), "/tmp/abc.db".to_owned()),
("NYX_HTTP_ENDPOINT".to_owned(), "http://127.0.0.1:12345".to_owned()),
(
"NYX_HTTP_ENDPOINT".to_owned(),
"http://127.0.0.1:12345".to_owned(),
),
];
let out = rewrite_extra_env_for_container(&extra, &[]);
assert_eq!(out, extra);
@ -2183,9 +2238,10 @@ mod tests {
#[test]
fn rewrite_extra_env_maps_fs_root_to_container_mount() {
let host_root = PathBuf::from("/tmp/host-fs-root-abc");
let extra = vec![
("NYX_FS_ROOT".to_owned(), host_root.to_string_lossy().into_owned()),
];
let extra = vec![(
"NYX_FS_ROOT".to_owned(),
host_root.to_string_lossy().into_owned(),
)];
let out = rewrite_extra_env_for_container(&extra, &[host_root]);
assert_eq!(out.len(), 1);
assert_eq!(out[0].0, "NYX_FS_ROOT");
@ -2198,13 +2254,8 @@ mod tests {
// active fs_stub_roots list is passed through unchanged. This
// keeps the rewrite from accidentally clobbering an emitter-
// supplied placeholder.
let extra = vec![
("NYX_FS_ROOT".to_owned(), "/some/host/path".to_owned()),
];
let out = rewrite_extra_env_for_container(
&extra,
&[PathBuf::from("/different/host/path")],
);
let extra = vec![("NYX_FS_ROOT".to_owned(), "/some/host/path".to_owned())];
let out = rewrite_extra_env_for_container(&extra, &[PathBuf::from("/different/host/path")]);
assert_eq!(out, extra);
}
@ -2212,9 +2263,10 @@ mod tests {
fn rewrite_extra_env_indexes_multiple_fs_roots() {
let root_a = PathBuf::from("/tmp/fs-a");
let root_b = PathBuf::from("/tmp/fs-b");
let extra = vec![
("NYX_FS_ROOT".to_owned(), root_b.to_string_lossy().into_owned()),
];
let extra = vec![(
"NYX_FS_ROOT".to_owned(),
root_b.to_string_lossy().into_owned(),
)];
let out = rewrite_extra_env_for_container(&extra, &[root_a, root_b]);
assert_eq!(out[0].1, format!("{}/1", docker::STUB_MOUNT_ROOT));
}
@ -2229,11 +2281,9 @@ mod tests {
fn collect_fs_stub_roots_returns_paths_for_filesystem_stubs() {
use crate::dynamic::stubs::StubKind;
let dir = tempfile::TempDir::new().expect("tempdir");
let harness = crate::dynamic::stubs::StubHarness::start(
&[StubKind::Filesystem],
dir.path(),
)
.expect("start stub harness");
let harness =
crate::dynamic::stubs::StubHarness::start(&[StubKind::Filesystem], dir.path())
.expect("start stub harness");
let endpoint = harness.stubs()[0].endpoint();
let opts = SandboxOptions {
stub_harness: Some(Arc::new(harness)),
@ -2248,11 +2298,9 @@ mod tests {
fn collect_fs_stub_roots_skips_network_stubs() {
use crate::dynamic::stubs::StubKind;
let dir = tempfile::TempDir::new().expect("tempdir");
let harness = crate::dynamic::stubs::StubHarness::start(
&[StubKind::Http, StubKind::Sql],
dir.path(),
)
.expect("start stub harness");
let harness =
crate::dynamic::stubs::StubHarness::start(&[StubKind::Http, StubKind::Sql], dir.path())
.expect("start stub harness");
let opts = SandboxOptions {
stub_harness: Some(Arc::new(harness)),
..SandboxOptions::default()

View file

@ -119,8 +119,14 @@ impl HardeningOutcome {
self.chroot,
self.seccomp,
];
let applied = primitives.iter().filter(|s| matches!(s, PrimitiveStatus::Applied)).count();
let failed = primitives.iter().filter(|s| matches!(s, PrimitiveStatus::Failed(_))).count();
let applied = primitives
.iter()
.filter(|s| matches!(s, PrimitiveStatus::Applied))
.count();
let failed = primitives
.iter()
.filter(|s| matches!(s, PrimitiveStatus::Failed(_)))
.count();
match (applied, failed) {
(_, 0) => HardeningLevel::Full,
(0, _) => HardeningLevel::None,
@ -147,7 +153,10 @@ impl StatusPipe {
if ret != 0 {
return Err(std::io::Error::last_os_error());
}
Ok(Self { write_fd: fds[1], read_fd: fds[0] })
Ok(Self {
write_fd: fds[1],
read_fd: fds[0],
})
}
}
@ -289,7 +298,10 @@ fn last_errno() -> i32 {
}
fn apply_rlimit(resource: i32, bytes: u64) -> PrimitiveStatus {
let rl = Rlimit { cur: bytes, max: bytes };
let rl = Rlimit {
cur: bytes,
max: bytes,
};
let ret = unsafe { setrlimit(resource, &rl) };
if ret == 0 {
PrimitiveStatus::Applied
@ -498,7 +510,9 @@ impl OutcomeCollector {
close_fd(self.write_fd);
let read_fd = self.read_fd;
let handle = std::thread::spawn(move || drain_outcome(read_fd));
OutcomeJoiner { handle: Some(handle) }
OutcomeJoiner {
handle: Some(handle),
}
}
/// Call when `cmd.spawn()` failed. Closes both ends so neither fd
@ -607,10 +621,8 @@ fn build_plan(opts: &SandboxOptions, workdir: &Path) -> PreExecPlan {
// prove that the corresponding seccomp slice carries its weight.
let ablation = opts.ablation;
let extras: Vec<&'static str> = ablation_extras(ablation);
let nrs = seccomp::allowed_syscall_numbers_with_extras(
opts.seccomp_caps,
extras.iter().copied(),
);
let nrs =
seccomp::allowed_syscall_numbers_with_extras(opts.seccomp_caps, extras.iter().copied());
let program = seccomp::bpf::compile(&nrs, seccomp::syscalls::AUDIT_ARCH);
let profile = match opts.process_hardening {
@ -718,7 +730,8 @@ fn nul_terminate(bytes: &[u8]) -> Vec<u8> {
}
fn canonicalize_workdir(workdir: &Path) -> Vec<u8> {
let canonical: PathBuf = std::fs::canonicalize(workdir).unwrap_or_else(|_| workdir.to_path_buf());
let canonical: PathBuf =
std::fs::canonicalize(workdir).unwrap_or_else(|_| workdir.to_path_buf());
let mut bytes = canonical.into_os_string().into_encoded_bytes();
if !bytes.ends_with(&[0]) {
bytes.push(0);
@ -797,20 +810,30 @@ mod tests {
let plan = build_plan(&opts, std::path::Path::new("/tmp"));
// The arch check + ld nr + KILL + ALLOW alone are 5 instructions;
// the BASE allowlist adds dozens more.
assert!(plan.seccomp_program.len() > 5, "BPF program too small: {}", plan.seccomp_program.len());
assert!(
plan.seccomp_program.len() > 5,
"BPF program too small: {}",
plan.seccomp_program.len()
);
assert_eq!(plan.profile, ProcessHardeningProfileTag::Strict);
}
#[test]
fn rlimit_as_bytes_floors_at_4_gib() {
let opts = SandboxOptions { memory_mib: 1, ..SandboxOptions::default() };
let opts = SandboxOptions {
memory_mib: 1,
..SandboxOptions::default()
};
let plan = build_plan(&opts, std::path::Path::new("/tmp"));
assert_eq!(plan.rlimit_as_bytes, 4096_u64 * 1024 * 1024);
}
#[test]
fn rlimit_as_bytes_scales_with_memory_mib() {
let opts = SandboxOptions { memory_mib: 1024, ..SandboxOptions::default() };
let opts = SandboxOptions {
memory_mib: 1024,
..SandboxOptions::default()
};
let plan = build_plan(&opts, std::path::Path::new("/tmp"));
// 1024 MiB * 8 = 8192 MiB
assert_eq!(plan.rlimit_as_bytes, 8192_u64 * 1024 * 1024);
@ -865,8 +888,14 @@ mod tests {
// Every entry's source must be NUL-terminated for the `mount(2)`
// call, and every dest must exist on disk.
for m in &plan.bind_mounts {
assert!(m.source_nul.ends_with(&[0]), "source path must be NUL-terminated");
assert!(m.dest_nul.ends_with(&[0]), "dest path must be NUL-terminated");
assert!(
m.source_nul.ends_with(&[0]),
"source path must be NUL-terminated"
);
assert!(
m.dest_nul.ends_with(&[0]),
"dest path must be NUL-terminated"
);
let dest_str = std::str::from_utf8(&m.dest_nul[..m.dest_nul.len() - 1])
.expect("dest path must be valid UTF-8");
assert!(
@ -920,8 +949,16 @@ mod tests {
..AblationMask::default()
}));
assert_eq!(flags & CLONE_NEWUSER, 0, "CLONE_NEWUSER must be dropped");
assert_eq!(flags & CLONE_NEWPID, CLONE_NEWPID, "CLONE_NEWPID must persist");
assert_eq!(flags & CLONE_NEWNS, CLONE_NEWNS, "CLONE_NEWNS must persist (bind-mount target)");
assert_eq!(
flags & CLONE_NEWPID,
CLONE_NEWPID,
"CLONE_NEWPID must persist"
);
assert_eq!(
flags & CLONE_NEWNS,
CLONE_NEWNS,
"CLONE_NEWNS must persist (bind-mount target)"
);
}
#[test]
@ -931,7 +968,11 @@ mod tests {
..AblationMask::default()
}));
assert_eq!(flags & CLONE_NEWPID, 0, "CLONE_NEWPID must be dropped");
assert_eq!(flags & CLONE_NEWUSER, CLONE_NEWUSER, "CLONE_NEWUSER must persist");
assert_eq!(
flags & CLONE_NEWUSER,
CLONE_NEWUSER,
"CLONE_NEWUSER must persist"
);
}
#[test]
@ -1054,8 +1095,8 @@ mod tests {
..SandboxOptions::default()
};
let plan = build_plan(&opts, std::path::Path::new("/tmp"));
let socket_nr = seccomp::syscalls::syscall_number("socket")
.expect("socket in per-arch syscall map");
let socket_nr =
seccomp::syscalls::syscall_number("socket").expect("socket in per-arch syscall map");
// BPF compile emits one JEQ per allowed syscall (+ a fixed arch
// prelude + a default-deny tail), so encoding socket as a JEQ
// instruction's k-field is the load-bearing signal.
@ -1080,8 +1121,8 @@ mod tests {
..SandboxOptions::default()
};
let plan = build_plan(&opts, std::path::Path::new("/tmp"));
let setuid_nr = seccomp::syscalls::syscall_number("setuid")
.expect("setuid in per-arch syscall map");
let setuid_nr =
seccomp::syscalls::syscall_number("setuid").expect("setuid in per-arch syscall map");
let program = plan.seccomp_program.as_slice();
let landed = program.iter().any(|insn| insn.k == setuid_nr);
assert!(
@ -1104,8 +1145,8 @@ mod tests {
..SandboxOptions::default()
};
let plan = build_plan(&opts, std::path::Path::new("/tmp"));
let socket_nr = seccomp::syscalls::syscall_number("socket")
.expect("socket in per-arch syscall map");
let socket_nr =
seccomp::syscalls::syscall_number("socket").expect("socket in per-arch syscall map");
let landed = plan.seccomp_program.iter().any(|insn| insn.k == socket_nr);
assert!(
!landed,
@ -1148,5 +1189,4 @@ mod tests {
outcome.no_new_privs,
);
}
}

View file

@ -124,7 +124,10 @@ const PROFILE_SOURCES: &[(&str, &str)] = &[
include_str!("../sandbox_profiles/path_traversal.sb"),
),
("ssrf", include_str!("../sandbox_profiles/ssrf.sb")),
("deserialize", include_str!("../sandbox_profiles/deserialize.sb")),
(
"deserialize",
include_str!("../sandbox_profiles/deserialize.sb"),
),
("xxe", include_str!("../sandbox_profiles/xxe.sb")),
(
"open_redirect",
@ -305,9 +308,7 @@ pub fn splice_deny_default(source: &str, seed: &str) -> String {
rewritten.push('\n');
}
rewritten.push('\n');
rewritten.push_str(
";; ── deny-default seed (spliced by NYX_SB_DENY_DEFAULT=1) ──────────\n",
);
rewritten.push_str(";; ── deny-default seed (spliced by NYX_SB_DENY_DEFAULT=1) ──────────\n");
rewritten.push_str(seed.trim_end());
rewritten.push('\n');
rewritten
@ -378,7 +379,9 @@ pub fn wrap_plan(input: &WrapInput<'_>) -> WrapResult {
},
};
}
let profile = input.profile_override.unwrap_or_else(|| profile_for_caps(input.caps));
let profile = input
.profile_override
.unwrap_or_else(|| profile_for_caps(input.caps));
// Profile keys must be `&'static str` (from `PROFILE_SOURCES`); reject
// unknown overrides up-front so we don't accidentally wrap with a
// profile we have no source for.
@ -411,7 +414,8 @@ pub fn wrap_plan(input: &WrapInput<'_>) -> WrapResult {
}
};
let workdir_abs = std::fs::canonicalize(input.workdir).unwrap_or_else(|_| input.workdir.to_path_buf());
let workdir_abs =
std::fs::canonicalize(input.workdir).unwrap_or_else(|_| input.workdir.to_path_buf());
let mut args: Vec<String> = Vec::with_capacity(6 + input.cmd_args.len());
args.push("-f".to_owned());
@ -573,7 +577,10 @@ mod tests {
// resetting the env var below restores the default for subsequent
// tests in the same process.
unsafe { std::env::set_var(SANDBOX_EXEC_BIN_ENV, "/nonexistent/sandbox-exec") };
assert_eq!(sandbox_exec_bin(), PathBuf::from("/nonexistent/sandbox-exec"));
assert_eq!(
sandbox_exec_bin(),
PathBuf::from("/nonexistent/sandbox-exec")
);
assert!(!sandbox_exec_available());
unsafe { std::env::remove_var(SANDBOX_EXEC_BIN_ENV) };
}

View file

@ -71,7 +71,12 @@ pub fn compile(allowed_nrs: &[u32], audit_arch: u32) -> Vec<SockFilter> {
// (1) jeq audit_arch ? next : KILL
// KILL is at the very end; computed below after we know the size.
let arch_check_idx = program.len();
program.push(SockFilter { code: BPF_JMP | BPF_JEQ | BPF_K, jt: 0, jf: 0, k: audit_arch });
program.push(SockFilter {
code: BPF_JMP | BPF_JEQ | BPF_K,
jt: 0,
jf: 0,
k: audit_arch,
});
// (2) ld [nr]
program.push(SockFilter {
@ -90,7 +95,12 @@ pub fn compile(allowed_nrs: &[u32], audit_arch: u32) -> Vec<SockFilter> {
// plus the KILL ret) to land on the ALLOW ret. Computed below.
let first_check_idx = program.len();
for &nr in allowed_nrs {
program.push(SockFilter { code: BPF_JMP | BPF_JEQ | BPF_K, jt: 0, jf: 0, k: nr });
program.push(SockFilter {
code: BPF_JMP | BPF_JEQ | BPF_K,
jt: 0,
jf: 0,
k: nr,
});
}
// (KILL) ret KILL_PROCESS
@ -103,7 +113,12 @@ pub fn compile(allowed_nrs: &[u32], audit_arch: u32) -> Vec<SockFilter> {
});
// (ALLOW) ret ALLOW
let allow_idx = program.len();
program.push(SockFilter { code: BPF_RET | BPF_K, jt: 0, jf: 0, k: SECCOMP_RET_ALLOW });
program.push(SockFilter {
code: BPF_RET | BPF_K,
jt: 0,
jf: 0,
k: SECCOMP_RET_ALLOW,
});
// Patch arch check: jt=0 (next on match), jf=N (KILL on mismatch).
let arch_jf = (kill_idx - arch_check_idx - 1) as u8;

View file

@ -34,7 +34,7 @@ pub mod syscalls;
use std::collections::BTreeSet;
use crate::dynamic::sandbox::seccomp::bpf::{SockFilter, SockFprog};
use crate::dynamic::sandbox::seccomp::syscalls::{syscall_number, AUDIT_ARCH};
use crate::dynamic::sandbox::seccomp::syscalls::{AUDIT_ARCH, syscall_number};
include!(concat!(env!("OUT_DIR"), "/seccomp_policy.rs"));
@ -174,15 +174,15 @@ mod tests {
#[test]
fn base_table_is_non_empty() {
assert!(!BASE.is_empty(), "seccomp BASE allowlist must include stdio + startup syscalls");
assert!(
!BASE.is_empty(),
"seccomp BASE allowlist must include stdio + startup syscalls"
);
}
#[test]
fn cap_table_includes_known_caps() {
let known: Vec<&str> = CAP
.iter()
.map(|(_, _)| "_")
.collect();
let known: Vec<&str> = CAP.iter().map(|(_, _)| "_").collect();
// We declared SQL_QUERY, FILE_IO, SSRF, CODE_EXEC, HTML_ESCAPE,
// DESERIALIZE, HEADER_INJECTION, OPEN_REDIRECT in the toml; the
// build script emits one entry per `[cap.X]` table. The exact