From 1f0566d3f59298d1bb88820a0d35294f1eeb07ea Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Thu, 9 Jul 2026 18:28:55 -0700 Subject: [PATCH] exec-server: expose process helper to outer sandbox (#31937) ## Why Sandboxed exec-server process requests can use a restricted filesystem profile that does not expose the exec-server binary. On Linux, the outer bubblewrap stage re-enters that binary with the `codex-linux-sandbox` argv0 to install seccomp, so hiding the binary prevents the requested process from starting. ## What changed - add the configured `codex_self_exe` to the process permission profile before constructing the outer platform sandbox - add a Linux exec-server integration test that starts a real remote process with restricted reads and verifies it can read an allowed workspace file ## Test plan - `just test -p codex-exec-server process_sandbox` - `just test -p codex-exec-server --test exec_process remote_process_keeps_sandbox_helper_visible_with_restricted_reads` --- codex-rs/exec-server/src/process_sandbox.rs | 16 +++ codex-rs/exec-server/tests/exec_process.rs | 144 ++++++++++++++++++++ 2 files changed, 160 insertions(+) diff --git a/codex-rs/exec-server/src/process_sandbox.rs b/codex-rs/exec-server/src/process_sandbox.rs index b5dd54e0b2..050e151e6e 100644 --- a/codex-rs/exec-server/src/process_sandbox.rs +++ b/codex-rs/exec-server/src/process_sandbox.rs @@ -77,6 +77,22 @@ pub(crate) fn prepare_exec_request( native_sandbox_policy_cwd.as_path(), ); let (file_system_policy, network_policy) = permissions.to_runtime_permissions(); + // Bubblewrap launches the configured helper, which may re-enter this executable to apply + // seccomp, so the outer filesystem sandbox must expose both paths. + #[cfg(target_os = "linux")] + let sandbox_helper_paths = std::iter::once(&runtime_paths.codex_self_exe) + .chain(runtime_paths.codex_linux_sandbox_exe.as_ref()) + .cloned() + .collect::>(); + #[cfg(target_os = "linux")] + let file_system_policy = file_system_policy + .with_additional_readable_roots(native_sandbox_policy_cwd.as_path(), &sandbox_helper_paths); + #[cfg(target_os = "linux")] + let permissions = PermissionProfile::from_runtime_permissions_with_enforcement( + permissions.enforcement(), + &file_system_policy, + network_policy, + ); let sandbox_manager = SandboxManager::new(); let sandbox = sandbox_manager.select_initial( &file_system_policy, diff --git a/codex-rs/exec-server/tests/exec_process.rs b/codex-rs/exec-server/tests/exec_process.rs index 73377da60b..5e51882772 100644 --- a/codex-rs/exec-server/tests/exec_process.rs +++ b/codex-rs/exec-server/tests/exec_process.rs @@ -1,6 +1,8 @@ mod common; use std::collections::HashMap; +#[cfg(target_os = "linux")] +use std::os::unix::fs::PermissionsExt; use std::sync::Arc; use anyhow::Context; @@ -11,11 +13,27 @@ use codex_exec_server::ExecOutputStream; use codex_exec_server::ExecParams; use codex_exec_server::ExecProcess; use codex_exec_server::ExecProcessEvent; +#[cfg(target_os = "linux")] +use codex_exec_server::FileSystemSandboxContext; use codex_exec_server::ProcessId; use codex_exec_server::ProcessSignal; use codex_exec_server::ReadResponse; use codex_exec_server::StartedExecProcess; use codex_exec_server::WriteStatus; +#[cfg(target_os = "linux")] +use codex_protocol::models::PermissionProfile; +#[cfg(target_os = "linux")] +use codex_protocol::permissions::FileSystemAccessMode; +#[cfg(target_os = "linux")] +use codex_protocol::permissions::FileSystemPath; +#[cfg(target_os = "linux")] +use codex_protocol::permissions::FileSystemSandboxEntry; +#[cfg(target_os = "linux")] +use codex_protocol::permissions::FileSystemSandboxPolicy; +#[cfg(target_os = "linux")] +use codex_protocol::permissions::FileSystemSpecialPath; +#[cfg(target_os = "linux")] +use codex_protocol::permissions::NetworkSandboxPolicy; use codex_utils_path_uri::PathUri; use pretty_assertions::assert_eq; use tempfile::TempDir; @@ -96,6 +114,132 @@ async fn assert_exec_process_starts_and_exits(use_remote: bool) -> Result<()> { Ok(()) } +#[cfg(target_os = "linux")] +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn remote_process_keeps_sandbox_helper_visible_with_restricted_reads() -> Result<()> { + if let Some(warning) = codex_sandboxing::system_bwrap_warning(&PermissionProfile::read_only()) { + eprintln!("skipping bwrap test: {warning}"); + return Ok(()); + } + + let context = create_process_context(/*use_remote*/ true).await?; + let workspace = TempDir::new()?; + let file = workspace.path().join("allowed.txt"); + std::fs::write(&file, b"allowed")?; + let cwd = PathUri::from_host_native_path(workspace.path())?; + let policy = FileSystemSandboxPolicy::restricted(vec![ + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Minimal, + }, + access: FileSystemAccessMode::Read, + }, + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::project_roots(/*subpath*/ None), + }, + access: FileSystemAccessMode::Read, + }, + ]); + let sandbox = FileSystemSandboxContext::from_permission_profile_with_cwd( + PermissionProfile::from_runtime_permissions(&policy, NetworkSandboxPolicy::Restricted), + cwd.clone(), + ); + + let session = context + .backend + .start(ExecParams { + process_id: ProcessId::from("proc-restricted-helper"), + argv: vec!["/bin/cat".to_string(), file.to_string_lossy().into_owned()], + cwd, + env_policy: None, + env: HashMap::from([("PATH".to_string(), std::env::var("PATH")?)]), + tty: false, + pipe_stdin: false, + arg0: None, + sandbox: Some(sandbox), + enforce_managed_network: false, + managed_network: None, + }) + .await?; + let output = collect_process_output_from_events(session.process).await?; + + assert_eq!( + output, + ("allowed".to_string(), String::new(), Some(0), true) + ); + Ok(()) +} + +#[cfg(target_os = "linux")] +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn remote_tty_process_uses_configured_sandbox_helper_with_hostile_path() -> Result<()> { + if let Some(warning) = codex_sandboxing::system_bwrap_warning(&PermissionProfile::read_only()) { + eprintln!("skipping bwrap test: {warning}"); + return Ok(()); + } + + let context = create_process_context(/*use_remote*/ true).await?; + let workspace = TempDir::new()?; + let file = workspace.path().join("allowed.txt"); + std::fs::write(&file, b"allowed")?; + let hostile_helper = workspace.path().join("codex-linux-sandbox"); + std::fs::write(&hostile_helper, b"#!/bin/sh\nprintf hostile")?; + let mut permissions = std::fs::metadata(&hostile_helper)?.permissions(); + permissions.set_mode(0o755); + std::fs::set_permissions(&hostile_helper, permissions)?; + let path = std::env::var_os("PATH").context("PATH is not set")?; + let hostile_path = std::env::join_paths( + std::iter::once(workspace.path().to_path_buf()).chain(std::env::split_paths(&path)), + )?; + let cwd = PathUri::from_host_native_path(workspace.path())?; + let policy = FileSystemSandboxPolicy::restricted(vec![ + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Minimal, + }, + access: FileSystemAccessMode::Read, + }, + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::project_roots(/*subpath*/ None), + }, + access: FileSystemAccessMode::Read, + }, + ]); + let sandbox = FileSystemSandboxContext::from_permission_profile_with_cwd( + PermissionProfile::from_runtime_permissions(&policy, NetworkSandboxPolicy::Restricted), + cwd.clone(), + ); + + let session = context + .backend + .start(ExecParams { + process_id: ProcessId::from("proc-hostile-helper-path"), + argv: vec!["/bin/cat".to_string(), file.to_string_lossy().into_owned()], + cwd, + env_policy: None, + env: HashMap::from([( + "PATH".to_string(), + hostile_path.to_string_lossy().into_owned(), + )]), + tty: true, + pipe_stdin: false, + arg0: None, + sandbox: Some(sandbox), + enforce_managed_network: false, + managed_network: None, + }) + .await?; + let output = collect_process_output_from_events(session.process).await?; + + assert_eq!( + output, + ("allowed".to_string(), String::new(), Some(0), true) + ); + Ok(()) +} + async fn read_process_until_change( session: Arc, wake_rx: &mut watch::Receiver,