Skip to content
Open
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
473 changes: 473 additions & 0 deletions crates/runner/src/descriptors.rs

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions crates/runner/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ fn reject_symlink_ancestors(root: &Path, dest: &Path) -> Result<(), ErrorBody> {

mod api;
mod apply;
mod descriptors;
mod files;
mod linux_sandbox;
mod patch_helper;
Expand All @@ -119,6 +120,7 @@ pub use api::{
RunnerResizeResult, RunnerWriteStdin, DEFAULT_TIMEOUT_MS, MAX_OUTPUT_BYTES,
};
pub use codespace_domain::{DEFAULT_FIND_LIMIT, DEFAULT_READ_LIMIT};
pub use descriptors::exclude_unrelated;
pub use files::VERSION_ABSENT;
pub use patch_helper::ensure_helper_for_tests;
pub use process::{
Expand Down
8 changes: 6 additions & 2 deletions crates/runner/src/linux_sandbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ fn probe_helper(helper: &Path) -> bool {
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
crate::descriptors::exclude_unrelated_std(&mut child);
let Ok(child) = child.spawn() else {
return false;
};
Expand Down Expand Up @@ -138,11 +139,14 @@ pub(crate) fn prepare_run_from_helper(
argv: argv.to_vec(),
};
let payload = serde_json::to_vec(&request).map_err(|err| spawn_failed(err.to_string()))?;
let spawned = Command::new(helper)
let mut command = Command::new(helper);
command
.arg("prepare")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.stderr(Stdio::piped());
crate::descriptors::exclude_unrelated_std(&mut command);
let spawned = command
.spawn()
.map_err(|err| spawn_failed(format!("failed to spawn linux sandbox prepare: {err}")))?;
prepare_from_child(helper, spawned, &payload, PREPARE_TIMEOUT, PREPARE_IO_LIMIT)
Expand Down
19 changes: 10 additions & 9 deletions crates/runner/src/patch_helper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,19 @@ async fn invoke(
check_only: bool,
) -> Result<HelperSuccess, ErrorBody> {
let bin = helper_bin()?;
let mut child = Command::new(&bin)
let mut command = Command::new(&bin);
command
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.kill_on_drop(true)
.spawn()
.map_err(|err| {
ErrorBody::new(
ErrorCode::InvalidPatch,
format!("failed to spawn patch helper: {err}"),
)
})?;
.kill_on_drop(true);
crate::descriptors::exclude_unrelated(&mut command);
let mut child = command.spawn().map_err(|err| {
ErrorBody::new(
ErrorCode::InvalidPatch,
format!("failed to spawn patch helper: {err}"),
)
})?;
let payload = serde_json::json!({
"op": op,
"root": root,
Expand Down
28 changes: 27 additions & 1 deletion crates/runner/src/process.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Managed workspace processes. Request lifetime is not process lifetime.
//! Pipe spawn uses `tokio::process::Command`. `tty: true` uses the isolated
//! Pipe spawn uses `tokio::process::Command`, whose children keep only their
//! standard descriptors (`descriptors`). `tty: true` uses the isolated
//! `codespace-pty` adapter. When the Linux helper probe succeeds, both wrap
//! the same helper argv. UDS dispatch lives in `UdsRunner`.

Expand Down Expand Up @@ -289,6 +290,7 @@ impl InProcessRunner {
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.kill_on_drop(true);
crate::descriptors::exclude_unrelated(&mut child);
for (key, value) in spawn_env(&cwd, &req, launch.sandboxed) {
child.env(key, value);
}
Expand Down Expand Up @@ -1485,6 +1487,30 @@ mod tests {
assert!(eof, "SIGTERM on the helper must reap the sandbox tree");
}

#[tokio::test]
async fn pipe_child_keeps_only_its_standard_descriptors() {
// A descriptor that another thread's creation window could leave inheritable (#79).
let held = crate::descriptors::tests::inheritable_descriptor();
let dir = tempdir().unwrap();
let ws = workspace(dir.path());
let runner = InProcessRunner::new(Arc::new(|_| {}));
let process_id = ProcessId("proc-descriptors".into());
let script =
crate::descriptors::tests::report_script(std::os::fd::AsRawFd::as_raw_fd(&held));
runner
.exec(
&ws,
RunnerExecRequest::for_host(
vec!["/bin/sh".into(), "-c".into(), script],
process_id.clone(),
Profile::WorkspaceWrite,
),
)
.await
.unwrap();
assert_eq!(wait_chunk(&runner, &process_id).await, "held\nclear\n");
}

#[tokio::test]
async fn echo_status_is_exited_zero() {
let dir = tempdir().unwrap();
Expand Down
52 changes: 48 additions & 4 deletions crates/server/src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,7 @@ impl RuntimeProcess {
let dir = allocate_private_runner_dir(runner_dir)
.map_err(|err| anyhow!("private runner dir: {err}"))?;
let socket = runner_socket_path(&dir);
let mut child = Command::new(bin)
.arg(&socket)
.stdin(Stdio::null())
.kill_on_drop(true)
let mut child = worker_command(bin, &socket)
.spawn()
.with_context(|| format!("spawn {}", bin.display()))?;
let shutdown = Arc::new(Notify::new());
Expand Down Expand Up @@ -119,6 +116,15 @@ impl Drop for RuntimeProcess {
}
}

/// The worker gets the socket path, a null stdin, the Gateway's stdout and stderr, and no other
/// descriptor of the Gateway's.
fn worker_command(bin: &Path, socket: &Path) -> Command {
let mut command = Command::new(bin);
command.arg(socket).stdin(Stdio::null()).kill_on_drop(true);
codespace_runner::exclude_unrelated(&mut command);
command
}

async fn wait_for_socket(socket: &Path, wait: &JoinHandle<()>) -> Result<UnixStream> {
for _ in 0..100 {
if wait.is_finished() {
Expand All @@ -142,6 +148,44 @@ mod tests {
std::env::var_os("CODESPACE_RUNTIME_BIN").map(PathBuf::from)
}

#[tokio::test]
async fn worker_keeps_only_its_standard_descriptors() {
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
// The server crate has no libc dependency; F_DUPFD is 0 on macOS and Linux.
extern "C" {
fn fcntl(fd: std::os::raw::c_int, cmd: std::os::raw::c_int, ...)
-> std::os::raw::c_int;
}
// A descriptor that another thread's creation window could leave inheritable (#79).
let null = std::fs::File::open("/dev/null").unwrap();
// SAFETY: F_DUPFD returns a new descriptor without close-on-exec, owned below.
let held = unsafe { fcntl(null.as_raw_fd(), 0, 400) };
assert!(held >= 400, "F_DUPFD");
// SAFETY: as above.
let held = unsafe { OwnedFd::from_raw_fd(held) };
let dir = tempfile::tempdir().unwrap();
let report = dir.path().join("report");
// `/bin/sh` reads the script given where the socket path goes; executing a freshly
// written file could meet ETXTBSY from a concurrent fork.
let script = dir.path().join("worker.sh");
std::fs::write(
&script,
format!(
"for n in 1 {}; do if [ -e /dev/fd/$n ]; then echo held; else echo clear; fi; done > '{}'\n",
held.as_raw_fd(),
report.display()
),
)
.unwrap();
let status = worker_command(Path::new("/bin/sh"), &script)
.status()
.await
.unwrap();
assert!(status.success());
let seen = std::fs::read_to_string(&report).unwrap();
assert_eq!(seen, "held\nclear\n");
}

#[tokio::test]
async fn spawn_hello_then_drop_kills_worker() {
let Some(bin) = runtime_bin() else {
Expand Down
2 changes: 1 addition & 1 deletion scripts/ci-policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
},
"rust-macos/single": {
"stages": ["macos-core", "dependencies"], "cache": "crates/file-system",
"compiles": ["file-system", "pty"]
"compiles": ["domain", "file-system", "linux-sandbox-protocol", "policy", "pty", "runner", "server", "store"]
}
}
}
11 changes: 6 additions & 5 deletions scripts/tests/test_ci_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,15 @@ def test_pty_selects_every_leg_that_compiles_it(self):
def test_components_follow_the_crate_graph(self):
rows = {
'crates/domain/src/lib.rs': ordered('rust-clippy/root', 'rust-clippy/adapters', 'rust-clippy/codex-adapters',
'rust-unit/patch', 'rust-unit/codex-runtime', 'rust-integration/single'),
'rust-unit/patch', 'rust-unit/codex-runtime', 'rust-integration/single',
'rust-macos/single'),
'crates/runner/src/lib.rs': ordered('rust-clippy/root', 'rust-clippy/codex-adapters',
'rust-unit/codex-runtime', 'rust-integration/single'),
'crates/server/src/main.rs': ['rust-clippy/root', 'rust-integration/single'],
'crates/store/src/lib.rs': ['rust-clippy/root', 'rust-integration/single'],
'rust-unit/codex-runtime', 'rust-integration/single', 'rust-macos/single'),
'crates/server/src/main.rs': ['rust-clippy/root', 'rust-integration/single', 'rust-macos/single'],
'crates/store/src/lib.rs': ['rust-clippy/root', 'rust-integration/single', 'rust-macos/single'],
'crates/linux-sandbox-protocol/src/lib.rs': ordered(
'rust-clippy/root', 'rust-clippy/codex-adapters', 'rust-unit/codex-runtime', 'rust-unit/linux-sandbox-protocol',
'rust-unit/linux-sandbox', 'rust-linux-isolation/single', 'rust-integration/single'),
'rust-unit/linux-sandbox', 'rust-linux-isolation/single', 'rust-integration/single', 'rust-macos/single'),
'crates/patch/src/lib.rs': ['rust-clippy/adapters', 'rust-unit/patch', 'rust-integration/single'],
'crates/codex-runtime/src/lib.rs': ['rust-clippy/codex-adapters', 'rust-unit/codex-runtime',
'rust-integration/single'],
Expand Down
5 changes: 4 additions & 1 deletion scripts/validate-upstream.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@ def stages():
'clippy-root': [cargo('clippy', 'root', '--all-targets', '--', '-D', 'warnings')],
'clippy-adapters': [cargo('clippy', a, '--all-targets', '--', '-D', 'warnings') for a in ('patch', 'pty', 'file-system')],
'clippy-codex': [cargo('clippy', a, '--all-targets', '--', '-D', 'warnings') for a in ('codex-runtime', 'linux-sandbox')],
'macos-core': [cargo(action, a, *(['--all-targets', '--', '-D', 'warnings'] if action == 'clippy' else [])) for a in ('pty', 'file-system') for action in ('clippy', 'test')],
'macos-core': [cargo(action, a, *(['--all-targets', '--', '-D', 'warnings'] if action == 'clippy' else [])) for a in ('pty', 'file-system') for action in ('clippy', 'test')]
# Descriptor hygiene of the runner host's spawns, whose macOS path the Linux legs cannot exercise.
+ [cargo('test', 'root', '-p', 'codespace-runner', '--lib', '--', 'descriptors', 'missing_executable_is_process_spawn_failed'),
cargo('test', 'root', '-p', 'codespace-server', '--lib', '--', 'descriptors')],
'integration': [],
'linux-isolation': [cargo('build', 'linux-sandbox', '--bin', 'codespace-linux-sandbox'), cargo('test', 'linux-sandbox', '--test', 'isolation')],
'dependencies': [],
Expand Down
Loading