mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Support Unix socket allowlists in macOS sandbox (#17654)
## Changes Allows sandboxes to restrict overall network access while granting access to specific unix sockets on mac. ## Details - `codex sandbox macos`: adds a repeatable `--allow-unix-socket` option. - `codex-sandboxing`: threads explicit Unix socket roots into the macOS Seatbelt profile generation. - Preserves restricted network behavior when only Unix socket IPC is requested, and preserves full network behavior when full network is already enabled. ## Verification - `cargo test -p codex-cli -p codex-sandboxing` - `cargo build -p codex-cli --bin codex` - verified that `codex sandbox macos --allow-unix-socket /tmp/test.sock -- test-client` grants access as expected
This commit is contained in:
committed by
GitHub
Unverified
parent
42528a905d
commit
2e1003728c
Generated
+4
@@ -1694,6 +1694,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",
|
||||
@@ -2694,6 +2695,8 @@ dependencies = [
|
||||
name = "codex-sandboxing"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"codex-network-proxy",
|
||||
"codex-protocol",
|
||||
"codex-utils-absolute-path",
|
||||
@@ -2702,6 +2705,7 @@ dependencies = [
|
||||
"pretty_assertions",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"url",
|
||||
"which 8.0.0",
|
||||
|
||||
@@ -45,6 +45,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 }
|
||||
|
||||
@@ -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::CreateSeatbeltCommandArgsParams;
|
||||
#[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<PathBuf>,
|
||||
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(CreateSeatbeltCommandArgsParams {
|
||||
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"),
|
||||
|
||||
@@ -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<AbsolutePathBuf>,
|
||||
|
||||
/// 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<String>,
|
||||
}
|
||||
|
||||
fn parse_allow_unix_socket_path(raw: &str) -> Result<AbsolutePathBuf, String> {
|
||||
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)
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -9,10 +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::create_seatbelt_command_args_for_policies;
|
||||
use codex_network_proxy::NetworkProxy;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
@@ -208,14 +204,19 @@ 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,
|
||||
use crate::seatbelt::CreateSeatbeltCommandArgsParams;
|
||||
use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE;
|
||||
use crate::seatbelt::create_seatbelt_command_args;
|
||||
|
||||
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,
|
||||
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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
},
|
||||
)
|
||||
.collect::<BTreeMap<_, _>>()
|
||||
.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,
|
||||
};
|
||||
}
|
||||
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::<Vec<_>>();
|
||||
|
||||
ProxyPolicyInputs::default()
|
||||
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::<Vec<_>>();
|
||||
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()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_path_for_sandbox(path: &Path) -> Option<AbsolutePathBuf> {
|
||||
@@ -244,8 +257,14 @@ 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_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_some_unix_socket_access);
|
||||
if should_use_restricted_network_policy {
|
||||
let mut policy = String::new();
|
||||
if proxy.allow_local_binding {
|
||||
@@ -285,9 +304,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 +380,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<String>,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
sandbox_policy_cwd: &Path,
|
||||
enforce_managed_network: bool,
|
||||
network: Option<&NetworkProxy>,
|
||||
) -> Vec<String> {
|
||||
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(CreateSeatbeltCommandArgsParams {
|
||||
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<String>,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
network_sandbox_policy: NetworkSandboxPolicy,
|
||||
sandbox_policy_cwd: &Path,
|
||||
enforce_managed_network: bool,
|
||||
network: Option<&NetworkProxy>,
|
||||
) -> Vec<String> {
|
||||
#[derive(Debug)]
|
||||
pub struct CreateSeatbeltCommandArgsParams<'a> {
|
||||
pub command: Vec<String>,
|
||||
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: CreateSeatbeltCommandArgsParams<'_>) -> Vec<String> {
|
||||
let CreateSeatbeltCommandArgsParams {
|
||||
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 +505,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);
|
||||
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
use super::CreateSeatbeltCommandArgsParams;
|
||||
use super::MACOS_PATH_TO_SEATBELT_EXECUTABLE;
|
||||
use super::MACOS_SEATBELT_BASE_POLICY;
|
||||
use super::ProxyPolicyInputs;
|
||||
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<Option<ConfigState>> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
async fn reload_now(&self) -> anyhow::Result<ConfigState> {
|
||||
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(CreateSeatbeltCommandArgsParams {
|
||||
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(CreateSeatbeltCommandArgsParams {
|
||||
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(CreateSeatbeltCommandArgsParams {
|
||||
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(/*managed_by_codex*/ false)
|
||||
.build()
|
||||
.await?;
|
||||
let extra_allow_unix_sockets = vec![absolute_path(explicit_socket)];
|
||||
|
||||
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,
|
||||
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::<Vec<_>>();
|
||||
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(CreateSeatbeltCommandArgsParams {
|
||||
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(),
|
||||
|
||||
Reference in New Issue
Block a user