Skip to content
4 changes: 4 additions & 0 deletions codex-rs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions codex-rs/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
26 changes: 18 additions & 8 deletions codex-rs/cli/src/debug_sandbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand All @@ -50,6 +54,7 @@ pub async fn run_command_under_seatbelt(
codex_linux_sandbox_exe,
SandboxType::Seatbelt,
log_denials,
&allow_unix_sockets,
)
.await
}
Expand Down Expand Up @@ -78,6 +83,7 @@ pub async fn run_command_under_landlock(
codex_linux_sandbox_exe,
SandboxType::Landlock,
/*log_denials*/ false,
&[],
)
.await
}
Expand All @@ -98,6 +104,7 @@ pub async fn run_command_under_windows(
codex_linux_sandbox_exe,
SandboxType::Windows,
/*log_denials*/ false,
&[],
)
.await
}
Expand All @@ -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
Expand Down Expand Up @@ -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"),
Expand Down
10 changes: 10 additions & 0 deletions codex-rs/cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand All @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions codex-rs/sandboxing/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
19 changes: 10 additions & 9 deletions codex-rs/sandboxing/src/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
144 changes: 92 additions & 52 deletions codex-rs/sandboxing/src/seatbelt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Vec<_>>();

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::<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,
};
})
.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()
},
}

ProxyPolicyInputs::default()
}

fn normalize_path_for_sandbox(path: &Path) -> Option<AbsolutePathBuf> {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So was it an oversight that the result of unix_socket_policy() was not included previously?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, I think so. I think it mostly wasn’t an issue before because the unix socket policy was exercised through the proxy/restricted-network path.

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()
}
Expand Down Expand Up @@ -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) =
Expand Down Expand Up @@ -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);

Expand Down
Loading
Loading