diff --git a/codex-rs/core/src/landlock.rs b/codex-rs/core/src/landlock.rs deleted file mode 100644 index c1dc4ba542..0000000000 --- a/codex-rs/core/src/landlock.rs +++ /dev/null @@ -1,63 +0,0 @@ -use crate::protocol::SandboxPolicy; -use crate::spawn::SpawnChildRequest; -use crate::spawn::StdioPolicy; -use crate::spawn::spawn_child_async; -use codex_network_proxy::NetworkProxy; -use codex_protocol::permissions::FileSystemSandboxPolicy; -use codex_protocol::permissions::NetworkSandboxPolicy; -use codex_sandboxing::landlock::allow_network_for_proxy; -use codex_sandboxing::landlock::create_linux_sandbox_command_args_for_policies; -use std::collections::HashMap; -use std::path::Path; -use std::path::PathBuf; -use tokio::process::Child; - -/// Spawn a shell tool command under the Linux sandbox helper -/// (codex-linux-sandbox), which defaults to bubblewrap for filesystem -/// isolation plus seccomp for network restrictions. -/// -/// Unlike macOS Seatbelt where we directly embed the policy text, the Linux -/// helper is a separate executable. We pass the legacy [`SandboxPolicy`] plus -/// split filesystem/network policies as JSON so the helper can migrate -/// incrementally without breaking older call sites. -#[allow(clippy::too_many_arguments)] -pub async fn spawn_command_under_linux_sandbox

