Allow trusted symlinks beneath CODEX_HOME on macOS (#42716)

## What changed

- Add the macOS-only `allow_symlinked_codex_home` user setting. When enabled in the execution host's `$CODEX_HOME/config.toml`, writable roots at or beneath that home may traverse symlinks.
- Keep the exception disabled by default and prevent project config, command-line overrides, and ignored user config from enabling it. Other writable roots remain subject to symlink checks.
- Propagate the resolved setting through local, interactive, and exec-server sandbox paths, including `CODEX_HOME` aliases.
- Explain the opt-out in symlink rejection errors, including that it trusts targets outside `CODEX_HOME` and targets that change between commands.

## Testing

- Add macOS coverage for shell commands, patches, filesystem helpers, process execution, and interactive startup with enabled, disabled, aliased, and out-of-scope homes.

GitOrigin-RevId: 99fcdf611200c9e1b7713cf06f7fdea5bfa7f089
This commit is contained in:
jif
2026-09-04 10:12:03 +00:00
committed by copyberry
parent 8e6a44b428
commit d13aeb77ea
24 changed files with 499 additions and 36 deletions
+16 -12
View File
@@ -37,9 +37,7 @@ use codex_sandboxing::policy_transforms::effective_network_sandbox_policy;
use std::sync::Arc;
use std::time::Instant;
pub(crate) struct ToolOrchestrator {
sandbox: SandboxManager,
}
pub(crate) struct ToolOrchestrator;
pub(crate) struct OrchestratorRunResult<Out> {
pub output: Out,
@@ -48,9 +46,7 @@ pub(crate) struct OrchestratorRunResult<Out> {
impl ToolOrchestrator {
pub fn new() -> Self {
Self {
sandbox: SandboxManager::new(),
}
Self
}
async fn run_attempt<Rq, Out, T>(
@@ -145,6 +141,14 @@ impl ToolOrchestrator {
let mut already_approved = false;
let environment = tool.turn_environment(req);
let sandbox_manager = SandboxManager::new();
#[cfg(target_os = "macos")]
let sandbox_manager = sandbox_manager.with_allowed_symlinked_codex_home(
environment
.environment
.local_runtime_paths()
.and_then(|paths| paths.allowed_symlinked_codex_home.clone()),
);
let sandbox_config = environment.config();
let owner_network_policy = sandbox_config.network_policy.is_some();
if owner_network_policy
@@ -264,14 +268,14 @@ impl ToolOrchestrator {
let sandbox_preference = tool.sandbox_preference();
let sandbox_requested = match sandbox_override {
SandboxOverride::BypassSandboxFirstAttempt => false,
SandboxOverride::NoOverride => self.sandbox.should_sandbox(
SandboxOverride::NoOverride => sandbox_manager.should_sandbox(
&permissions,
sandbox_preference,
managed_network_active,
),
};
let initial_sandbox = if sandbox_requested && !executor_managed_process_sandbox {
self.sandbox.select_initial(
sandbox_manager.select_initial(
&permissions,
sandbox_preference,
sandbox_config.windows_sandbox_level,
@@ -291,7 +295,7 @@ impl ToolOrchestrator {
permissions: &permissions,
exec_server_permissions: permission_profile,
enforce_managed_network: managed_network_active,
manager: &self.sandbox,
manager: &sandbox_manager,
sandbox_cwd: &sandbox_policy_cwd,
workspace_roots,
codex_linux_sandbox_exe: turn_ctx.config.codex_linux_sandbox_exe.as_ref(),
@@ -438,14 +442,14 @@ impl ToolOrchestrator {
}
let retry_sandbox_requested = !unsandboxed_allowed
&& self.sandbox.should_sandbox(
&& sandbox_manager.should_sandbox(
&permissions,
sandbox_preference,
managed_network_active,
);
let retry_sandbox = if retry_sandbox_requested && !executor_managed_process_sandbox
{
self.sandbox.select_initial(
sandbox_manager.select_initial(
&permissions,
sandbox_preference,
sandbox_config.windows_sandbox_level,
@@ -465,7 +469,7 @@ impl ToolOrchestrator {
permissions: &permissions,
exec_server_permissions: permission_profile,
enforce_managed_network: managed_network_active,
manager: &self.sandbox,
manager: &sandbox_manager,
sandbox_cwd: &sandbox_policy_cwd,
workspace_roots,
codex_linux_sandbox_exe: retry_codex_linux_sandbox_exe,
@@ -84,7 +84,7 @@ fn approval_sandbox_permissions(
pub(crate) async fn prepare_unified_exec_zsh_fork(
req: &crate::tools::runtimes::unified_exec::UnifiedExecRequest,
_attempt: &SandboxAttempt<'_>,
attempt: &SandboxAttempt<'_>,
ctx: &ToolCtx,
exec_request: ExecRequest,
shell_zsh_path: &std::path::Path,
@@ -128,6 +128,7 @@ pub(crate) async fn prepare_unified_exec_zsh_fork(
.to_abs_path()
.map_err(|err| ToolError::Rejected(err.to_string()))?;
let command_executor = CoreShellCommandExecutor {
sandbox_manager: attempt.manager.clone(),
command: exec_request.command.clone(),
cwd,
permission_profile: exec_request.permission_profile.clone(),
@@ -565,6 +566,7 @@ fn commands_for_intercepted_exec_policy(
// TODO(anp): Capture these Windows and Landlock settings from
// TurnEnvironment::sandbox_context when preparing this executor, preserving its snapshot.
struct CoreShellCommandExecutor {
sandbox_manager: SandboxManager,
command: Vec<String>,
cwd: AbsolutePathBuf,
permission_profile: PermissionProfile,
@@ -757,7 +759,7 @@ impl CoreShellCommandExecutor {
let (program, args) = command
.split_first()
.ok_or_else(|| anyhow::anyhow!("prepared command must not be empty"))?;
let sandbox_manager = SandboxManager::new();
let sandbox_manager = &self.sandbox_manager;
let sandbox = sandbox_manager.select_initial(
permission_profile,
SandboxablePreference::Auto,
@@ -358,6 +358,7 @@ async fn unsandboxed_intercepted_exec_strips_managed_network_env() -> anyhow::Re
let workdir = test_sandbox_cwd();
let network = test_credential_broker_network_proxy().await?;
let executor = CoreShellCommandExecutor {
sandbox_manager: codex_sandboxing::SandboxManager::new(),
command: Vec::new(),
cwd: workdir.clone(),
permission_profile: PermissionProfile::workspace_write(),