From 912524d6feec6d22744112d1b4ecbcc61eb8167c Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Fri, 7 Aug 2026 01:21:49 +0000 Subject: [PATCH] Mount a minimal `/dev` in full-filesystem Bubblewrap sandboxes (#37349) ## Why Binding the full host filesystem into a network-isolated Bubblewrap sandbox also inherits the host's device tree. ## What changed - Overlay `/dev` with Bubblewrap's minimal device filesystem while preserving the root bind's `nodev` behavior. - Bind `/dev/shm` back into the sandbox when available so host and sandbox processes can continue to share memory files. ## Testing Add a managed-proxy integration test that verifies standard device nodes remain usable, `/dev` is not the host directory, and `/dev/shm` writes are shared with the host. GitOrigin-RevId: 1d91fe44affd792d2777adae8f2cce6d85d14103 --- codex-rs/linux-sandbox/src/bwrap.rs | 12 +++ .../tests/suite/managed_proxy.rs | 78 +++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/codex-rs/linux-sandbox/src/bwrap.rs b/codex-rs/linux-sandbox/src/bwrap.rs index 8178a2740b..f0e188b255 100644 --- a/codex-rs/linux-sandbox/src/bwrap.rs +++ b/codex-rs/linux-sandbox/src/bwrap.rs @@ -271,6 +271,13 @@ fn create_bwrap_flags_full_filesystem(command: Vec, options: BwrapOption "--bind".to_string(), "/".to_string(), "/".to_string(), + // Preserve nodev on the root bind while exposing only standard devices. + "--dev".to_string(), + "/dev".to_string(), + // Restore shared memory without exposing other host devices. + "--bind-try".to_string(), + "/dev/shm".to_string(), + "/dev/shm".to_string(), // Always enter a fresh user namespace so root inside a container does // not need ambient CAP_SYS_ADMIN to create the remaining namespaces. "--unshare-user".to_string(), @@ -1401,6 +1408,11 @@ mod tests { "--bind".to_string(), "/".to_string(), "/".to_string(), + "--dev".to_string(), + "/dev".to_string(), + "--bind-try".to_string(), + "/dev/shm".to_string(), + "/dev/shm".to_string(), "--unshare-user".to_string(), "--unshare-pid".to_string(), "--unshare-net".to_string(), diff --git a/codex-rs/linux-sandbox/tests/suite/managed_proxy.rs b/codex-rs/linux-sandbox/tests/suite/managed_proxy.rs index 658f2e1a2e..b39b7cba48 100644 --- a/codex-rs/linux-sandbox/tests/suite/managed_proxy.rs +++ b/codex-rs/linux-sandbox/tests/suite/managed_proxy.rs @@ -17,9 +17,11 @@ use std::io::Read; use std::io::Write; use std::net::Ipv4Addr; use std::net::TcpListener; +use std::os::unix::fs::MetadataExt; use std::process::Output; use std::process::Stdio; use std::time::Duration; +use tempfile::NamedTempFile; use tokio::process::Command; const BWRAP_UNAVAILABLE_ERR: &str = "bubblewrap is unavailable: no system bwrap was found"; @@ -169,6 +171,82 @@ fn linux_sandbox_command( cmd } +#[tokio::test] +async fn managed_proxy_full_filesystem_uses_minimal_dev_nodes() { + if let Some(skip_reason) = managed_proxy_skip_reason().await { + eprintln!("skipping managed proxy test: {skip_reason}"); + return; + } + + let host_dev = std::fs::metadata("/dev").expect("host /dev should exist"); + let host_dev_id = format!("{}:{}", host_dev.dev(), host_dev.ino()); + let shared_memory_file = match NamedTempFile::new_in("/dev/shm") { + Ok(file) => { + std::fs::write(file.path(), "host-before").expect("seed /dev/shm file"); + Some(file) + } + Err(err) => { + eprintln!("skipping /dev/shm interoperability check: {err}"); + None + } + }; + + let mut env = create_env_from_core_vars(); + strip_proxy_env(&mut env); + env.insert("HTTP_PROXY".to_string(), "http://127.0.0.1:9".to_string()); + if let Some(file) = &shared_memory_file { + env.insert( + "CODEX_TEST_SHM_PATH".to_string(), + file.path().to_string_lossy().into_owned(), + ); + } + + let output = run_linux_sandbox_direct( + &[ + "bash", + "-c", + concat!( + "set -e; ", + "for node in null zero urandom; do test -c \"/dev/$node\"; done; ", + "printf test >/dev/null; ", + "head -c 1 /dev/zero >/dev/null; ", + "head -c 1 /dev/urandom >/dev/null; ", + "if command -v python3 >/dev/null 2>&1; then ", + "python3 -c 'import os; assert len(os.urandom(16)) == 16'; ", + "fi; ", + "if [ -n \"${CODEX_TEST_SHM_PATH:-}\" ]; then ", + "test \"$(cat \"$CODEX_TEST_SHM_PATH\")\" = host-before; ", + "printf sandbox-after >\"$CODEX_TEST_SHM_PATH\"; ", + "fi; ", + "stat -c '%d:%i' /dev", + ), + ], + &PermissionProfile::Disabled, + /*allow_network_for_proxy*/ true, + env, + NETWORK_TIMEOUT_MS, + ) + .await; + + assert_eq!( + output.status.success(), + true, + "standard devices should be usable; stderr={}", + String::from_utf8_lossy(&output.stderr) + ); + assert_ne!( + String::from_utf8_lossy(&output.stdout).trim(), + host_dev_id, + "/dev should be synthetic rather than inherited from the host" + ); + if let Some(file) = shared_memory_file { + assert_eq!( + std::fs::read_to_string(file.path()).expect("read /dev/shm file"), + "sandbox-after" + ); + } +} + #[tokio::test] async fn managed_proxy_bridges_release_command_output_after_exit() { if let Some(skip_reason) = managed_proxy_skip_reason().await {