( - codex_linux_sandbox_exe: P, - command: Vec, - command_cwd: PathBuf, - sandbox_policy: &SandboxPolicy, - sandbox_policy_cwd: &Path, - use_legacy_landlock: bool, - stdio_policy: StdioPolicy, - network: Option<&NetworkProxy>, - env: HashMap, -) -> std::io::Result -where - P: AsRef, -{ - let file_system_sandbox_policy = - FileSystemSandboxPolicy::from_legacy_sandbox_policy(sandbox_policy, sandbox_policy_cwd); - let network_sandbox_policy = NetworkSandboxPolicy::from(sandbox_policy); - let args = create_linux_sandbox_command_args_for_policies( - command, - command_cwd.as_path(), - sandbox_policy, - &file_system_sandbox_policy, - network_sandbox_policy, - sandbox_policy_cwd, - use_legacy_landlock, - allow_network_for_proxy(/*enforce_managed_network*/ false), - ); - let arg0 = Some("codex-linux-sandbox"); - spawn_child_async(SpawnChildRequest { - program: codex_linux_sandbox_exe.as_ref().to_path_buf(), - args, - arg0, - cwd: command_cwd, - network_sandbox_policy, - network, - stdio_policy, - env, - }) - .await -} diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index cd6b51b7e9..cdfe91ac5c 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -45,7 +45,6 @@ pub mod git_info; mod guardian; mod hook_runtime; pub mod instructions; -pub mod landlock; pub mod mcp; mod mcp_connection_manager; mod mcp_tool_approval_templates; @@ -121,7 +120,6 @@ pub mod default_client { pub mod project_doc; mod rollout; pub(crate) mod safety; -pub mod seatbelt; pub mod shell; pub mod shell_snapshot; pub mod skills; diff --git a/codex-rs/core/src/seatbelt.rs b/codex-rs/core/src/seatbelt.rs deleted file mode 100644 index f81016a990..0000000000 --- a/codex-rs/core/src/seatbelt.rs +++ /dev/null @@ -1,49 +0,0 @@ -#![cfg(target_os = "macos")] - -use crate::protocol::SandboxPolicy; -use crate::spawn::CODEX_SANDBOX_ENV_VAR; -use crate::spawn::SpawnChildRequest; -use crate::spawn::StdioPolicy; -use crate::spawn::spawn_child_async; -use codex_network_proxy::NetworkProxy; -use codex_protocol::permissions::FileSystemSandboxPolicy; -use codex_protocol::permissions::NetworkSandboxPolicy; -use codex_sandboxing::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE; -use codex_sandboxing::seatbelt::create_seatbelt_command_args_for_policies_with_extensions; -use std::collections::HashMap; -use std::path::Path; -use std::path::PathBuf; -use tokio::process::Child; - -pub async fn spawn_command_under_seatbelt( - command: Vec, - command_cwd: PathBuf, - sandbox_policy: &SandboxPolicy, - sandbox_policy_cwd: &Path, - stdio_policy: StdioPolicy, - network: Option<&NetworkProxy>, - mut env: HashMap, -) -> std::io::Result { - let args = create_seatbelt_command_args_for_policies_with_extensions( - command, - &FileSystemSandboxPolicy::from_legacy_sandbox_policy(sandbox_policy, sandbox_policy_cwd), - NetworkSandboxPolicy::from(sandbox_policy), - sandbox_policy_cwd, - /*enforce_managed_network*/ false, - network, - /*extensions*/ None, - ); - let arg0 = None; - env.insert(CODEX_SANDBOX_ENV_VAR.to_string(), "seatbelt".to_string()); - spawn_child_async(SpawnChildRequest { - program: PathBuf::from(MACOS_PATH_TO_SEATBELT_EXECUTABLE), - args, - arg0, - cwd: command_cwd, - network_sandbox_policy: NetworkSandboxPolicy::from(sandbox_policy), - network, - stdio_policy, - env, - }) - .await -} diff --git a/codex-rs/core/tests/suite/mod.rs b/codex-rs/core/tests/suite/mod.rs index 5f7e50f061..eb29da6dc8 100644 --- a/codex-rs/core/tests/suite/mod.rs +++ b/codex-rs/core/tests/suite/mod.rs @@ -118,7 +118,6 @@ mod rmcp_client; mod rollout_list_find; mod safety_check_downgrade; mod search_tool; -mod seatbelt; mod shell_command; mod shell_serialization; mod shell_snapshot; diff --git a/codex-rs/core/tests/suite/seatbelt.rs b/codex-rs/core/tests/suite/seatbelt.rs deleted file mode 100644 index cfb91caeed..0000000000 --- a/codex-rs/core/tests/suite/seatbelt.rs +++ /dev/null @@ -1,315 +0,0 @@ -#![cfg(target_os = "macos")] - -//! Tests for the macOS sandboxing that are specific to Seatbelt. -//! Tests that apply to both Mac and Linux sandboxing should go in sandbox.rs. - -use std::collections::HashMap; -use std::path::Path; -use std::path::PathBuf; - -use codex_core::seatbelt::spawn_command_under_seatbelt; -use codex_core::spawn::CODEX_SANDBOX_ENV_VAR; -use codex_core::spawn::StdioPolicy; -use codex_protocol::protocol::SandboxPolicy; -use tempfile::TempDir; - -struct TestScenario { - repo_parent: PathBuf, - file_outside_repo: PathBuf, - repo_root: PathBuf, - file_in_repo_root: PathBuf, - file_in_dot_git_dir: PathBuf, -} - -struct TestExpectations { - file_outside_repo_is_writable: bool, - file_in_repo_root_is_writable: bool, - file_in_dot_git_dir_is_writable: bool, -} - -impl TestScenario { - async fn run_test(&self, policy: &SandboxPolicy, expectations: TestExpectations) { - if std::env::var(CODEX_SANDBOX_ENV_VAR) == Ok("seatbelt".to_string()) { - eprintln!("{CODEX_SANDBOX_ENV_VAR} is set to 'seatbelt', skipping test."); - return; - } - - assert_eq!( - touch(&self.file_outside_repo, policy).await, - expectations.file_outside_repo_is_writable - ); - assert_eq!( - self.file_outside_repo.exists(), - expectations.file_outside_repo_is_writable - ); - - assert_eq!( - touch(&self.file_in_repo_root, policy).await, - expectations.file_in_repo_root_is_writable - ); - assert_eq!( - self.file_in_repo_root.exists(), - expectations.file_in_repo_root_is_writable - ); - - assert_eq!( - touch(&self.file_in_dot_git_dir, policy).await, - expectations.file_in_dot_git_dir_is_writable - ); - assert_eq!( - self.file_in_dot_git_dir.exists(), - expectations.file_in_dot_git_dir_is_writable - ); - } -} - -/// If the user has added a workspace root that is not a Git repo root, then -/// the user has to specify `--skip-git-repo-check` or go through some -/// interstitial that indicates they are taking on some risk because Git -/// cannot be used to backup their work before the agent begins. -/// -/// Because the user has agreed to this risk, we do not try find all .git -/// folders in the workspace and block them (though we could change our -/// position on this in the future). -#[tokio::test] -async fn if_parent_of_repo_is_writable_then_dot_git_folder_is_writable() { - let tmp = TempDir::new().expect("should be able to create temp dir"); - let test_scenario = create_test_scenario(&tmp); - let policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![test_scenario.repo_parent.as_path().try_into().unwrap()], - read_only_access: Default::default(), - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: true, - }; - - test_scenario - .run_test( - &policy, - TestExpectations { - file_outside_repo_is_writable: true, - file_in_repo_root_is_writable: true, - file_in_dot_git_dir_is_writable: true, - }, - ) - .await; -} - -/// When the writable root is the root of a Git repository (as evidenced by the -/// presence of a .git folder), then the .git folder should be read-only if -/// the policy is `WorkspaceWrite`. -#[tokio::test] -async fn if_git_repo_is_writable_root_then_dot_git_folder_is_read_only() { - let tmp = TempDir::new().expect("should be able to create temp dir"); - let test_scenario = create_test_scenario(&tmp); - let policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![test_scenario.repo_root.as_path().try_into().unwrap()], - read_only_access: Default::default(), - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: true, - }; - - test_scenario - .run_test( - &policy, - TestExpectations { - file_outside_repo_is_writable: false, - file_in_repo_root_is_writable: true, - file_in_dot_git_dir_is_writable: false, - }, - ) - .await; -} - -/// Under DangerFullAccess, all writes should be permitted anywhere on disk, -/// including inside the .git folder. -#[tokio::test] -async fn danger_full_access_allows_all_writes() { - let tmp = TempDir::new().expect("should be able to create temp dir"); - let test_scenario = create_test_scenario(&tmp); - let policy = SandboxPolicy::DangerFullAccess; - - test_scenario - .run_test( - &policy, - TestExpectations { - file_outside_repo_is_writable: true, - file_in_repo_root_is_writable: true, - file_in_dot_git_dir_is_writable: true, - }, - ) - .await; -} - -/// Under ReadOnly, writes should not be permitted anywhere on disk. -#[tokio::test] -async fn read_only_forbids_all_writes() { - let tmp = TempDir::new().expect("should be able to create temp dir"); - let test_scenario = create_test_scenario(&tmp); - let policy = SandboxPolicy::new_read_only_policy(); - - test_scenario - .run_test( - &policy, - TestExpectations { - file_outside_repo_is_writable: false, - file_in_repo_root_is_writable: false, - file_in_dot_git_dir_is_writable: false, - }, - ) - .await; -} - -#[tokio::test] -async fn openpty_works_under_seatbelt() { - if std::env::var(CODEX_SANDBOX_ENV_VAR) == Ok("seatbelt".to_string()) { - eprintln!("{CODEX_SANDBOX_ENV_VAR} is set to 'seatbelt', skipping test."); - return; - } - - if which::which("python3").is_err() { - eprintln!("python3 not found in PATH, skipping test."); - return; - } - - let policy = SandboxPolicy::new_read_only_policy(); - let command_cwd = std::env::current_dir().expect("getcwd"); - let sandbox_cwd = command_cwd.clone(); - - let mut child = spawn_command_under_seatbelt( - vec![ - "python3".to_string(), - "-c".to_string(), - r#"import os - -master, slave = os.openpty() -os.write(slave, b"ping") -assert os.read(master, 4) == b"ping""# - .to_string(), - ], - command_cwd, - &policy, - sandbox_cwd.as_path(), - StdioPolicy::RedirectForShellTool, - None, - HashMap::new(), - ) - .await - .expect("should be able to spawn python under seatbelt"); - - let status = child - .wait() - .await - .expect("should be able to wait for child process"); - assert!(status.success(), "python exited with {status:?}"); -} - -#[tokio::test] -async fn java_home_finds_runtime_under_seatbelt() { - if std::env::var(CODEX_SANDBOX_ENV_VAR) == Ok("seatbelt".to_string()) { - eprintln!("{CODEX_SANDBOX_ENV_VAR} is set to 'seatbelt', skipping test."); - return; - } - - let java_home_path = Path::new("/usr/libexec/java_home"); - if !java_home_path.exists() { - eprintln!("/usr/libexec/java_home is not present, skipping test."); - return; - } - - let baseline_output = tokio::process::Command::new(java_home_path) - .env_remove("JAVA_HOME") - .output() - .await - .expect("should be able to invoke java_home outside seatbelt"); - if !baseline_output.status.success() { - eprintln!( - "java_home exited with {:?} outside seatbelt, skipping test", - baseline_output.status - ); - return; - } - - let policy = SandboxPolicy::new_read_only_policy(); - let command_cwd = std::env::current_dir().expect("getcwd"); - let sandbox_cwd = command_cwd.clone(); - - let mut env: HashMap = std::env::vars().collect(); - env.remove("JAVA_HOME"); - env.remove(CODEX_SANDBOX_ENV_VAR); - - let child = spawn_command_under_seatbelt( - vec![java_home_path.to_string_lossy().to_string()], - command_cwd, - &policy, - sandbox_cwd.as_path(), - StdioPolicy::RedirectForShellTool, - None, - env, - ) - .await - .expect("should be able to spawn java_home under seatbelt"); - - let output = child - .wait_with_output() - .await - .expect("should be able to wait for java_home child"); - assert!( - output.status.success(), - "java_home under seatbelt exited with {:?}, stderr: {}", - output.status, - String::from_utf8_lossy(&output.stderr) - ); - - let stdout = String::from_utf8_lossy(&output.stdout); - assert!( - !stdout.trim().is_empty(), - "java_home stdout unexpectedly empty under seatbelt" - ); -} - -#[expect(clippy::expect_used)] -fn create_test_scenario(tmp: &TempDir) -> TestScenario { - let repo_parent = tmp.path().to_path_buf(); - let repo_root = repo_parent.join("repo"); - let dot_git_dir = repo_root.join(".git"); - - std::fs::create_dir(&repo_root).expect("should be able to create repo root"); - std::fs::create_dir(&dot_git_dir).expect("should be able to create .git dir"); - - TestScenario { - file_outside_repo: repo_parent.join("outside.txt"), - repo_parent, - file_in_repo_root: repo_root.join("repo_file.txt"), - repo_root, - file_in_dot_git_dir: dot_git_dir.join("dot_git_file.txt"), - } -} - -#[expect(clippy::expect_used)] -/// Note that `path` must be absolute. -async fn touch(path: &Path, policy: &SandboxPolicy) -> bool { - assert!(path.is_absolute(), "Path must be absolute: {path:?}"); - let command_cwd = std::env::current_dir().expect("getcwd"); - let sandbox_cwd = command_cwd.clone(); - let mut child = spawn_command_under_seatbelt( - vec![ - "/usr/bin/touch".to_string(), - path.to_string_lossy().to_string(), - ], - command_cwd, - policy, - sandbox_cwd.as_path(), - StdioPolicy::RedirectForShellTool, - None, - HashMap::new(), - ) - .await - .expect("should be able to spawn command under seatbelt"); - child - .wait() - .await - .expect("should be able to wait for child process") - .success() -} diff --git a/codex-rs/exec/tests/suite/mod.rs b/codex-rs/exec/tests/suite/mod.rs index d86d079774..a8b51e039b 100644 --- a/codex-rs/exec/tests/suite/mod.rs +++ b/codex-rs/exec/tests/suite/mod.rs @@ -7,5 +7,4 @@ mod mcp_required_exit; mod originator; mod output_schema; mod resume; -mod sandbox; mod server_error_exit; diff --git a/codex-rs/exec/tests/suite/sandbox.rs b/codex-rs/exec/tests/suite/sandbox.rs deleted file mode 100644 index e6dd37927b..0000000000 --- a/codex-rs/exec/tests/suite/sandbox.rs +++ /dev/null @@ -1,442 +0,0 @@ -#![cfg(unix)] -use codex_core::spawn::StdioPolicy; -use codex_protocol::protocol::SandboxPolicy; -use codex_utils_absolute_path::AbsolutePathBuf; -use std::collections::HashMap; -use std::future::Future; -use std::io; -use std::path::Path; -use std::path::PathBuf; -use std::process::ExitStatus; -use tokio::fs::create_dir_all; -use tokio::process::Child; - -#[cfg(target_os = "macos")] -async fn spawn_command_under_sandbox( - command: Vec, - command_cwd: PathBuf, - sandbox_policy: &SandboxPolicy, - sandbox_cwd: &Path, - stdio_policy: StdioPolicy, - env: HashMap, -) -> std::io::Result { - use codex_core::seatbelt::spawn_command_under_seatbelt; - spawn_command_under_seatbelt( - command, - command_cwd, - sandbox_policy, - sandbox_cwd, - stdio_policy, - None, - env, - ) - .await -} - -#[cfg(target_os = "linux")] -async fn spawn_command_under_sandbox( - command: Vec, - command_cwd: PathBuf, - sandbox_policy: &SandboxPolicy, - sandbox_cwd: &Path, - stdio_policy: StdioPolicy, - env: HashMap, -) -> std::io::Result { - use codex_core::landlock::spawn_command_under_linux_sandbox; - let codex_linux_sandbox_exe = codex_utils_cargo_bin::cargo_bin("codex-exec") - .map_err(|err| io::Error::new(io::ErrorKind::NotFound, err))?; - spawn_command_under_linux_sandbox( - codex_linux_sandbox_exe, - command, - command_cwd, - sandbox_policy, - sandbox_cwd, - false, - stdio_policy, - None, - env, - ) - .await -} - -#[cfg(target_os = "linux")] -/// Determines whether Linux sandbox tests can run on this host. -/// -/// These tests require an enforceable filesystem sandbox. We run a tiny command -/// under the production Landlock path and skip when enforcement is unavailable -/// (for example on kernels or container profiles where Landlock is not -/// enforced). -async fn linux_sandbox_test_env() -> Option> { - let command_cwd = std::env::current_dir().ok()?; - let sandbox_cwd = command_cwd.clone(); - let policy = SandboxPolicy::new_read_only_policy(); - - if can_apply_linux_sandbox_policy(&policy, &command_cwd, sandbox_cwd.as_path(), HashMap::new()) - .await - { - return Some(HashMap::new()); - } - - eprintln!("Skipping test: Landlock is not enforceable on this host."); - None -} - -#[cfg(target_os = "linux")] -/// Returns whether a minimal command can run successfully with the requested -/// Linux sandbox policy applied. -/// -/// This is used as a capability probe so sandbox behavior tests only run when -/// Landlock enforcement is actually active. -async fn can_apply_linux_sandbox_policy( - policy: &SandboxPolicy, - command_cwd: &Path, - sandbox_cwd: &Path, - env: HashMap, -) -> bool { - let spawn_result = spawn_command_under_sandbox( - vec!["/usr/bin/true".to_string()], - command_cwd.to_path_buf(), - policy, - sandbox_cwd, - StdioPolicy::RedirectForShellTool, - env, - ) - .await; - let Ok(mut child) = spawn_result else { - return false; - }; - child - .wait() - .await - .map(|status| status.success()) - .unwrap_or(false) -} - -#[tokio::test] -async fn python_multiprocessing_lock_works_under_sandbox() { - core_test_support::skip_if_sandbox!(); - #[cfg(target_os = "linux")] - let sandbox_env = match linux_sandbox_test_env().await { - Some(env) => env, - None => return, - }; - #[cfg(not(target_os = "linux"))] - let sandbox_env = HashMap::new(); - #[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 = vec!["/dev/shm".try_into().unwrap()]; - - let policy = SandboxPolicy::WorkspaceWrite { - writable_roots, - read_only_access: Default::default(), - 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 command_cwd = std::env::current_dir().expect("should be able to get current dir"); - let sandbox_cwd = command_cwd.clone(); - let mut child = spawn_command_under_sandbox( - vec![ - "python3".to_string(), - "-c".to_string(), - python_code.to_string(), - ], - command_cwd, - &policy, - sandbox_cwd.as_path(), - StdioPolicy::Inherit, - sandbox_env, - ) - .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:?}"); -} - -#[tokio::test] -async fn python_getpwuid_works_under_sandbox() { - core_test_support::skip_if_sandbox!(); - #[cfg(target_os = "linux")] - let sandbox_env = match linux_sandbox_test_env().await { - Some(env) => env, - None => return, - }; - #[cfg(not(target_os = "linux"))] - let sandbox_env = HashMap::new(); - - if std::process::Command::new("python3") - .arg("--version") - .status() - .is_err() - { - eprintln!("python3 not found in PATH, skipping test."); - return; - } - - let policy = SandboxPolicy::new_read_only_policy(); - let command_cwd = std::env::current_dir().expect("should be able to get current dir"); - let sandbox_cwd = command_cwd.clone(); - - let mut child = spawn_command_under_sandbox( - vec![ - "python3".to_string(), - "-c".to_string(), - "import pwd, os; print(pwd.getpwuid(os.getuid()))".to_string(), - ], - command_cwd, - &policy, - sandbox_cwd.as_path(), - StdioPolicy::RedirectForShellTool, - sandbox_env, - ) - .await - .expect("should be able to spawn python under sandbox"); - - let status = child - .wait() - .await - .expect("should be able to wait for child process"); - assert!(status.success(), "python exited with {status:?}"); -} - -#[tokio::test] -async fn sandbox_distinguishes_command_and_policy_cwds() { - core_test_support::skip_if_sandbox!(); - #[cfg(target_os = "linux")] - let sandbox_env = match linux_sandbox_test_env().await { - Some(env) => env, - None => return, - }; - #[cfg(not(target_os = "linux"))] - let sandbox_env = HashMap::new(); - let temp = tempfile::tempdir().expect("should be able to create temp dir"); - let sandbox_root = temp.path().join("sandbox"); - let command_root = temp.path().join("command"); - create_dir_all(&sandbox_root).await.expect("mkdir"); - create_dir_all(&command_root).await.expect("mkdir"); - let canonical_sandbox_root = tokio::fs::canonicalize(&sandbox_root) - .await - .expect("canonicalize sandbox root"); - let canonical_allowed_path = canonical_sandbox_root.join("allowed.txt"); - - let disallowed_path = command_root.join("forbidden.txt"); - - // Note writable_roots is empty: verify that `canonical_allowed_path` is - // writable only because it is under the sandbox policy cwd, not because it - // is under a writable root. - let policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![], - read_only_access: Default::default(), - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: true, - }; - - // Attempt to write inside the command cwd, which is outside of the sandbox policy cwd. - let mut child = spawn_command_under_sandbox( - vec![ - "bash".to_string(), - "-lc".to_string(), - "echo forbidden > forbidden.txt".to_string(), - ], - command_root.clone(), - &policy, - canonical_sandbox_root.as_path(), - StdioPolicy::Inherit, - sandbox_env.clone(), - ) - .await - .expect("should spawn command writing to forbidden path"); - - let status = child - .wait() - .await - .expect("should wait for forbidden command"); - assert!( - !status.success(), - "sandbox unexpectedly allowed writing to command cwd: {status:?}" - ); - let forbidden_exists = tokio::fs::try_exists(&disallowed_path) - .await - .expect("try_exists failed"); - assert!( - !forbidden_exists, - "forbidden path should not have been created" - ); - - // Writing to the sandbox policy cwd after changing directories into it should succeed. - let mut child = spawn_command_under_sandbox( - vec![ - "/usr/bin/touch".to_string(), - canonical_allowed_path.to_string_lossy().into_owned(), - ], - command_root, - &policy, - canonical_sandbox_root.as_path(), - StdioPolicy::Inherit, - sandbox_env, - ) - .await - .expect("should spawn command writing to sandbox root"); - - let status = child.wait().await.expect("should wait for allowed command"); - assert!( - status.success(), - "sandbox blocked allowed write: {status:?}" - ); - let allowed_exists = tokio::fs::try_exists(&canonical_allowed_path) - .await - .expect("try_exists allowed failed"); - assert!(allowed_exists, "allowed path should exist"); -} - -fn unix_sock_body() { - unsafe { - let mut fds = [0i32; 2]; - let r = libc::socketpair(libc::AF_UNIX, libc::SOCK_DGRAM, 0, fds.as_mut_ptr()); - assert_eq!( - r, - 0, - "socketpair(AF_UNIX, SOCK_DGRAM) failed: {}", - io::Error::last_os_error() - ); - - let msg = b"hello_unix"; - // write() from one end (generic write is allowed) - let sent = libc::write(fds[0], msg.as_ptr() as *const libc::c_void, msg.len()); - assert!(sent >= 0, "write() failed: {}", io::Error::last_os_error()); - - // recvfrom() on the other end. We don’t need the address for socketpair, - // so we pass null pointers for src address. - let mut buf = [0u8; 64]; - let recvd = libc::recvfrom( - fds[1], - buf.as_mut_ptr() as *mut libc::c_void, - buf.len(), - 0, - std::ptr::null_mut(), - std::ptr::null_mut(), - ); - assert!( - recvd >= 0, - "recvfrom() failed: {}", - io::Error::last_os_error() - ); - - let recvd_slice = &buf[..(recvd as usize)]; - assert_eq!( - recvd_slice, - &msg[..], - "payload mismatch: sent {} bytes, got {} bytes", - msg.len(), - recvd - ); - - // Also exercise AF_UNIX stream socketpair quickly to ensure AF_UNIX in general works. - let mut sfds = [0i32; 2]; - let sr = libc::socketpair(libc::AF_UNIX, libc::SOCK_STREAM, 0, sfds.as_mut_ptr()); - assert_eq!( - sr, - 0, - "socketpair(AF_UNIX, SOCK_STREAM) failed: {}", - io::Error::last_os_error() - ); - let snt2 = libc::write(sfds[0], msg.as_ptr() as *const libc::c_void, msg.len()); - assert!( - snt2 >= 0, - "write(stream) failed: {}", - io::Error::last_os_error() - ); - let mut b2 = [0u8; 64]; - let rcv2 = libc::recv(sfds[1], b2.as_mut_ptr() as *mut libc::c_void, b2.len(), 0); - assert!( - rcv2 >= 0, - "recv(stream) failed: {}", - io::Error::last_os_error() - ); - - // Clean up - let _ = libc::close(sfds[0]); - let _ = libc::close(sfds[1]); - let _ = libc::close(fds[0]); - let _ = libc::close(fds[1]); - } -} - -#[tokio::test] -async fn allow_unix_socketpair_recvfrom() { - run_code_under_sandbox( - "allow_unix_socketpair_recvfrom", - &SandboxPolicy::new_read_only_policy(), - || async { unix_sock_body() }, - ) - .await - .expect("should be able to reexec"); -} - -const IN_SANDBOX_ENV_VAR: &str = "IN_SANDBOX"; - -#[expect(clippy::expect_used)] -pub async fn run_code_under_sandbox( - test_selector: &str, - policy: &SandboxPolicy, - child_body: F, -) -> io::Result> -where - F: FnOnce() -> Fut + Send + 'static, - Fut: Future + Send + 'static, -{ - if std::env::var(IN_SANDBOX_ENV_VAR).is_err() { - let exe = std::env::current_exe()?; - let mut cmds = vec![exe.to_string_lossy().into_owned(), "--exact".into()]; - let mut stdio_policy = StdioPolicy::RedirectForShellTool; - // Allow for us to pass forward --nocapture / use the right stdio policy. - if std::env::args().any(|a| a == "--nocapture") { - cmds.push("--nocapture".into()); - stdio_policy = StdioPolicy::Inherit; - } - cmds.push(test_selector.into()); - - // Your existing launcher: - let command_cwd = std::env::current_dir().expect("should be able to get current dir"); - let sandbox_cwd = command_cwd.clone(); - let mut child = spawn_command_under_sandbox( - cmds, - command_cwd, - policy, - sandbox_cwd.as_path(), - stdio_policy, - HashMap::from([("IN_SANDBOX".into(), "1".into())]), - ) - .await?; - - let status = child.wait().await?; - Ok(Some(status)) - } else { - // Child branch: run the provided body. - child_body().await; - Ok(None) - } -}