Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions crates/memory/tests/launch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -856,6 +856,79 @@ fn a_prompted_launch_runs_headless_and_insists_on_a_handoff() {
);
}

/// A confined prompted run captures the harness's standard output through a pipe, which
/// holds 64 KiB. A harness that writes more than that finishes, and all of what it wrote
/// reaches the launcher's own output. Bounded in time: a launch that never returns is
/// this test failing, and its process group is killed so nothing outlives the test.
#[cfg(target_os = "linux")]
#[test]
fn a_confined_prompted_run_captures_more_than_a_pipe_holds() {
use std::os::unix::process::CommandExt;
use std::time::{Duration, Instant};
if !namespaces_available() {
eprintln!("skipping: unprivileged namespaces are not available here");
return;
}
let dir = tempfile::tempdir().unwrap();
let root = init_root(dir.path());
let work = dir.path().join("work");
std::fs::create_dir_all(&work).unwrap();
let bins = fake_harness(
&work,
"printf 'begin\\n'; head -c 200000 /dev/zero | tr '\\0' a; printf '\\nend\\n'; exit 0",
);
let stdout = dir.path().join("stdout");
let stderr = dir.path().join("stderr");
let mut cmd = memory();
cmd.args([
"--root",
root.to_str().unwrap(),
"launch",
"--harness",
"claude",
"--auth",
"key",
])
.args(["--workdir", work.to_str().unwrap()])
.args(["--prompt", "hello", "--no-handoff"])
.env("PATH", path_with(&bins))
.env("ANTHROPIC_API_KEY", "sk-ant-placeholder")
.stdin(Stdio::null())
.stdout(std::fs::File::create(&stdout).unwrap())
.stderr(std::fs::File::create(&stderr).unwrap())
.process_group(0);
let mut child = cmd.spawn().unwrap();
let deadline = Instant::now() + Duration::from_secs(90);
let status = loop {
if let Some(s) = child.try_wait().unwrap() {
break s;
}
if Instant::now() > deadline {
Command::new("kill")
.args(["-KILL", "--", &format!("-{}", child.id())])
.status()
.ok();
child.wait().ok();
panic!(
"the launch had not returned after 90 s, with {} bytes out: {}",
std::fs::metadata(&stdout).map(|m| m.len()).unwrap_or(0),
std::fs::read_to_string(&stderr).unwrap_or_default()
);
}
std::thread::sleep(Duration::from_millis(100));
};
let err = std::fs::read_to_string(&stderr).unwrap();
assert_eq!(status.code(), Some(0), "{err}");
assert!(err.contains("confined to"), "{err}");
let out = std::fs::read_to_string(&stdout).unwrap();
let want = format!("begin\n{}\nend\n", "a".repeat(200_000));
assert!(
out.contains(&want),
"the harness's output, whole: {} bytes out",
out.len()
);
}

// ---- the wslc backend's inside half, driven by a fake host ----

/// `memory inside` writes run requests for the host and waits for exit codes. This fake
Expand Down
23 changes: 22 additions & 1 deletion crates/sandbox/src/linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -575,7 +575,8 @@ fn cloexec_from_three(limit: libc::c_int) -> std::io::Result<()> {
fn become_pid_one() -> Result<(), Error> {
unshare(CloneFlags::CLONE_NEWPID).map_err(|e| confine(format!("unshare pid: {e}")))?;
// SAFETY: fork between fork and exec in an already single-threaded child; the parent
// branch only waits and exits, the child branch only mounts and returns to exec.
// branch only closes descriptors, waits and exits, the child branch only mounts and
// returns to exec.
let pid = unsafe { libc::fork() };
if pid < 0 {
return Err(confine(format!(
Expand All @@ -584,6 +585,11 @@ fn become_pid_one() -> Result<(), Error> {
)));
}
if pid > 0 {
// The intermediate never execs, so its copies of the launcher's descriptors
// stay open until it exits: among them std's exec-error channel, whose end of
// file is what lets `spawn` return. The child, pid 1, keeps its own copies and
// reports through that channel as before.
close_from_three();
let mut status: libc::c_int = 0;
// SAFETY: waitpid on our own child with a valid status pointer.
let r = unsafe { libc::waitpid(pid, &mut status, 0) };
Expand Down Expand Up @@ -650,6 +656,21 @@ fn say_the_relay_died(signal: i32, core_dumped: bool) {
unsafe { libc::write(2, buf.as_ptr().cast(), n) };
}

/// Close every descriptor from 3 up. Runs between fork and exec, so it allocates nothing:
/// `close_range` (Linux 5.9) in one call, or else `close` on each number below the
/// open-files limit.
fn close_from_three() {
// SAFETY: close_range with integer arguments only.
let r = unsafe { libc::syscall(libc::SYS_close_range, 3, libc::c_uint::MAX, 0) };
if r == 0 {
return;
}
for fd in 3..open_files_limit() {
// SAFETY: close of a descriptor number; one that is not open fails with EBADF.
unsafe { libc::close(fd) };
}
}

/// What `memory relay` does inside: spawn the harness and wait for it. The relay is
/// already pid 1 of the harness's namespace, courtesy of [`run`].
///
Expand Down
Loading