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