diff --git a/codex-rs/cli/src/debug_sandbox.rs b/codex-rs/cli/src/debug_sandbox.rs index 7f0983cb..30e144ed 100644 --- a/codex-rs/cli/src/debug_sandbox.rs +++ b/codex-rs/cli/src/debug_sandbox.rs @@ -4,8 +4,8 @@ use codex_common::CliConfigOverrides; use codex_core::config::Config; use codex_core::config::ConfigOverrides; use codex_core::config_types::SandboxMode; -use codex_core::exec::spawn_command_under_linux_sandbox; use codex_core::exec_env::create_env; +use codex_core::landlock::spawn_command_under_linux_sandbox; use codex_core::seatbelt::spawn_command_under_seatbelt; use codex_core::spawn::StdioPolicy; diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index 50bba1b4..9430433c 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -3,7 +3,6 @@ use std::os::unix::process::ExitStatusExt; use std::collections::HashMap; use std::io; -use std::path::Path; use std::path::PathBuf; use std::process::ExitStatus; use std::time::Duration; @@ -18,6 +17,7 @@ use tokio::process::Child; use crate::error::CodexErr; use crate::error::Result; use crate::error::SandboxErr; +use crate::landlock::spawn_command_under_linux_sandbox; use crate::protocol::Event; use crate::protocol::EventMsg; use crate::protocol::ExecCommandOutputDeltaEvent; @@ -163,65 +163,6 @@ pub async fn process_exec_tool_call( } } -/// Spawn a shell tool command under the Linux Landlock+seccomp sandbox helper -/// (codex-linux-sandbox). -/// -/// Unlike macOS Seatbelt where we directly embed the policy text, the Linux -/// helper accepts a list of `--sandbox-permission`/`-s` flags mirroring the -/// public CLI. We convert the internal [`SandboxPolicy`] representation into -/// the equivalent CLI options. -pub async fn spawn_command_under_linux_sandbox

( - codex_linux_sandbox_exe: P, - command: Vec, - sandbox_policy: &SandboxPolicy, - cwd: PathBuf, - stdio_policy: StdioPolicy, - env: HashMap, -) -> std::io::Result -where - P: AsRef, -{ - let args = create_linux_sandbox_command_args(command, sandbox_policy, &cwd); - let arg0 = Some("codex-linux-sandbox"); - spawn_child_async( - codex_linux_sandbox_exe.as_ref().to_path_buf(), - args, - arg0, - cwd, - sandbox_policy, - stdio_policy, - env, - ) - .await -} - -/// Converts the sandbox policy into the CLI invocation for `codex-linux-sandbox`. -fn create_linux_sandbox_command_args( - command: Vec, - sandbox_policy: &SandboxPolicy, - cwd: &Path, -) -> Vec { - #[expect(clippy::expect_used)] - let sandbox_policy_cwd = cwd.to_str().expect("cwd must be valid UTF-8").to_string(); - - #[expect(clippy::expect_used)] - let sandbox_policy_json = - serde_json::to_string(sandbox_policy).expect("Failed to serialize SandboxPolicy to JSON"); - - let mut linux_cmd: Vec = vec![ - sandbox_policy_cwd, - sandbox_policy_json, - // Separator so that command arguments starting with `-` are not parsed as - // options of the helper itself. - "--".to_string(), - ]; - - // Append the original tool command. - linux_cmd.extend(command); - - linux_cmd -} - /// We don't have a fully deterministic way to tell if our command failed /// because of the sandbox - a command in the user's zshrc file might hit an /// error, but the command itself might fail or succeed for other reasons. diff --git a/codex-rs/core/src/landlock.rs b/codex-rs/core/src/landlock.rs new file mode 100644 index 00000000..8c8840c2 --- /dev/null +++ b/codex-rs/core/src/landlock.rs @@ -0,0 +1,66 @@ +use crate::protocol::SandboxPolicy; +use crate::spawn::StdioPolicy; +use crate::spawn::spawn_child_async; +use std::collections::HashMap; +use std::path::Path; +use std::path::PathBuf; +use tokio::process::Child; + +/// Spawn a shell tool command under the Linux Landlock+seccomp sandbox helper +/// (codex-linux-sandbox). +/// +/// Unlike macOS Seatbelt where we directly embed the policy text, the Linux +/// helper accepts a list of `--sandbox-permission`/`-s` flags mirroring the +/// public CLI. We convert the internal [`SandboxPolicy`] representation into +/// the equivalent CLI options. +pub async fn spawn_command_under_linux_sandbox

