#![cfg(unix)]
use std::time::Duration;
use text_mirror::runner::jail::platform_backend;
use text_mirror::runner::{Limits, Runner, locate_worker};
fn worker_path() -> std::path::PathBuf {
locate_worker().expect("the worker binary sits beside the test binary")
}
fn runner_with(limits: Limits) -> Runner {
let backend = platform_backend().expect("this platform has a jail backend");
Runner::new(backend, worker_path(), limits)
}
fn brisk_limits() -> Limits {
Limits {
wall_timeout: Duration::from_secs(4),
max_response_bytes: 4 * 1024 * 1024,
max_stderr_bytes: 64 * 1024,
..Limits::default()
}
}
fn runner() -> Runner {
runner_with(brisk_limits())
}
#[test]
fn the_backend_probe_proves_the_network_is_denied() {
let runner = runner();
runner
.backend()
.probe(&runner)
.expect("the network probe must pass on a supported platform");
}
#[test]
fn a_hung_adapter_is_killed_at_the_wall_clock_deadline() {
let runner = runner_with(Limits {
wall_timeout: Duration::from_millis(1500),
..brisk_limits()
});
let error = runner
.run("harness-sleep", serde_json::json!({}), &[])
.unwrap_err();
assert_eq!(error.code, "adapter_timeout", "{error}");
}
#[test]
fn an_oversized_response_frame_is_refused_before_allocation() {
let error = runner()
.run("harness-oversized", serde_json::json!({}), &[])
.unwrap_err();
assert_eq!(error.code, "adapter_frame_oversized", "{error}");
}
#[test]
fn bytes_after_the_response_frame_overflow_the_output_budget() {
let error = runner()
.run("harness-trailing", serde_json::json!({}), &[])
.unwrap_err();
assert_eq!(error.code, "adapter_output_overflow", "{error}");
}
#[test]
fn a_stderr_flood_is_capped_and_kills_the_child() {
let error = runner()
.run("harness-stderr-flood", serde_json::json!({}), &[])
.unwrap_err();
assert_eq!(error.code, "adapter_output_overflow", "{error}");
}
#[test]
fn a_truncated_response_frame_is_a_protocol_error() {
let error = runner()
.run("harness-truncated", serde_json::json!({}), &[])
.unwrap_err();
assert_eq!(error.code, "adapter_protocol_error", "{error}");
assert!(error.message.contains("truncated"), "{error}");
}
#[test]
fn a_crash_after_plausible_json_is_refused_on_the_nonzero_exit() {
let error = runner()
.run("harness-crash", serde_json::json!({}), &[])
.unwrap_err();
assert_eq!(error.code, "adapter_crash", "{error}");
}
#[test]
fn the_child_environment_is_scrubbed() {
let response = runner()
.run("harness-env", serde_json::json!({}), &[])
.expect("harness-env runs to completion");
let ok = response.ok.expect("harness-env succeeds");
let env = ok
.get("env")
.and_then(|v| v.as_array())
.expect("env array present");
const RUNTIME_INJECTED: &[&str] = &["__CF_USER_TEXT_ENCODING"];
for pair in env {
let key = pair
.as_array()
.and_then(|kv| kv.first())
.and_then(|k| k.as_str())
.unwrap_or("");
assert!(
RUNTIME_INJECTED.contains(&key),
"the child inherited environment variable {key:?}"
);
}
for leaked in [
"PATH",
"HOME",
"USER",
"TMPDIR",
"LD_PRELOAD",
"DYLD_INSERT_LIBRARIES",
] {
assert!(
!env.iter().any(|pair| pair
.as_array()
.and_then(|kv| kv.first())
.and_then(|k| k.as_str())
== Some(leaked)),
"the child inherited {leaked}"
);
}
}
#[test]
fn a_jail_escape_read_and_write_are_refused() {
let host_file = existing_host_file();
assert!(
std::path::Path::new(&host_file).exists(),
"the escape read target {host_file} must exist on the host"
);
let response = runner()
.run(
"harness-escape",
serde_json::json!({ "read_path": host_file }),
&[],
)
.expect("harness-escape runs to completion");
let ok = response.ok.expect("harness-escape succeeds");
let read = ok.get("read").and_then(|v| v.as_str()).unwrap_or("");
let write = ok.get("write").and_then(|v| v.as_str()).unwrap_or("");
assert!(
read.contains("denied"),
"a read of a host file outside the jail was not denied: {read:?}"
);
assert!(
write.contains("denied"),
"a write outside the jail was not denied: {write:?}"
);
}
fn existing_host_file() -> String {
for candidate in ["/etc/hosts", "/etc/services", "/etc/passwd"] {
if std::path::Path::new(candidate).exists() {
return candidate.to_string();
}
}
panic!("no known host file exists to prove the read confinement");
}
#[cfg(target_os = "linux")]
#[test]
fn a_stdio_closing_descendant_does_not_outlive_the_invocation() {
let worker = worker_path();
let response = runner()
.run(
"harness-survivor",
serde_json::json!({ "program": worker.to_str().unwrap() }),
&[],
)
.expect("harness-survivor runs to completion");
let ok = response.ok.expect("harness-survivor succeeds");
let pid = ok
.get("survivor_pid")
.and_then(|v| v.as_u64())
.expect("survivor pid reported") as i32;
let mut alive = true;
for _ in 0..50 {
if !pid_alive(pid) {
alive = false;
break;
}
std::thread::sleep(Duration::from_millis(20));
}
assert!(
!alive,
"a descendant that closed its stdio outlived the invocation (pid {pid})"
);
}
#[cfg(target_os = "linux")]
#[test]
fn the_process_count_limit_bounds_a_spawn_storm() {
let runner = runner_with(Limits {
max_processes: 8,
wall_timeout: Duration::from_secs(6),
..brisk_limits()
});
let worker = worker_path();
let response = runner
.run(
"harness-spawn-storm",
serde_json::json!({ "program": worker.to_str().unwrap(), "attempts": 200 }),
&[],
)
.expect("harness-spawn-storm runs to completion");
let ok = response.ok.expect("harness-spawn-storm succeeds");
let spawned = ok.get("spawned").and_then(|v| v.as_u64()).unwrap();
let failed = ok.get("failed").and_then(|v| v.as_u64()).unwrap();
assert!(
failed > 0,
"the process-count limit let all 200 spawns through, spawned={spawned} failed={failed}"
);
assert!(
spawned < 200,
"the process-count limit bounded nothing, spawned={spawned}"
);
}
#[cfg(target_os = "macos")]
#[test]
fn the_parent_never_hangs_on_a_stdout_holding_adapter() {
let runner = runner_with(Limits {
wall_timeout: Duration::from_millis(800),
..brisk_limits()
});
let started = std::time::Instant::now();
let error = runner
.run("harness-setsid-hold", serde_json::json!({}), &[])
.unwrap_err();
let elapsed = started.elapsed();
assert!(
matches!(error.code, "adapter_timeout" | "adapter_drain_timeout"),
"unexpected code {error}"
);
assert!(
elapsed < Duration::from_secs(8),
"the parent took {elapsed:?}, it may have hung on the escaped descendant"
);
}
#[cfg(target_os = "linux")]
fn pid_alive(pid: i32) -> bool {
use std::process::Command;
let output = Command::new("ps")
.args(["-o", "state=", "-p", &pid.to_string()])
.output();
match output {
Ok(out) if out.status.success() => {
let state = String::from_utf8_lossy(&out.stdout);
let state = state.trim();
!state.is_empty() && !state.starts_with('Z')
}
_ => false,
}
}
#[test]
fn a_valid_request_and_response_round_trip_through_the_jail() {
let response = runner()
.run("harness-echo", serde_json::json!({"token": "abc"}), &[])
.expect("harness-echo runs to completion");
let ok = response.ok.expect("harness-echo succeeds");
assert_eq!(ok.get("token").and_then(|v| v.as_str()), Some("abc"));
}
#[test]
fn a_staged_input_is_readable_only_by_bare_name() {
let error = runner()
.run(
"harness-echo",
serde_json::json!({}),
&[("../escape", b"x" as &[u8])],
)
.unwrap_err();
assert_eq!(error.code, "adapter_spawn_error", "{error}");
}
#[cfg(target_os = "macos")]
#[test]
fn a_high_inherited_descriptor_does_not_reach_the_adapter() {
use std::os::fd::AsRawFd;
let file = std::fs::File::open("/etc/hosts").expect("open a host file");
const HIGH_FD: i32 = 900;
let copied = unsafe { nix::libc::dup2(file.as_raw_fd(), HIGH_FD) };
assert_eq!(copied, HIGH_FD, "dup onto a high fd failed");
let response = runner()
.run(
"harness-fd-check",
serde_json::json!({ "fd": HIGH_FD }),
&[],
)
.expect("harness-fd-check runs to completion");
unsafe {
nix::libc::close(HIGH_FD);
}
let ok = response.ok.expect("harness-fd-check succeeds");
let open = ok.get("open").and_then(|v| v.as_bool()).unwrap_or(true);
assert!(
!open,
"a high inherited descriptor reached the adapter, the sweep missed it"
);
}