From e49ceb1db5db32766ea832d1dcd837f748d70c4b Mon Sep 17 00:00:00 2001 From: Aaron Levine Date: Tue, 14 Apr 2026 00:07:17 -0700 Subject: [PATCH 1/5] Support Unix socket allowlists in macOS sandbox --- codex-rs/Cargo.lock | 4 + codex-rs/cli/Cargo.toml | 1 + codex-rs/cli/src/debug_sandbox.rs | 26 ++- codex-rs/cli/src/lib.rs | 10 + codex-rs/core/src/seatbelt.rs | 16 +- codex-rs/sandboxing/Cargo.toml | 3 + codex-rs/sandboxing/src/manager.rs | 15 +- codex-rs/sandboxing/src/seatbelt.rs | 147 +++++++++------ codex-rs/sandboxing/src/seatbelt_tests.rs | 215 +++++++++++++++++++--- 9 files changed, 339 insertions(+), 98 deletions(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index fc5c70e6b667..1b7de6955b41 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1692,6 +1692,7 @@ dependencies = [ "codex-stdio-to-uds", "codex-terminal-detection", "codex-tui", + "codex-utils-absolute-path", "codex-utils-cargo-bin", "codex-utils-cli", "codex-utils-path", @@ -2689,6 +2690,8 @@ dependencies = [ name = "codex-sandboxing" version = "0.0.0" dependencies = [ + "anyhow", + "async-trait", "codex-network-proxy", "codex-protocol", "codex-utils-absolute-path", @@ -2697,6 +2700,7 @@ dependencies = [ "pretty_assertions", "serde_json", "tempfile", + "tokio", "tracing", "url", "which 8.0.0", diff --git a/codex-rs/cli/Cargo.toml b/codex-rs/cli/Cargo.toml index ab0d86de2c7e..ea8a0e01da63 100644 --- a/codex-rs/cli/Cargo.toml +++ b/codex-rs/cli/Cargo.toml @@ -44,6 +44,7 @@ codex-state = { workspace = true } codex-stdio-to-uds = { workspace = true } codex-terminal-detection = { workspace = true } codex-tui = { workspace = true } +codex-utils-absolute-path = { workspace = true } codex-utils-path = { workspace = true } libc = { workspace = true } owo-colors = { workspace = true } diff --git a/codex-rs/cli/src/debug_sandbox.rs b/codex-rs/cli/src/debug_sandbox.rs index 0ad2f36382ad..a38df89ab0ff 100644 --- a/codex-rs/cli/src/debug_sandbox.rs +++ b/codex-rs/cli/src/debug_sandbox.rs @@ -18,7 +18,10 @@ use codex_protocol::config_types::SandboxMode; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_sandboxing::landlock::create_linux_sandbox_command_args_for_policies; #[cfg(target_os = "macos")] -use codex_sandboxing::seatbelt::create_seatbelt_command_args_for_policies; +use codex_sandboxing::seatbelt::SeatbeltCommandArgs; +#[cfg(target_os = "macos")] +use codex_sandboxing::seatbelt::create_seatbelt_command_args; +use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_cli::CliConfigOverrides; use tokio::process::Child; use tokio::process::Command as TokioCommand; @@ -39,6 +42,7 @@ pub async fn run_command_under_seatbelt( ) -> anyhow::Result<()> { let SeatbeltCommand { full_auto, + allow_unix_sockets, log_denials, config_overrides, command, @@ -50,6 +54,7 @@ pub async fn run_command_under_seatbelt( codex_linux_sandbox_exe, SandboxType::Seatbelt, log_denials, + &allow_unix_sockets, ) .await } @@ -78,6 +83,7 @@ pub async fn run_command_under_landlock( codex_linux_sandbox_exe, SandboxType::Landlock, /*log_denials*/ false, + &[], ) .await } @@ -98,6 +104,7 @@ pub async fn run_command_under_windows( codex_linux_sandbox_exe, SandboxType::Windows, /*log_denials*/ false, + &[], ) .await } @@ -116,6 +123,8 @@ async fn run_command_under_sandbox( codex_linux_sandbox_exe: Option, sandbox_type: SandboxType, log_denials: bool, + #[cfg_attr(not(target_os = "macos"), allow(unused_variables))] + allow_unix_sockets: &[AbsolutePathBuf], ) -> anyhow::Result<()> { let config = load_debug_sandbox_config( config_overrides @@ -252,14 +261,15 @@ async fn run_command_under_sandbox( let mut child = match sandbox_type { #[cfg(target_os = "macos")] SandboxType::Seatbelt => { - let args = create_seatbelt_command_args_for_policies( + let args = create_seatbelt_command_args(SeatbeltCommandArgs { command, - &config.permissions.file_system_sandbox_policy, - config.permissions.network_sandbox_policy, - sandbox_policy_cwd.as_path(), - /*enforce_managed_network*/ false, - network.as_ref(), - ); + file_system_sandbox_policy: &config.permissions.file_system_sandbox_policy, + network_sandbox_policy: config.permissions.network_sandbox_policy, + sandbox_policy_cwd: sandbox_policy_cwd.as_path(), + enforce_managed_network: false, + network: network.as_ref(), + extra_allow_unix_sockets: allow_unix_sockets, + }); let network_policy = config.permissions.network_sandbox_policy; spawn_debug_sandbox_child( PathBuf::from("/usr/bin/sandbox-exec"), diff --git a/codex-rs/cli/src/lib.rs b/codex-rs/cli/src/lib.rs index 848391293c2e..e0ae8c8ae93b 100644 --- a/codex-rs/cli/src/lib.rs +++ b/codex-rs/cli/src/lib.rs @@ -3,6 +3,7 @@ mod exit_status; pub(crate) mod login; use clap::Parser; +use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_cli::CliConfigOverrides; pub use debug_sandbox::run_command_under_landlock; @@ -22,6 +23,10 @@ pub struct SeatbeltCommand { #[arg(long = "full-auto", default_value_t = false)] pub full_auto: bool, + /// Allow the sandboxed command to bind/connect AF_UNIX sockets rooted at this path. Relative paths are resolved against the current directory. Repeat to allow multiple paths. + #[arg(long = "allow-unix-socket", value_parser = parse_allow_unix_socket_path)] + pub allow_unix_sockets: Vec, + /// While the command runs, capture macOS sandbox denials via `log stream` and print them after exit #[arg(long = "log-denials", default_value_t = false)] pub log_denials: bool, @@ -34,6 +39,11 @@ pub struct SeatbeltCommand { pub command: Vec, } +fn parse_allow_unix_socket_path(raw: &str) -> Result { + AbsolutePathBuf::relative_to_current_dir(raw) + .map_err(|err| format!("invalid path {raw:?}: {err}")) +} + #[derive(Debug, Parser)] pub struct LandlockCommand { /// Convenience alias for low-friction sandboxed automatic execution (network-disabled sandbox that can write to cwd and TMPDIR) diff --git a/codex-rs/core/src/seatbelt.rs b/codex-rs/core/src/seatbelt.rs index cc59f65c836b..411c1fa8a36a 100644 --- a/codex-rs/core/src/seatbelt.rs +++ b/codex-rs/core/src/seatbelt.rs @@ -9,7 +9,8 @@ use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::SandboxPolicy; use codex_sandboxing::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE; -use codex_sandboxing::seatbelt::create_seatbelt_command_args_for_policies; +use codex_sandboxing::seatbelt::SeatbeltCommandArgs; +use codex_sandboxing::seatbelt::create_seatbelt_command_args; use std::collections::HashMap; use std::path::Path; use std::path::PathBuf; @@ -24,14 +25,17 @@ pub async fn spawn_command_under_seatbelt( network: Option<&NetworkProxy>, mut env: HashMap, ) -> std::io::Result { - let args = create_seatbelt_command_args_for_policies( + let file_system_sandbox_policy = + FileSystemSandboxPolicy::from_legacy_sandbox_policy(sandbox_policy, sandbox_policy_cwd); + let args = create_seatbelt_command_args(SeatbeltCommandArgs { command, - &FileSystemSandboxPolicy::from_legacy_sandbox_policy(sandbox_policy, sandbox_policy_cwd), - NetworkSandboxPolicy::from(sandbox_policy), + file_system_sandbox_policy: &file_system_sandbox_policy, + network_sandbox_policy: NetworkSandboxPolicy::from(sandbox_policy), sandbox_policy_cwd, - /*enforce_managed_network*/ false, + enforce_managed_network: false, network, - ); + extra_allow_unix_sockets: &[], + }); let arg0 = None; env.insert(CODEX_SANDBOX_ENV_VAR.to_string(), "seatbelt".to_string()); spawn_child_async(SpawnChildRequest { diff --git a/codex-rs/sandboxing/Cargo.toml b/codex-rs/sandboxing/Cargo.toml index 274e5bce228e..18c5ffb45dcc 100644 --- a/codex-rs/sandboxing/Cargo.toml +++ b/codex-rs/sandboxing/Cargo.toml @@ -23,5 +23,8 @@ url = { workspace = true } which = { workspace = true } [dev-dependencies] +anyhow = { workspace = true } +async-trait = { workspace = true } pretty_assertions = { workspace = true } tempfile = { workspace = true } +tokio = { workspace = true, features = ["macros", "rt"] } diff --git a/codex-rs/sandboxing/src/manager.rs b/codex-rs/sandboxing/src/manager.rs index ebff33f105b6..8eb1f6ad38a3 100644 --- a/codex-rs/sandboxing/src/manager.rs +++ b/codex-rs/sandboxing/src/manager.rs @@ -12,7 +12,9 @@ use crate::policy_transforms::should_require_platform_sandbox; #[cfg(target_os = "macos")] use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE; #[cfg(target_os = "macos")] -use crate::seatbelt::create_seatbelt_command_args_for_policies; +use crate::seatbelt::SeatbeltCommandArgs; +#[cfg(target_os = "macos")] +use crate::seatbelt::create_seatbelt_command_args; use codex_network_proxy::NetworkProxy; use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::models::PermissionProfile; @@ -208,14 +210,15 @@ impl SandboxManager { SandboxType::None => (os_argv_to_strings(argv), None), #[cfg(target_os = "macos")] SandboxType::MacosSeatbelt => { - let mut args = create_seatbelt_command_args_for_policies( - os_argv_to_strings(argv), - &effective_file_system_policy, - effective_network_policy, + let mut args = create_seatbelt_command_args(SeatbeltCommandArgs { + command: os_argv_to_strings(argv), + file_system_sandbox_policy: &effective_file_system_policy, + network_sandbox_policy: effective_network_policy, sandbox_policy_cwd, enforce_managed_network, network, - ); + extra_allow_unix_sockets: &[], + }); let mut full_command = Vec::with_capacity(1 + args.len()); full_command.push(MACOS_PATH_TO_SEATBELT_EXECUTABLE.to_string()); full_command.append(&mut args); diff --git a/codex-rs/sandboxing/src/seatbelt.rs b/codex-rs/sandboxing/src/seatbelt.rs index 517a9163df5e..fff11d3808ce 100644 --- a/codex-rs/sandboxing/src/seatbelt.rs +++ b/codex-rs/sandboxing/src/seatbelt.rs @@ -100,41 +100,54 @@ struct UnixSocketPathParam { path: AbsolutePathBuf, } -fn proxy_policy_inputs(network: Option<&NetworkProxy>) -> ProxyPolicyInputs { - if let Some(network) = network { - let mut env = HashMap::new(); - network.apply_to_env(&mut env); - let unix_domain_socket_policy = if network.dangerously_allow_all_unix_sockets() { - UnixDomainSocketPolicy::AllowAll - } else { - let allowed = network - .allow_unix_sockets() - .iter() - .filter_map( - |socket_path| match normalize_path_for_sandbox(Path::new(socket_path)) { - Some(path) => Some((path.to_string_lossy().to_string(), path)), - None => { - warn!( - "ignoring network.allow_unix_sockets entry because it could not be normalized: {socket_path}" - ); - None +fn proxy_policy_inputs( + network: Option<&NetworkProxy>, + extra_allow_unix_sockets: &[AbsolutePathBuf], +) -> ProxyPolicyInputs { + let extra_allowed = extra_allow_unix_sockets + .iter() + .filter_map(|socket_path| normalize_path_for_sandbox(socket_path.as_path())) + .collect::>(); + + match network { + Some(network) => { + let mut env = HashMap::new(); + network.apply_to_env(&mut env); + let unix_domain_socket_policy = if network.dangerously_allow_all_unix_sockets() { + UnixDomainSocketPolicy::AllowAll + } else { + let mut allowed = network + .allow_unix_sockets() + .iter() + .filter_map(|socket_path| { + match normalize_path_for_sandbox(Path::new(socket_path)) { + Some(path) => Some(path), + None => { + warn!( + "ignoring network.allow_unix_sockets entry because it could not be normalized: {socket_path}" + ); + None + } } - }, - ) - .collect::>() - .into_values() - .collect(); - UnixDomainSocketPolicy::Restricted { allowed } - }; - return ProxyPolicyInputs { - ports: proxy_loopback_ports_from_env(&env), - has_proxy_config: has_proxy_url_env_vars(&env), - allow_local_binding: network.allow_local_binding(), - unix_domain_socket_policy, - }; + }) + .collect::>(); + allowed.extend(extra_allowed); + UnixDomainSocketPolicy::Restricted { allowed } + }; + ProxyPolicyInputs { + ports: proxy_loopback_ports_from_env(&env), + has_proxy_config: has_proxy_url_env_vars(&env), + allow_local_binding: network.allow_local_binding(), + unix_domain_socket_policy, + } + } + None => ProxyPolicyInputs { + unix_domain_socket_policy: UnixDomainSocketPolicy::Restricted { + allowed: extra_allowed, + }, + ..Default::default() + }, } - - ProxyPolicyInputs::default() } fn normalize_path_for_sandbox(path: &Path) -> Option { @@ -244,8 +257,17 @@ fn dynamic_network_policy_for_network( enforce_managed_network: bool, proxy: &ProxyPolicyInputs, ) -> String { - let should_use_restricted_network_policy = - !proxy.ports.is_empty() || proxy.has_proxy_config || enforce_managed_network; + let has_unix_socket_access = matches!( + proxy.unix_domain_socket_policy, + UnixDomainSocketPolicy::AllowAll + ) || matches!( + &proxy.unix_domain_socket_policy, + UnixDomainSocketPolicy::Restricted { allowed } if !allowed.is_empty() + ); + let should_use_restricted_network_policy = !proxy.ports.is_empty() + || proxy.has_proxy_config + || enforce_managed_network + || (!network_policy.is_enabled() && has_unix_socket_access); if should_use_restricted_network_policy { let mut policy = String::new(); if proxy.allow_local_binding { @@ -285,9 +307,13 @@ fn dynamic_network_policy_for_network( if network_policy.is_enabled() { // No proxy env is configured: retain the existing full-network behavior. - format!( - "(allow network-outbound)\n(allow network-inbound)\n{MACOS_SEATBELT_NETWORK_POLICY}" - ) + let mut policy = String::from("(allow network-outbound)\n(allow network-inbound)\n"); + let unix_socket_policy = unix_socket_policy(proxy); + if !unix_socket_policy.is_empty() { + policy.push_str("; allow unix domain sockets for local IPC\n"); + policy.push_str(&unix_socket_policy); + } + format!("{policy}{MACOS_SEATBELT_NETWORK_POLICY}") } else { String::new() } @@ -357,31 +383,48 @@ fn build_seatbelt_access_policy( } #[cfg_attr(not(test), allow(dead_code))] -fn create_seatbelt_command_args( +fn create_seatbelt_command_args_for_legacy_policy( command: Vec, sandbox_policy: &SandboxPolicy, sandbox_policy_cwd: &Path, enforce_managed_network: bool, network: Option<&NetworkProxy>, ) -> Vec { - create_seatbelt_command_args_for_policies( + let file_system_sandbox_policy = + FileSystemSandboxPolicy::from_legacy_sandbox_policy(sandbox_policy, sandbox_policy_cwd); + create_seatbelt_command_args(SeatbeltCommandArgs { command, - &FileSystemSandboxPolicy::from_legacy_sandbox_policy(sandbox_policy, sandbox_policy_cwd), - NetworkSandboxPolicy::from(sandbox_policy), + file_system_sandbox_policy: &file_system_sandbox_policy, + network_sandbox_policy: NetworkSandboxPolicy::from(sandbox_policy), sandbox_policy_cwd, enforce_managed_network, network, - ) + extra_allow_unix_sockets: &[], + }) } -pub fn create_seatbelt_command_args_for_policies( - command: Vec, - file_system_sandbox_policy: &FileSystemSandboxPolicy, - network_sandbox_policy: NetworkSandboxPolicy, - sandbox_policy_cwd: &Path, - enforce_managed_network: bool, - network: Option<&NetworkProxy>, -) -> Vec { +#[derive(Debug)] +pub struct SeatbeltCommandArgs<'a> { + pub command: Vec, + pub file_system_sandbox_policy: &'a FileSystemSandboxPolicy, + pub network_sandbox_policy: NetworkSandboxPolicy, + pub sandbox_policy_cwd: &'a Path, + pub enforce_managed_network: bool, + pub network: Option<&'a NetworkProxy>, + pub extra_allow_unix_sockets: &'a [AbsolutePathBuf], +} + +pub fn create_seatbelt_command_args(args: SeatbeltCommandArgs<'_>) -> Vec { + let SeatbeltCommandArgs { + command, + file_system_sandbox_policy, + network_sandbox_policy, + sandbox_policy_cwd, + enforce_managed_network, + network, + extra_allow_unix_sockets, + } = args; + let unreadable_roots = file_system_sandbox_policy.get_unreadable_roots_with_cwd(sandbox_policy_cwd); let (file_write_policy, file_write_dir_params) = @@ -465,7 +508,7 @@ pub fn create_seatbelt_command_args_for_policies( } }; - let proxy = proxy_policy_inputs(network); + let proxy = proxy_policy_inputs(network, extra_allow_unix_sockets); let network_policy = dynamic_network_policy_for_network(network_sandbox_policy, enforce_managed_network, &proxy); diff --git a/codex-rs/sandboxing/src/seatbelt_tests.rs b/codex-rs/sandboxing/src/seatbelt_tests.rs index 419f3968d6ab..eddac1c882dc 100644 --- a/codex-rs/sandboxing/src/seatbelt_tests.rs +++ b/codex-rs/sandboxing/src/seatbelt_tests.rs @@ -1,14 +1,23 @@ use super::MACOS_PATH_TO_SEATBELT_EXECUTABLE; use super::MACOS_SEATBELT_BASE_POLICY; use super::ProxyPolicyInputs; +use super::SeatbeltCommandArgs; use super::UnixDomainSocketPolicy; use super::create_seatbelt_command_args; -use super::create_seatbelt_command_args_for_policies; +use super::create_seatbelt_command_args_for_legacy_policy; use super::dynamic_network_policy; use super::macos_dir_params; use super::normalize_path_for_sandbox; use super::unix_socket_dir_params; use super::unix_socket_policy; +use codex_network_proxy::ConfigReloader; +use codex_network_proxy::ConfigState; +use codex_network_proxy::NetworkMode; +use codex_network_proxy::NetworkProxy; +use codex_network_proxy::NetworkProxyConfig; +use codex_network_proxy::NetworkProxyConstraints; +use codex_network_proxy::NetworkProxyState; +use codex_network_proxy::build_config_state; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; @@ -23,6 +32,7 @@ use std::fs; use std::path::Path; use std::path::PathBuf; use std::process::Command; +use std::sync::Arc; use tempfile::TempDir; fn assert_seatbelt_denied(stderr: &[u8], path: &Path) { @@ -48,6 +58,23 @@ fn seatbelt_policy_arg(args: &[String]) -> &str { .expect("seatbelt args should include policy text") } +struct TestConfigReloader; + +#[async_trait::async_trait] +impl ConfigReloader for TestConfigReloader { + fn source_label(&self) -> String { + "seatbelt test config".to_string() + } + + async fn maybe_reload(&self) -> anyhow::Result> { + Ok(None) + } + + async fn reload_now(&self) -> anyhow::Result { + Err(anyhow::anyhow!("seatbelt test config cannot reload")) + } +} + #[test] fn base_policy_allows_node_cpu_sysctls() { assert!( @@ -128,14 +155,15 @@ fn explicit_unreadable_paths_are_excluded_from_full_disk_read_and_write_access() }, ]); - let args = create_seatbelt_command_args_for_policies( - vec!["/bin/true".to_string()], - &file_system_policy, - NetworkSandboxPolicy::Restricted, - Path::new("/"), - /*enforce_managed_network*/ false, - /*network*/ None, - ); + let args = create_seatbelt_command_args(SeatbeltCommandArgs { + command: vec!["/bin/true".to_string()], + file_system_sandbox_policy: &file_system_policy, + network_sandbox_policy: NetworkSandboxPolicy::Restricted, + sandbox_policy_cwd: Path::new("/"), + enforce_managed_network: false, + network: None, + extra_allow_unix_sockets: &[], + }); let policy = seatbelt_policy_arg(&args); let unreadable_roots = file_system_policy.get_unreadable_roots_with_cwd(Path::new("/")); @@ -193,14 +221,15 @@ fn explicit_unreadable_paths_are_excluded_from_readable_roots() { }, ]); - let args = create_seatbelt_command_args_for_policies( - vec!["/bin/true".to_string()], - &file_system_policy, - NetworkSandboxPolicy::Restricted, - Path::new("/"), - /*enforce_managed_network*/ false, - /*network*/ None, - ); + let args = create_seatbelt_command_args(SeatbeltCommandArgs { + command: vec!["/bin/true".to_string()], + file_system_sandbox_policy: &file_system_policy, + network_sandbox_policy: NetworkSandboxPolicy::Restricted, + sandbox_policy_cwd: Path::new("/"), + enforce_managed_network: false, + network: None, + extra_allow_unix_sockets: &[], + }); let policy = seatbelt_policy_arg(&args); let readable_roots = file_system_policy.get_readable_roots_with_cwd(Path::new("/")); @@ -231,7 +260,7 @@ fn explicit_unreadable_paths_are_excluded_from_readable_roots() { #[test] fn seatbelt_args_without_extension_profile_keep_legacy_preferences_read_access() { let cwd = std::env::temp_dir(); - let args = create_seatbelt_command_args( + let args = create_seatbelt_command_args_for_legacy_policy( vec!["echo".to_string(), "ok".to_string()], &SandboxPolicy::new_read_only_policy(), cwd.as_path(), @@ -249,7 +278,7 @@ fn seatbelt_legacy_workspace_write_nested_readable_root_stays_writable() { let cwd = tmp.path().join("workspace"); fs::create_dir_all(cwd.join("docs")).expect("create docs"); let docs = AbsolutePathBuf::from_absolute_path(cwd.join("docs")).expect("absolute docs"); - let args = create_seatbelt_command_args( + let args = create_seatbelt_command_args_for_legacy_policy( vec!["/bin/true".to_string()], &SandboxPolicy::WorkspaceWrite { writable_roots: vec![], @@ -451,6 +480,140 @@ fn create_seatbelt_args_allowlists_unix_socket_paths() { ); } +#[test] +fn create_seatbelt_args_allowlists_explicit_unix_socket_paths_without_proxy() { + let cwd = TempDir::new().expect("temp cwd"); + let file_system_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy( + &SandboxPolicy::new_read_only_policy(), + cwd.path(), + ); + let extra_allow_unix_sockets = vec![absolute_path("/tmp/codex-browser-use")]; + let args = create_seatbelt_command_args(SeatbeltCommandArgs { + command: vec!["/usr/bin/true".to_string()], + file_system_sandbox_policy: &file_system_policy, + network_sandbox_policy: NetworkSandboxPolicy::Restricted, + sandbox_policy_cwd: cwd.path(), + enforce_managed_network: false, + network: None, + extra_allow_unix_sockets: &extra_allow_unix_sockets, + }); + let policy = seatbelt_policy_arg(&args); + + assert!( + policy.contains("(allow system-socket (socket-domain AF_UNIX))"), + "policy should allow AF_UNIX when explicit socket paths are requested:\n{policy}" + ); + assert!( + policy.contains( + "(allow network-outbound (remote unix-socket (subpath (param \"UNIX_SOCKET_PATH_0\"))))" + ), + "policy should allow outbound AF_UNIX traffic for explicit socket paths:\n{policy}" + ); + let expected_socket_root = normalize_path_for_sandbox(Path::new("/tmp/codex-browser-use")) + .expect("socket root should normalize") + .to_string_lossy() + .into_owned(); + assert!( + args.iter() + .any(|arg| arg == &format!("-DUNIX_SOCKET_PATH_0={expected_socket_root}")), + "seatbelt args should pass the configured socket root as a sandbox param: {args:?}" + ); +} + +#[tokio::test] +async fn create_seatbelt_args_merges_proxy_and_explicit_unix_socket_paths() -> anyhow::Result<()> { + let cwd = TempDir::new().expect("temp cwd"); + let file_system_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy( + &SandboxPolicy::new_read_only_policy(), + cwd.path(), + ); + let network_socket = "/tmp/codex-proxy-use"; + let explicit_socket = "/tmp/codex-browser-use"; + let mut network_config = NetworkProxyConfig::default(); + network_config.network.enabled = true; + network_config.network.mode = NetworkMode::Full; + network_config + .network + .set_allow_unix_sockets(vec![network_socket.to_string()]); + let state = build_config_state(network_config, NetworkProxyConstraints::default())?; + let network_proxy = NetworkProxy::builder() + .state(Arc::new(NetworkProxyState::with_reloader( + state, + Arc::new(TestConfigReloader), + ))) + .managed_by_codex(false) + .build() + .await?; + let extra_allow_unix_sockets = vec![absolute_path(explicit_socket)]; + + let args = create_seatbelt_command_args(SeatbeltCommandArgs { + command: vec!["/usr/bin/true".to_string()], + file_system_sandbox_policy: &file_system_policy, + network_sandbox_policy: NetworkSandboxPolicy::Restricted, + sandbox_policy_cwd: cwd.path(), + enforce_managed_network: false, + network: Some(&network_proxy), + extra_allow_unix_sockets: &extra_allow_unix_sockets, + }); + + let expected_explicit_socket = normalize_path_for_sandbox(Path::new(explicit_socket)) + .expect("explicit socket root should normalize"); + let expected_network_socket = normalize_path_for_sandbox(Path::new(network_socket)) + .expect("network socket root should normalize"); + let unix_socket_definitions = args + .iter() + .filter(|arg| arg.starts_with("-DUNIX_SOCKET_PATH_")) + .cloned() + .collect::>(); + assert_eq!( + unix_socket_definitions, + vec![ + format!( + "-DUNIX_SOCKET_PATH_0={}", + expected_explicit_socket.display() + ), + format!("-DUNIX_SOCKET_PATH_1={}", expected_network_socket.display()), + ], + "seatbelt args should include both explicit and network proxy socket roots: {args:?}" + ); + Ok(()) +} + +#[test] +fn create_seatbelt_args_preserves_full_network_with_explicit_unix_socket_paths() { + let cwd = TempDir::new().expect("temp cwd"); + let file_system_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy( + &SandboxPolicy::new_read_only_policy(), + cwd.path(), + ); + let extra_allow_unix_sockets = vec![absolute_path("/tmp/codex-browser-use")]; + let args = create_seatbelt_command_args(SeatbeltCommandArgs { + command: vec!["/usr/bin/true".to_string()], + file_system_sandbox_policy: &file_system_policy, + network_sandbox_policy: NetworkSandboxPolicy::Enabled, + sandbox_policy_cwd: cwd.path(), + enforce_managed_network: false, + network: None, + extra_allow_unix_sockets: &extra_allow_unix_sockets, + }); + let policy = seatbelt_policy_arg(&args); + + assert!( + policy.contains("(allow network-outbound)\n"), + "policy should preserve full outbound network access:\n{policy}" + ); + assert!( + policy.contains("(allow network-inbound)\n"), + "policy should preserve full inbound network access:\n{policy}" + ); + assert!( + policy.contains( + "(allow network-outbound (remote unix-socket (subpath (param \"UNIX_SOCKET_PATH_0\"))))" + ), + "policy should still allow outbound AF_UNIX traffic for explicit socket paths:\n{policy}" + ); +} + #[test] fn unix_socket_policy_non_empty_output_is_newline_terminated() { let allowlist_policy = unix_socket_policy(&ProxyPolicyInputs { @@ -615,7 +778,7 @@ fn create_seatbelt_args_with_read_only_git_and_codex_subpaths() { .iter() .map(std::string::ToString::to_string) .collect(); - let args = create_seatbelt_command_args( + let args = create_seatbelt_command_args_for_legacy_policy( shell_command.clone(), &policy, &cwd, @@ -729,7 +892,7 @@ fn create_seatbelt_args_with_read_only_git_and_codex_subpaths() { .iter() .map(std::string::ToString::to_string) .collect(); - let write_hooks_file_args = create_seatbelt_command_args( + let write_hooks_file_args = create_seatbelt_command_args_for_legacy_policy( shell_command_git, &policy, &cwd, @@ -765,7 +928,7 @@ fn create_seatbelt_args_with_read_only_git_and_codex_subpaths() { .iter() .map(std::string::ToString::to_string) .collect(); - let write_allowed_file_args = create_seatbelt_command_args( + let write_allowed_file_args = create_seatbelt_command_args_for_legacy_policy( shell_command_allowed, &policy, &cwd, @@ -830,7 +993,7 @@ fn create_seatbelt_args_block_first_time_dot_codex_creation_with_exact_and_desce .iter() .map(std::string::ToString::to_string) .collect(); - let args = create_seatbelt_command_args( + let args = create_seatbelt_command_args_for_legacy_policy( shell_command, &policy, repo_root.as_path(), @@ -885,7 +1048,7 @@ fn create_seatbelt_args_with_read_only_git_pointer_file() { .iter() .map(std::string::ToString::to_string) .collect(); - let args = create_seatbelt_command_args( + let args = create_seatbelt_command_args_for_legacy_policy( shell_command, &policy, &cwd, @@ -921,7 +1084,7 @@ fn create_seatbelt_args_with_read_only_git_pointer_file() { .iter() .map(std::string::ToString::to_string) .collect(); - let gitdir_args = create_seatbelt_command_args( + let gitdir_args = create_seatbelt_command_args_for_legacy_policy( shell_command_gitdir, &policy, &cwd, @@ -984,7 +1147,7 @@ fn create_seatbelt_args_for_cwd_as_git_repo() { .iter() .map(std::string::ToString::to_string) .collect(); - let args = create_seatbelt_command_args( + let args = create_seatbelt_command_args_for_legacy_policy( shell_command.clone(), &policy, vulnerable_root.as_path(), From b8695507f71911de1c876d9d38e734952da76119 Mon Sep 17 00:00:00 2001 From: Aaron Levine Date: Tue, 14 Apr 2026 23:18:33 -0700 Subject: [PATCH 2/5] Fix argument comment lint --- codex-rs/sandboxing/src/seatbelt_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codex-rs/sandboxing/src/seatbelt_tests.rs b/codex-rs/sandboxing/src/seatbelt_tests.rs index eddac1c882dc..1f92a1a12dca 100644 --- a/codex-rs/sandboxing/src/seatbelt_tests.rs +++ b/codex-rs/sandboxing/src/seatbelt_tests.rs @@ -541,7 +541,7 @@ async fn create_seatbelt_args_merges_proxy_and_explicit_unix_socket_paths() -> a state, Arc::new(TestConfigReloader), ))) - .managed_by_codex(false) + .managed_by_codex(/*managed_by_codex*/ false) .build() .await?; let extra_allow_unix_sockets = vec![absolute_path(explicit_socket)]; From dae861f6940586be9743a187f385f04ad6bdc518 Mon Sep 17 00:00:00 2001 From: Aaron Levine Date: Tue, 14 Apr 2026 23:24:40 -0700 Subject: [PATCH 3/5] Address sandbox review comments --- codex-rs/cli/src/lib.rs | 2 +- codex-rs/sandboxing/src/manager.rs | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/codex-rs/cli/src/lib.rs b/codex-rs/cli/src/lib.rs index e0ae8c8ae93b..cac34b3b6191 100644 --- a/codex-rs/cli/src/lib.rs +++ b/codex-rs/cli/src/lib.rs @@ -41,7 +41,7 @@ pub struct SeatbeltCommand { fn parse_allow_unix_socket_path(raw: &str) -> Result { AbsolutePathBuf::relative_to_current_dir(raw) - .map_err(|err| format!("invalid path {raw:?}: {err}")) + .map_err(|err| format!("invalid path {raw}: {err}")) } #[derive(Debug, Parser)] diff --git a/codex-rs/sandboxing/src/manager.rs b/codex-rs/sandboxing/src/manager.rs index 8eb1f6ad38a3..3b00ddb68340 100644 --- a/codex-rs/sandboxing/src/manager.rs +++ b/codex-rs/sandboxing/src/manager.rs @@ -9,12 +9,6 @@ use crate::policy_transforms::EffectiveSandboxPermissions; use crate::policy_transforms::effective_file_system_sandbox_policy; use crate::policy_transforms::effective_network_sandbox_policy; use crate::policy_transforms::should_require_platform_sandbox; -#[cfg(target_os = "macos")] -use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE; -#[cfg(target_os = "macos")] -use crate::seatbelt::SeatbeltCommandArgs; -#[cfg(target_os = "macos")] -use crate::seatbelt::create_seatbelt_command_args; use codex_network_proxy::NetworkProxy; use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::models::PermissionProfile; @@ -210,6 +204,10 @@ impl SandboxManager { SandboxType::None => (os_argv_to_strings(argv), None), #[cfg(target_os = "macos")] SandboxType::MacosSeatbelt => { + use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE; + use crate::seatbelt::SeatbeltCommandArgs; + use crate::seatbelt::create_seatbelt_command_args; + let mut args = create_seatbelt_command_args(SeatbeltCommandArgs { command: os_argv_to_strings(argv), file_system_sandbox_policy: &effective_file_system_policy, From 6d7eca3e6947f155280f6bf755d8729cfa35f610 Mon Sep 17 00:00:00 2001 From: Aaron Levine Date: Tue, 14 Apr 2026 23:29:09 -0700 Subject: [PATCH 4/5] Refine Unix socket access check --- codex-rs/sandboxing/src/seatbelt.rs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/codex-rs/sandboxing/src/seatbelt.rs b/codex-rs/sandboxing/src/seatbelt.rs index fff11d3808ce..6ce985653a49 100644 --- a/codex-rs/sandboxing/src/seatbelt.rs +++ b/codex-rs/sandboxing/src/seatbelt.rs @@ -257,17 +257,14 @@ fn dynamic_network_policy_for_network( enforce_managed_network: bool, proxy: &ProxyPolicyInputs, ) -> String { - let has_unix_socket_access = matches!( - proxy.unix_domain_socket_policy, - UnixDomainSocketPolicy::AllowAll - ) || matches!( - &proxy.unix_domain_socket_policy, - UnixDomainSocketPolicy::Restricted { allowed } if !allowed.is_empty() - ); + let has_some_unix_socket_access = match &proxy.unix_domain_socket_policy { + UnixDomainSocketPolicy::AllowAll => true, + UnixDomainSocketPolicy::Restricted { allowed } => !allowed.is_empty(), + }; let should_use_restricted_network_policy = !proxy.ports.is_empty() || proxy.has_proxy_config || enforce_managed_network - || (!network_policy.is_enabled() && has_unix_socket_access); + || (!network_policy.is_enabled() && has_some_unix_socket_access); if should_use_restricted_network_policy { let mut policy = String::new(); if proxy.allow_local_binding { From 8c160d4f012ef9e331179852a6ef952684cd3b27 Mon Sep 17 00:00:00 2001 From: Aaron Levine Date: Tue, 14 Apr 2026 23:46:54 -0700 Subject: [PATCH 5/5] Rename seatbelt command params --- codex-rs/cli/src/debug_sandbox.rs | 4 ++-- codex-rs/sandboxing/src/manager.rs | 4 ++-- codex-rs/sandboxing/src/seatbelt.rs | 8 ++++---- codex-rs/sandboxing/src/seatbelt_tests.rs | 12 ++++++------ 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/codex-rs/cli/src/debug_sandbox.rs b/codex-rs/cli/src/debug_sandbox.rs index a38df89ab0ff..a2cbf6187425 100644 --- a/codex-rs/cli/src/debug_sandbox.rs +++ b/codex-rs/cli/src/debug_sandbox.rs @@ -18,7 +18,7 @@ use codex_protocol::config_types::SandboxMode; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_sandboxing::landlock::create_linux_sandbox_command_args_for_policies; #[cfg(target_os = "macos")] -use codex_sandboxing::seatbelt::SeatbeltCommandArgs; +use codex_sandboxing::seatbelt::CreateSeatbeltCommandArgsParams; #[cfg(target_os = "macos")] use codex_sandboxing::seatbelt::create_seatbelt_command_args; use codex_utils_absolute_path::AbsolutePathBuf; @@ -261,7 +261,7 @@ async fn run_command_under_sandbox( let mut child = match sandbox_type { #[cfg(target_os = "macos")] SandboxType::Seatbelt => { - let args = create_seatbelt_command_args(SeatbeltCommandArgs { + let args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams { command, file_system_sandbox_policy: &config.permissions.file_system_sandbox_policy, network_sandbox_policy: config.permissions.network_sandbox_policy, diff --git a/codex-rs/sandboxing/src/manager.rs b/codex-rs/sandboxing/src/manager.rs index 3b00ddb68340..0836eb256517 100644 --- a/codex-rs/sandboxing/src/manager.rs +++ b/codex-rs/sandboxing/src/manager.rs @@ -204,11 +204,11 @@ impl SandboxManager { SandboxType::None => (os_argv_to_strings(argv), None), #[cfg(target_os = "macos")] SandboxType::MacosSeatbelt => { + use crate::seatbelt::CreateSeatbeltCommandArgsParams; use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE; - use crate::seatbelt::SeatbeltCommandArgs; use crate::seatbelt::create_seatbelt_command_args; - let mut args = create_seatbelt_command_args(SeatbeltCommandArgs { + let mut args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams { command: os_argv_to_strings(argv), file_system_sandbox_policy: &effective_file_system_policy, network_sandbox_policy: effective_network_policy, diff --git a/codex-rs/sandboxing/src/seatbelt.rs b/codex-rs/sandboxing/src/seatbelt.rs index 6ce985653a49..b12492210c28 100644 --- a/codex-rs/sandboxing/src/seatbelt.rs +++ b/codex-rs/sandboxing/src/seatbelt.rs @@ -389,7 +389,7 @@ fn create_seatbelt_command_args_for_legacy_policy( ) -> Vec { let file_system_sandbox_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy(sandbox_policy, sandbox_policy_cwd); - create_seatbelt_command_args(SeatbeltCommandArgs { + create_seatbelt_command_args(CreateSeatbeltCommandArgsParams { command, file_system_sandbox_policy: &file_system_sandbox_policy, network_sandbox_policy: NetworkSandboxPolicy::from(sandbox_policy), @@ -401,7 +401,7 @@ fn create_seatbelt_command_args_for_legacy_policy( } #[derive(Debug)] -pub struct SeatbeltCommandArgs<'a> { +pub struct CreateSeatbeltCommandArgsParams<'a> { pub command: Vec, pub file_system_sandbox_policy: &'a FileSystemSandboxPolicy, pub network_sandbox_policy: NetworkSandboxPolicy, @@ -411,8 +411,8 @@ pub struct SeatbeltCommandArgs<'a> { pub extra_allow_unix_sockets: &'a [AbsolutePathBuf], } -pub fn create_seatbelt_command_args(args: SeatbeltCommandArgs<'_>) -> Vec { - let SeatbeltCommandArgs { +pub fn create_seatbelt_command_args(args: CreateSeatbeltCommandArgsParams<'_>) -> Vec { + let CreateSeatbeltCommandArgsParams { command, file_system_sandbox_policy, network_sandbox_policy, diff --git a/codex-rs/sandboxing/src/seatbelt_tests.rs b/codex-rs/sandboxing/src/seatbelt_tests.rs index 1f92a1a12dca..f222dd15d1d5 100644 --- a/codex-rs/sandboxing/src/seatbelt_tests.rs +++ b/codex-rs/sandboxing/src/seatbelt_tests.rs @@ -1,7 +1,7 @@ +use super::CreateSeatbeltCommandArgsParams; use super::MACOS_PATH_TO_SEATBELT_EXECUTABLE; use super::MACOS_SEATBELT_BASE_POLICY; use super::ProxyPolicyInputs; -use super::SeatbeltCommandArgs; use super::UnixDomainSocketPolicy; use super::create_seatbelt_command_args; use super::create_seatbelt_command_args_for_legacy_policy; @@ -155,7 +155,7 @@ fn explicit_unreadable_paths_are_excluded_from_full_disk_read_and_write_access() }, ]); - let args = create_seatbelt_command_args(SeatbeltCommandArgs { + let args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams { command: vec!["/bin/true".to_string()], file_system_sandbox_policy: &file_system_policy, network_sandbox_policy: NetworkSandboxPolicy::Restricted, @@ -221,7 +221,7 @@ fn explicit_unreadable_paths_are_excluded_from_readable_roots() { }, ]); - let args = create_seatbelt_command_args(SeatbeltCommandArgs { + let args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams { command: vec!["/bin/true".to_string()], file_system_sandbox_policy: &file_system_policy, network_sandbox_policy: NetworkSandboxPolicy::Restricted, @@ -488,7 +488,7 @@ fn create_seatbelt_args_allowlists_explicit_unix_socket_paths_without_proxy() { cwd.path(), ); let extra_allow_unix_sockets = vec![absolute_path("/tmp/codex-browser-use")]; - let args = create_seatbelt_command_args(SeatbeltCommandArgs { + let args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams { command: vec!["/usr/bin/true".to_string()], file_system_sandbox_policy: &file_system_policy, network_sandbox_policy: NetworkSandboxPolicy::Restricted, @@ -546,7 +546,7 @@ async fn create_seatbelt_args_merges_proxy_and_explicit_unix_socket_paths() -> a .await?; let extra_allow_unix_sockets = vec![absolute_path(explicit_socket)]; - let args = create_seatbelt_command_args(SeatbeltCommandArgs { + let args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams { command: vec!["/usr/bin/true".to_string()], file_system_sandbox_policy: &file_system_policy, network_sandbox_policy: NetworkSandboxPolicy::Restricted, @@ -587,7 +587,7 @@ fn create_seatbelt_args_preserves_full_network_with_explicit_unix_socket_paths() cwd.path(), ); let extra_allow_unix_sockets = vec![absolute_path("/tmp/codex-browser-use")]; - let args = create_seatbelt_command_args(SeatbeltCommandArgs { + let args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams { command: vec!["/usr/bin/true".to_string()], file_system_sandbox_policy: &file_system_policy, network_sandbox_policy: NetworkSandboxPolicy::Enabled,