( + codex_linux_sandbox_exe: P, + command: Vec, + sandbox_policy: &SandboxPolicy, + cwd: PathBuf, + stdio_policy: StdioPolicy, + env: HashMap, +) -> std::io::Result +where + P: AsRef, +{ + let args = create_linux_sandbox_command_args(command, sandbox_policy, &cwd); + let arg0 = Some("codex-linux-sandbox"); + spawn_child_async( + codex_linux_sandbox_exe.as_ref().to_path_buf(), + args, + arg0, + cwd, + sandbox_policy, + stdio_policy, + env, + ) + .await +} + +/// Converts the sandbox policy into the CLI invocation for `codex-linux-sandbox`. +fn create_linux_sandbox_command_args( + command: Vec, + sandbox_policy: &SandboxPolicy, + cwd: &Path, +) -> Vec { + #[expect(clippy::expect_used)] + let sandbox_policy_cwd = cwd.to_str().expect("cwd must be valid UTF-8").to_string(); + + #[expect(clippy::expect_used)] + let sandbox_policy_json = + serde_json::to_string(sandbox_policy).expect("Failed to serialize SandboxPolicy to JSON"); + + let mut linux_cmd: Vec = vec![ + sandbox_policy_cwd, + sandbox_policy_json, + // Separator so that command arguments starting with `-` are not parsed as + // options of the helper itself. + "--".to_string(), + ]; + + // Append the original tool command. + linux_cmd.extend(command); + + linux_cmd +} diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index d19fbbdb..e895377b 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -24,6 +24,7 @@ pub mod exec_env; mod flags; pub mod git_info; mod is_safe_command; +pub mod landlock; mod mcp_connection_manager; mod mcp_tool_call; mod message_history; diff --git a/codex-rs/core/tests/sandbox.rs b/codex-rs/core/tests/sandbox.rs deleted file mode 100644 index 93ffa605..00000000 --- a/codex-rs/core/tests/sandbox.rs +++ /dev/null @@ -1,49 +0,0 @@ -// TODO(mbolin): Update this test to run on Linux, as well. -// (Should rename the test as part of that work.) -#[cfg(target_os = "macos")] -#[tokio::test] -async fn python_multiprocessing_lock_works_under_seatbelt() { - #![expect(clippy::expect_used)] - use codex_core::protocol::SandboxPolicy; - use codex_core::seatbelt::spawn_command_under_seatbelt; - use codex_core::spawn::StdioPolicy; - use std::collections::HashMap; - - let policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![], - network_access: false, - exclude_tmpdir_env_var: false, - exclude_slash_tmp: false, - }; - - let python_code = r#"import multiprocessing -from multiprocessing import Lock, Process - -def f(lock): - with lock: - print("Lock acquired in child process") - -if __name__ == '__main__': - lock = Lock() - p = Process(target=f, args=(lock,)) - p.start() - p.join() -"#; - - let mut child = spawn_command_under_seatbelt( - vec![ - "python3".to_string(), - "-c".to_string(), - python_code.to_string(), - ], - &policy, - std::env::current_dir().expect("should be able to get current dir"), - StdioPolicy::RedirectForShellTool, - HashMap::new(), - ) - .await - .expect("should be able to spawn python under seatbelt"); - - let status = child.wait().await.expect("should wait for child process"); - assert!(status.success(), "python exited with {status:?}"); -} diff --git a/codex-rs/exec/tests/sandbox.rs b/codex-rs/exec/tests/sandbox.rs new file mode 100644 index 00000000..8cc31bba --- /dev/null +++ b/codex-rs/exec/tests/sandbox.rs @@ -0,0 +1,92 @@ +#![cfg(unix)] +#![expect(clippy::expect_used)] + +use codex_core::protocol::SandboxPolicy; +use codex_core::spawn::StdioPolicy; +use std::collections::HashMap; +use std::path::PathBuf; +use tokio::process::Child; + +#[cfg(target_os = "macos")] +async fn spawn_command_under_sandbox( + command: Vec, + sandbox_policy: &SandboxPolicy, + cwd: PathBuf, + stdio_policy: StdioPolicy, + env: HashMap, +) -> std::io::Result { + use codex_core::seatbelt::spawn_command_under_seatbelt; + spawn_command_under_seatbelt(command, sandbox_policy, cwd, stdio_policy, env).await +} + +#[cfg(target_os = "linux")] +async fn spawn_command_under_sandbox( + command: Vec, + sandbox_policy: &SandboxPolicy, + cwd: PathBuf, + stdio_policy: StdioPolicy, + env: HashMap, +) -> std::io::Result { + use codex_core::landlock::spawn_command_under_linux_sandbox; + let codex_linux_sandbox_exe = assert_cmd::cargo::cargo_bin("codex-exec"); + spawn_command_under_linux_sandbox( + codex_linux_sandbox_exe, + command, + sandbox_policy, + cwd, + stdio_policy, + env, + ) + .await +} + +#[tokio::test] +async fn python_multiprocessing_lock_works_under_sandbox() { + #[cfg(target_os = "macos")] + let writable_roots = Vec::::new(); + + // From https://man7.org/linux/man-pages/man7/sem_overview.7.html + // + // > On Linux, named semaphores are created in a virtual filesystem, + // > normally mounted under /dev/shm. + #[cfg(target_os = "linux")] + let writable_roots = vec![PathBuf::from("/dev/shm")]; + + let policy = SandboxPolicy::WorkspaceWrite { + writable_roots, + network_access: false, + exclude_tmpdir_env_var: false, + exclude_slash_tmp: false, + }; + + let python_code = r#"import multiprocessing +from multiprocessing import Lock, Process + +def f(lock): + with lock: + print("Lock acquired in child process") + +if __name__ == '__main__': + lock = Lock() + p = Process(target=f, args=(lock,)) + p.start() + p.join() +"#; + + let mut child = spawn_command_under_sandbox( + vec![ + "python3".to_string(), + "-c".to_string(), + python_code.to_string(), + ], + &policy, + std::env::current_dir().expect("should be able to get current dir"), + StdioPolicy::Inherit, + HashMap::new(), + ) + .await + .expect("should be able to spawn python under sandbox"); + + let status = child.wait().await.expect("should wait for child process"); + assert!(status.success(), "python exited with {status:?}"); +}