mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-15 20:05:13 +02:00
cargo fmt
This commit is contained in:
parent
bec7bbf96c
commit
3a35cd6c8f
294 changed files with 6809 additions and 3911 deletions
|
|
@ -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}");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
))
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue