From 9651dfdbae7258ac88a3652f16fba33b3ee815d4 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Mon, 9 Mar 2026 21:29:26 -0700 Subject: [PATCH 01/17] fix: fail closed for unsupported split windows sandboxing --- codex-rs/core/README.md | 15 ++++-- codex-rs/core/src/exec.rs | 68 ++++++++++++++++++------ codex-rs/core/src/sandboxing/mod.rs | 2 + codex-rs/core/src/tasks/user_shell.rs | 1 + codex-rs/windows-sandbox-rs/src/allow.rs | 31 +++++++++++ 5 files changed, 97 insertions(+), 20 deletions(-) diff --git a/codex-rs/core/README.md b/codex-rs/core/README.md index 77966ea88c6..a8b94fad75d 100644 --- a/codex-rs/core/README.md +++ b/codex-rs/core/README.md @@ -14,8 +14,10 @@ When using the workspace-write sandbox policy, the Seatbelt profile allows writes under the configured writable roots while keeping `.git` (directory or pointer file), the resolved `gitdir:` target, and `.codex` read-only. -Network access and filesystem read/write roots are controlled by -`SandboxPolicy`. Seatbelt consumes the resolved policy and enforces it. +Network access and filesystem read/write roots are controlled by the split +`FileSystemSandboxPolicy` and `NetworkSandboxPolicy` views. On macOS, Seatbelt +enforces overlapping path rules with the most specific entry winning, so a +broader writable root can still contain nested read-only or denied carveouts. Seatbelt also supports macOS permission-profile extensions layered on top of `SandboxPolicy`: @@ -51,7 +53,6 @@ Expects the binary containing `codex-core` to run the equivalent of `codex sandb Legacy `SandboxPolicy` / `sandbox_mode` configs are still supported on Linux. They can continue to use the legacy Landlock path when the split filesystem policy is sandbox-equivalent to the legacy model after `cwd` resolution. - Split filesystem policies that need direct `FileSystemSandboxPolicy` enforcement, such as read-only or denied carveouts under a broader writable root, automatically route through bubblewrap. The legacy Landlock path is used @@ -65,6 +66,14 @@ falls back to the vendored bubblewrap path otherwise. When `/usr/bin/bwrap` is missing, Codex also surfaces a startup warning through its normal notification path instead of printing directly from the sandbox helper. +### Windows + +The restricted-token sandbox currently enforces only the subset of filesystem +and network restrictions that round-trip through the legacy `SandboxPolicy` +model. Split-only filesystem carveouts that require direct +`FileSystemSandboxPolicy` enforcement are rejected instead of running with a +weaker sandbox. + ### All Platforms Expects the binary containing `codex-core` to simulate the virtual `apply_patch` CLI when `arg1` is `--codex-run-as-apply-patch`. See the `codex-arg0` crate for details. diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index 4f462821e5c..3c587342d4c 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -288,6 +288,7 @@ pub(crate) async fn execute_exec_request( let ExecRequest { command, cwd, + sandbox_policy_cwd, env, network, expiration, @@ -319,10 +320,13 @@ pub(crate) async fn execute_exec_request( let start = Instant::now(); let raw_output_result = exec( params, - sandbox, - sandbox_policy, - &file_system_sandbox_policy, - network_sandbox_policy, + ExecSandboxContext { + sandbox, + sandbox_policy, + file_system_sandbox_policy: &file_system_sandbox_policy, + network_sandbox_policy, + sandbox_policy_cwd: sandbox_policy_cwd.as_path(), + }, stdout_stream, after_spawn, ) @@ -401,6 +405,7 @@ fn record_windows_sandbox_spawn_failure( async fn exec_windows_sandbox( params: ExecParams, sandbox_policy: &SandboxPolicy, + sandbox_policy_cwd: &Path, ) -> Result { use crate::config::find_codex_home; use codex_protocol::config_types::WindowsSandboxLevel; @@ -430,7 +435,7 @@ async fn exec_windows_sandbox( "failed to serialize Windows sandbox policy: {err}" ))) })?; - let sandbox_cwd = cwd.clone(); + let sandbox_cwd = sandbox_policy_cwd.to_path_buf(); let codex_home = find_codex_home().map_err(|err| { CodexErr::Io(io::Error::other(format!( "windows sandbox: failed to resolve codex_home: {err}" @@ -753,16 +758,30 @@ impl Default for ExecToolCallOutput { } } +#[derive(Clone, Copy)] +struct ExecSandboxContext<'a> { + sandbox: SandboxType, + sandbox_policy: &'a SandboxPolicy, + file_system_sandbox_policy: &'a FileSystemSandboxPolicy, + network_sandbox_policy: NetworkSandboxPolicy, + sandbox_policy_cwd: &'a Path, +} + #[cfg_attr(not(target_os = "windows"), allow(unused_variables))] async fn exec( params: ExecParams, - sandbox: SandboxType, - sandbox_policy: &SandboxPolicy, - file_system_sandbox_policy: &FileSystemSandboxPolicy, - network_sandbox_policy: NetworkSandboxPolicy, + sandbox_context: ExecSandboxContext<'_>, stdout_stream: Option, after_spawn: Option>, ) -> Result { + let ExecSandboxContext { + sandbox, + sandbox_policy, + file_system_sandbox_policy, + network_sandbox_policy, + sandbox_policy_cwd, + } = sandbox_context; + #[cfg(target_os = "windows")] if sandbox == SandboxType::WindowsRestrictedToken { if let Some(reason) = unsupported_windows_restricted_token_sandbox_reason( @@ -770,10 +789,11 @@ async fn exec( sandbox_policy, file_system_sandbox_policy, network_sandbox_policy, + sandbox_policy_cwd, ) { return Err(CodexErr::Io(io::Error::other(reason))); } - return exec_windows_sandbox(params, sandbox_policy).await; + return exec_windows_sandbox(params, sandbox_policy, sandbox_policy_cwd).await; } let ExecParams { command, @@ -836,21 +856,35 @@ fn unsupported_windows_restricted_token_sandbox_reason( sandbox_policy: &SandboxPolicy, file_system_sandbox_policy: &FileSystemSandboxPolicy, network_sandbox_policy: NetworkSandboxPolicy, + sandbox_policy_cwd: &Path, ) -> Option { if should_use_windows_restricted_token_sandbox( sandbox, sandbox_policy, file_system_sandbox_policy, - ) { + ) && !file_system_sandbox_policy + .needs_direct_runtime_enforcement(network_sandbox_policy, sandbox_policy_cwd) + { return None; } - (sandbox == SandboxType::WindowsRestrictedToken).then(|| { - format!( - "windows sandbox backend cannot enforce file_system={:?}, network={network_sandbox_policy:?}, legacy_policy={sandbox_policy:?}; refusing to run unsandboxed", - file_system_sandbox_policy.kind, - ) - }) + if sandbox != SandboxType::WindowsRestrictedToken { + return None; + } + + if file_system_sandbox_policy + .needs_direct_runtime_enforcement(network_sandbox_policy, sandbox_policy_cwd) + { + return Some( + "windows sandbox backend cannot enforce split filesystem permissions directly; refusing to run unsandboxed" + .to_string(), + ); + } + + Some(format!( + "windows sandbox backend cannot enforce file_system={:?}, network={network_sandbox_policy:?}, legacy_policy={sandbox_policy:?}; refusing to run unsandboxed", + file_system_sandbox_policy.kind, + )) } /// Consumes the output of a child process, truncating it so it is suitable for /// use as the output of a `shell` tool call. Also enforces specified timeout. diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs index db88788814e..f03702217c5 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -64,6 +64,7 @@ pub struct CommandSpec { pub struct ExecRequest { pub command: Vec, pub cwd: PathBuf, + pub sandbox_policy_cwd: PathBuf, pub env: HashMap, pub network: Option, pub expiration: ExecExpiration, @@ -704,6 +705,7 @@ impl SandboxManager { Ok(ExecRequest { command, cwd: spec.cwd, + sandbox_policy_cwd: sandbox_policy_cwd.to_path_buf(), env, network: network.cloned(), expiration: spec.expiration, diff --git a/codex-rs/core/src/tasks/user_shell.rs b/codex-rs/core/src/tasks/user_shell.rs index 83368efc950..51806ec3eec 100644 --- a/codex-rs/core/src/tasks/user_shell.rs +++ b/codex-rs/core/src/tasks/user_shell.rs @@ -157,6 +157,7 @@ pub(crate) async fn execute_user_shell_command( let exec_env = ExecRequest { command: exec_command.clone(), cwd: cwd.clone(), + sandbox_policy_cwd: cwd.clone(), env: create_env( &turn_context.shell_environment_policy, Some(session.conversation_id), diff --git a/codex-rs/windows-sandbox-rs/src/allow.rs b/codex-rs/windows-sandbox-rs/src/allow.rs index b40532cda85..1f8b2253694 100644 --- a/codex-rs/windows-sandbox-rs/src/allow.rs +++ b/codex-rs/windows-sandbox-rs/src/allow.rs @@ -127,6 +127,37 @@ mod tests { assert!(paths.deny.is_empty(), "no deny paths expected"); } + #[test] + fn resolves_relative_writable_roots_against_policy_cwd() { + let tmp = TempDir::new().expect("tempdir"); + let policy_cwd = tmp.path().join("policy"); + let command_cwd = tmp.path().join("command"); + let relative_root = PathBuf::from("shared"); + let shared_from_policy = policy_cwd.join(&relative_root); + let shared_from_command = command_cwd.join(&relative_root); + let _ = fs::create_dir_all(&policy_cwd); + let _ = fs::create_dir_all(&command_cwd); + let _ = fs::create_dir_all(&shared_from_policy); + let _ = fs::create_dir_all(&shared_from_command); + + let policy = SandboxPolicy::WorkspaceWrite { + writable_roots: vec![AbsolutePathBuf::try_from(relative_root.as_path()).unwrap()], + read_only_access: Default::default(), + network_access: false, + exclude_tmpdir_env_var: true, + exclude_slash_tmp: false, + }; + + let paths = compute_allow_paths(&policy, &policy_cwd, &command_cwd, &HashMap::new()); + + assert!(paths + .allow + .contains(&dunce::canonicalize(&shared_from_policy).unwrap())); + assert!(!paths + .allow + .contains(&dunce::canonicalize(&shared_from_command).unwrap())); + } + #[test] fn excludes_tmp_env_vars_when_requested() { let tmp = TempDir::new().expect("tempdir"); From d5813ff38eae5f1b49242bef448a437a7e27cfcb Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Thu, 12 Mar 2026 10:21:35 -0700 Subject: [PATCH 02/17] fix: thread sandbox policy cwd through unix escalation --- codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs index 89bf4d424c3..e52c87bb352 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs @@ -121,6 +121,7 @@ pub(super) async fn try_run_zsh_fork( let crate::sandboxing::ExecRequest { command, cwd: sandbox_cwd, + sandbox_policy_cwd, env: sandbox_env, network: sandbox_network, expiration: _sandbox_expiration, @@ -155,7 +156,7 @@ pub(super) async fn try_run_zsh_fork( sandbox_permissions, justification, arg0, - sandbox_policy_cwd: ctx.turn.cwd.clone(), + sandbox_policy_cwd, macos_seatbelt_profile_extensions: ctx .turn .config @@ -261,7 +262,7 @@ pub(crate) async fn prepare_unified_exec_zsh_fork( sandbox_permissions: exec_request.sandbox_permissions, justification: exec_request.justification.clone(), arg0: exec_request.arg0.clone(), - sandbox_policy_cwd: ctx.turn.cwd.clone(), + sandbox_policy_cwd: exec_request.sandbox_policy_cwd.clone(), macos_seatbelt_profile_extensions: ctx .turn .config @@ -900,6 +901,7 @@ impl ShellCommandExecutor for CoreShellCommandExecutor { crate::sandboxing::ExecRequest { command: self.command.clone(), cwd: self.cwd.clone(), + sandbox_policy_cwd: self.sandbox_policy_cwd.clone(), env: exec_env, network: self.network.clone(), expiration: ExecExpiration::Cancellation(cancel_rx), From baef62751021b806dc2af4fe8390b77f5293bb43 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Thu, 12 Mar 2026 10:45:34 -0700 Subject: [PATCH 03/17] fix: set sandbox policy cwd in app-server exec fixtures --- codex-rs/app-server/src/command_exec.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/codex-rs/app-server/src/command_exec.rs b/codex-rs/app-server/src/command_exec.rs index f761b18c962..0840527c244 100644 --- a/codex-rs/app-server/src/command_exec.rs +++ b/codex-rs/app-server/src/command_exec.rs @@ -730,6 +730,7 @@ mod tests { ExecRequest { command: vec!["cmd".to_string()], cwd: PathBuf::from("."), + sandbox_policy_cwd: PathBuf::from("."), env: HashMap::new(), network: None, expiration: ExecExpiration::DefaultTimeout, @@ -842,6 +843,7 @@ mod tests { exec_request: ExecRequest { command: vec!["sh".to_string(), "-lc".to_string(), "sleep 30".to_string()], cwd: PathBuf::from("."), + sandbox_policy_cwd: PathBuf::from("."), env: HashMap::new(), network: None, expiration: ExecExpiration::Cancellation(CancellationToken::new()), From cb69855b74005fea8f836700d4ecd548850fe18b Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Thu, 12 Mar 2026 12:20:37 -0700 Subject: [PATCH 04/17] test: restore windows sandbox exec regressions --- codex-rs/core/src/exec_tests.rs | 111 +++++++++++++++++++++++++++++--- 1 file changed, 103 insertions(+), 8 deletions(-) diff --git a/codex-rs/core/src/exec_tests.rs b/codex-rs/core/src/exec_tests.rs index 10ba5734faf..9f7527a59e7 100644 --- a/codex-rs/core/src/exec_tests.rs +++ b/codex-rs/core/src/exec_tests.rs @@ -185,7 +185,7 @@ fn windows_restricted_token_skips_external_sandbox_policies() { let policy = SandboxPolicy::ExternalSandbox { network_access: codex_protocol::protocol::NetworkAccess::Restricted, }; - let file_system_policy = FileSystemSandboxPolicy::restricted(vec![]); + let file_system_policy = FileSystemSandboxPolicy::from(&policy); assert_eq!( should_use_windows_restricted_token_sandbox( @@ -200,7 +200,7 @@ fn windows_restricted_token_skips_external_sandbox_policies() { #[test] fn windows_restricted_token_runs_for_legacy_restricted_policies() { let policy = SandboxPolicy::new_read_only_policy(); - let file_system_policy = FileSystemSandboxPolicy::restricted(vec![]); + let file_system_policy = FileSystemSandboxPolicy::from(&policy); assert_eq!( should_use_windows_restricted_token_sandbox( @@ -225,6 +225,7 @@ fn windows_restricted_token_rejects_network_only_restrictions() { &policy, &file_system_policy, NetworkSandboxPolicy::Restricted, + Path::new("/tmp"), ), Some( "windows sandbox backend cannot enforce file_system=Unrestricted, network=Restricted, legacy_policy=ExternalSandbox { network_access: Restricted }; refusing to run unsandboxed".to_string() @@ -235,7 +236,7 @@ fn windows_restricted_token_rejects_network_only_restrictions() { #[test] fn windows_restricted_token_allows_legacy_restricted_policies() { let policy = SandboxPolicy::new_read_only_policy(); - let file_system_policy = FileSystemSandboxPolicy::restricted(vec![]); + let file_system_policy = FileSystemSandboxPolicy::from(&policy); assert_eq!( unsupported_windows_restricted_token_sandbox_reason( @@ -243,6 +244,7 @@ fn windows_restricted_token_allows_legacy_restricted_policies() { &policy, &file_system_policy, NetworkSandboxPolicy::Restricted, + Path::new("/tmp"), ), None ); @@ -265,11 +267,98 @@ fn windows_restricted_token_allows_legacy_workspace_write_policies() { &policy, &file_system_policy, NetworkSandboxPolicy::Restricted, + Path::new("/tmp"), ), None ); } +#[test] +fn windows_restricted_token_rejects_split_only_filesystem_policies() { + let temp_dir = tempfile::TempDir::new().expect("tempdir"); + let docs = temp_dir.path().join("docs"); + std::fs::create_dir_all(&docs).expect("create docs"); + let policy = SandboxPolicy::WorkspaceWrite { + writable_roots: vec![], + read_only_access: codex_protocol::protocol::ReadOnlyAccess::FullAccess, + network_access: false, + exclude_tmpdir_env_var: false, + exclude_slash_tmp: false, + }; + let file_system_policy = FileSystemSandboxPolicy::restricted(vec![ + codex_protocol::permissions::FileSystemSandboxEntry { + path: codex_protocol::permissions::FileSystemPath::Special { + value: codex_protocol::permissions::FileSystemSpecialPath::CurrentWorkingDirectory, + }, + access: codex_protocol::permissions::FileSystemAccessMode::Write, + }, + codex_protocol::permissions::FileSystemSandboxEntry { + path: codex_protocol::permissions::FileSystemPath::Path { + path: codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(&docs) + .expect("absolute docs"), + }, + access: codex_protocol::permissions::FileSystemAccessMode::Read, + }, + ]); + + assert_eq!( + unsupported_windows_restricted_token_sandbox_reason( + SandboxType::WindowsRestrictedToken, + &policy, + &file_system_policy, + NetworkSandboxPolicy::Restricted, + temp_dir.path(), + ), + Some( + "windows sandbox backend cannot enforce split filesystem permissions directly; refusing to run unsandboxed" + .to_string() + ) + ); +} + +#[test] +fn windows_restricted_token_rejects_root_write_read_only_carveouts() { + let temp_dir = tempfile::TempDir::new().expect("tempdir"); + let docs = temp_dir.path().join("docs"); + std::fs::create_dir_all(&docs).expect("create docs"); + let policy = SandboxPolicy::WorkspaceWrite { + writable_roots: vec![], + read_only_access: codex_protocol::protocol::ReadOnlyAccess::FullAccess, + network_access: false, + exclude_tmpdir_env_var: false, + exclude_slash_tmp: false, + }; + let file_system_policy = FileSystemSandboxPolicy::restricted(vec![ + codex_protocol::permissions::FileSystemSandboxEntry { + path: codex_protocol::permissions::FileSystemPath::Special { + value: codex_protocol::permissions::FileSystemSpecialPath::Root, + }, + access: codex_protocol::permissions::FileSystemAccessMode::Write, + }, + codex_protocol::permissions::FileSystemSandboxEntry { + path: codex_protocol::permissions::FileSystemPath::Path { + path: codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(&docs) + .expect("absolute docs"), + }, + access: codex_protocol::permissions::FileSystemAccessMode::Read, + }, + ]); + + assert_eq!( + unsupported_windows_restricted_token_sandbox_reason( + SandboxType::WindowsRestrictedToken, + &policy, + &file_system_policy, + NetworkSandboxPolicy::Restricted, + temp_dir.path(), + ), + Some( + "windows sandbox backend cannot enforce split filesystem permissions directly; refusing to run unsandboxed" + .to_string() + ) + ); +} + #[test] fn process_exec_tool_call_uses_platform_sandbox_for_network_only_restrictions() { let expected = crate::get_platform_sandbox(false).unwrap_or(SandboxType::None); @@ -310,10 +399,11 @@ async fn kill_child_process_group_kills_grandchildren_on_timeout() -> Result<()> "-c".to_string(), "sleep 60 & echo $!; sleep 60".to_string(), ]; + let cwd = std::env::current_dir()?; let env: HashMap = std::env::vars().collect(); let params = ExecParams { command, - cwd: std::env::current_dir()?, + cwd: cwd.clone(), expiration: 500.into(), env, network: None, @@ -326,10 +416,15 @@ async fn kill_child_process_group_kills_grandchildren_on_timeout() -> Result<()> let output = exec( params, - SandboxType::None, - &SandboxPolicy::new_read_only_policy(), - &FileSystemSandboxPolicy::from(&SandboxPolicy::new_read_only_policy()), - NetworkSandboxPolicy::Restricted, + ExecSandboxContext { + sandbox: SandboxType::None, + sandbox_policy: &SandboxPolicy::new_read_only_policy(), + file_system_sandbox_policy: &FileSystemSandboxPolicy::from( + &SandboxPolicy::new_read_only_policy(), + ), + network_sandbox_policy: NetworkSandboxPolicy::Restricted, + sandbox_policy_cwd: cwd.as_path(), + }, None, None, ) From e902c4bc28ab2f559080de1950de2ddcd4104d4c Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Thu, 12 Mar 2026 14:41:03 -0700 Subject: [PATCH 05/17] fix: fail closed split windows sandboxing during transform --- codex-rs/app-server/src/command_exec.rs | 2 - codex-rs/core/README.md | 5 ++ codex-rs/core/src/exec.rs | 54 +++++------------- codex-rs/core/src/exec_tests.rs | 13 ++--- codex-rs/core/src/sandboxing/mod.rs | 16 +++++- codex-rs/core/src/sandboxing/mod_tests.rs | 56 +++++++++++++++++++ codex-rs/core/src/tasks/user_shell.rs | 1 - .../tools/runtimes/shell/unix_escalation.rs | 6 +- codex-rs/windows-sandbox-rs/src/allow.rs | 31 ---------- 9 files changed, 95 insertions(+), 89 deletions(-) diff --git a/codex-rs/app-server/src/command_exec.rs b/codex-rs/app-server/src/command_exec.rs index 0840527c244..f761b18c962 100644 --- a/codex-rs/app-server/src/command_exec.rs +++ b/codex-rs/app-server/src/command_exec.rs @@ -730,7 +730,6 @@ mod tests { ExecRequest { command: vec!["cmd".to_string()], cwd: PathBuf::from("."), - sandbox_policy_cwd: PathBuf::from("."), env: HashMap::new(), network: None, expiration: ExecExpiration::DefaultTimeout, @@ -843,7 +842,6 @@ mod tests { exec_request: ExecRequest { command: vec!["sh".to_string(), "-lc".to_string(), "sleep 30".to_string()], cwd: PathBuf::from("."), - sandbox_policy_cwd: PathBuf::from("."), env: HashMap::new(), network: None, expiration: ExecExpiration::Cancellation(CancellationToken::new()), diff --git a/codex-rs/core/README.md b/codex-rs/core/README.md index a8b94fad75d..7bca5a2a189 100644 --- a/codex-rs/core/README.md +++ b/codex-rs/core/README.md @@ -14,10 +14,15 @@ When using the workspace-write sandbox policy, the Seatbelt profile allows writes under the configured writable roots while keeping `.git` (directory or pointer file), the resolved `gitdir:` target, and `.codex` read-only. +<<<<<<< HEAD Network access and filesystem read/write roots are controlled by the split `FileSystemSandboxPolicy` and `NetworkSandboxPolicy` views. On macOS, Seatbelt enforces overlapping path rules with the most specific entry winning, so a broader writable root can still contain nested read-only or denied carveouts. +======= +Network access and filesystem read/write roots are controlled by +`SandboxPolicy`. Seatbelt consumes the resolved policy and enforces it. +>>>>>>> 28e7c4637 (fix: fail closed split windows sandboxing during transform) Seatbelt also supports macOS permission-profile extensions layered on top of `SandboxPolicy`: diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index 3c587342d4c..0c30dc8b521 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -288,7 +288,6 @@ pub(crate) async fn execute_exec_request( let ExecRequest { command, cwd, - sandbox_policy_cwd, env, network, expiration, @@ -320,13 +319,10 @@ pub(crate) async fn execute_exec_request( let start = Instant::now(); let raw_output_result = exec( params, - ExecSandboxContext { - sandbox, - sandbox_policy, - file_system_sandbox_policy: &file_system_sandbox_policy, - network_sandbox_policy, - sandbox_policy_cwd: sandbox_policy_cwd.as_path(), - }, + sandbox, + sandbox_policy, + &file_system_sandbox_policy, + network_sandbox_policy, stdout_stream, after_spawn, ) @@ -405,7 +401,6 @@ fn record_windows_sandbox_spawn_failure( async fn exec_windows_sandbox( params: ExecParams, sandbox_policy: &SandboxPolicy, - sandbox_policy_cwd: &Path, ) -> Result { use crate::config::find_codex_home; use codex_protocol::config_types::WindowsSandboxLevel; @@ -435,7 +430,7 @@ async fn exec_windows_sandbox( "failed to serialize Windows sandbox policy: {err}" ))) })?; - let sandbox_cwd = sandbox_policy_cwd.to_path_buf(); + let sandbox_cwd = cwd.clone(); let codex_home = find_codex_home().map_err(|err| { CodexErr::Io(io::Error::other(format!( "windows sandbox: failed to resolve codex_home: {err}" @@ -588,6 +583,9 @@ pub(crate) mod errors { SandboxTransformError::MissingLinuxSandboxExecutable => { CodexErr::LandlockSandboxExecutableNotProvided } + SandboxTransformError::UnsupportedWindowsRestrictedToken(reason) => { + CodexErr::UnsupportedOperation(reason) + } #[cfg(not(target_os = "macos"))] SandboxTransformError::SeatbeltUnavailable => CodexErr::UnsupportedOperation( "seatbelt sandbox is only available on macOS".to_string(), @@ -758,42 +756,19 @@ impl Default for ExecToolCallOutput { } } -#[derive(Clone, Copy)] -struct ExecSandboxContext<'a> { - sandbox: SandboxType, - sandbox_policy: &'a SandboxPolicy, - file_system_sandbox_policy: &'a FileSystemSandboxPolicy, - network_sandbox_policy: NetworkSandboxPolicy, - sandbox_policy_cwd: &'a Path, -} - #[cfg_attr(not(target_os = "windows"), allow(unused_variables))] async fn exec( params: ExecParams, - sandbox_context: ExecSandboxContext<'_>, + sandbox: SandboxType, + sandbox_policy: &SandboxPolicy, + file_system_sandbox_policy: &FileSystemSandboxPolicy, + network_sandbox_policy: NetworkSandboxPolicy, stdout_stream: Option, after_spawn: Option>, ) -> Result { - let ExecSandboxContext { - sandbox, - sandbox_policy, - file_system_sandbox_policy, - network_sandbox_policy, - sandbox_policy_cwd, - } = sandbox_context; - #[cfg(target_os = "windows")] if sandbox == SandboxType::WindowsRestrictedToken { - if let Some(reason) = unsupported_windows_restricted_token_sandbox_reason( - sandbox, - sandbox_policy, - file_system_sandbox_policy, - network_sandbox_policy, - sandbox_policy_cwd, - ) { - return Err(CodexErr::Io(io::Error::other(reason))); - } - return exec_windows_sandbox(params, sandbox_policy, sandbox_policy_cwd).await; + return exec_windows_sandbox(params, sandbox_policy).await; } let ExecParams { command, @@ -850,8 +825,7 @@ fn should_use_windows_restricted_token_sandbox( ) } -#[cfg(any(target_os = "windows", test))] -fn unsupported_windows_restricted_token_sandbox_reason( +pub(crate) fn unsupported_windows_restricted_token_sandbox_reason( sandbox: SandboxType, sandbox_policy: &SandboxPolicy, file_system_sandbox_policy: &FileSystemSandboxPolicy, diff --git a/codex-rs/core/src/exec_tests.rs b/codex-rs/core/src/exec_tests.rs index 9f7527a59e7..61d922a56e2 100644 --- a/codex-rs/core/src/exec_tests.rs +++ b/codex-rs/core/src/exec_tests.rs @@ -416,15 +416,10 @@ async fn kill_child_process_group_kills_grandchildren_on_timeout() -> Result<()> let output = exec( params, - ExecSandboxContext { - sandbox: SandboxType::None, - sandbox_policy: &SandboxPolicy::new_read_only_policy(), - file_system_sandbox_policy: &FileSystemSandboxPolicy::from( - &SandboxPolicy::new_read_only_policy(), - ), - network_sandbox_policy: NetworkSandboxPolicy::Restricted, - sandbox_policy_cwd: cwd.as_path(), - }, + SandboxType::None, + &SandboxPolicy::new_read_only_policy(), + &FileSystemSandboxPolicy::from(&SandboxPolicy::new_read_only_policy()), + NetworkSandboxPolicy::Restricted, None, None, ) diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs index f03702217c5..ef35da038f1 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -13,6 +13,7 @@ use crate::exec::ExecToolCallOutput; use crate::exec::SandboxType; use crate::exec::StdoutStream; use crate::exec::execute_exec_request; +use crate::exec::unsupported_windows_restricted_token_sandbox_reason; use crate::landlock::allow_network_for_proxy; use crate::landlock::create_linux_sandbox_command_args_for_policies; use crate::protocol::SandboxPolicy; @@ -64,7 +65,6 @@ pub struct CommandSpec { pub struct ExecRequest { pub command: Vec, pub cwd: PathBuf, - pub sandbox_policy_cwd: PathBuf, pub env: HashMap, pub network: Option, pub expiration: ExecExpiration, @@ -111,6 +111,8 @@ pub enum SandboxPreference { pub(crate) enum SandboxTransformError { #[error("missing codex-linux-sandbox executable path")] MissingLinuxSandboxExecutable, + #[error("{0}")] + UnsupportedWindowsRestrictedToken(String), #[cfg(not(target_os = "macos"))] #[error("seatbelt sandbox is only available on macOS")] SeatbeltUnavailable, @@ -633,6 +635,17 @@ impl SandboxManager { } else { (file_system_policy.clone(), network_policy) }; + if let Some(reason) = unsupported_windows_restricted_token_sandbox_reason( + sandbox, + &effective_policy, + &effective_file_system_policy, + effective_network_policy, + sandbox_policy_cwd, + ) { + return Err(SandboxTransformError::UnsupportedWindowsRestrictedToken( + reason, + )); + } let mut env = spec.env; if !effective_network_policy.is_enabled() { env.insert( @@ -705,7 +718,6 @@ impl SandboxManager { Ok(ExecRequest { command, cwd: spec.cwd, - sandbox_policy_cwd: sandbox_policy_cwd.to_path_buf(), env, network: network.cloned(), expiration: spec.expiration, diff --git a/codex-rs/core/src/sandboxing/mod_tests.rs b/codex-rs/core/src/sandboxing/mod_tests.rs index 4d45dfb0080..73dd4f2265e 100644 --- a/codex-rs/core/src/sandboxing/mod_tests.rs +++ b/codex-rs/core/src/sandboxing/mod_tests.rs @@ -190,6 +190,62 @@ fn transform_preserves_unrestricted_file_system_policy_for_restricted_network() ); } +#[test] +fn transform_rejects_unsupported_windows_split_only_filesystem_policies() { + let manager = SandboxManager::new(); + let temp_dir = TempDir::new().expect("create temp dir"); + let docs = AbsolutePathBuf::from_absolute_path(temp_dir.path().join("docs")) + .expect("absolute docs path"); + let err = manager + .transform(super::SandboxTransformRequest { + spec: super::CommandSpec { + program: "true".to_string(), + args: Vec::new(), + cwd: temp_dir.path().to_path_buf(), + env: HashMap::new(), + expiration: crate::exec::ExecExpiration::DefaultTimeout, + sandbox_permissions: super::SandboxPermissions::UseDefault, + additional_permissions: None, + justification: None, + }, + policy: &SandboxPolicy::WorkspaceWrite { + writable_roots: vec![], + read_only_access: ReadOnlyAccess::FullAccess, + network_access: false, + exclude_tmpdir_env_var: false, + exclude_slash_tmp: false, + }, + file_system_policy: &FileSystemSandboxPolicy::restricted(vec![ + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::CurrentWorkingDirectory, + }, + access: FileSystemAccessMode::Write, + }, + FileSystemSandboxEntry { + path: FileSystemPath::Path { path: docs }, + access: FileSystemAccessMode::Read, + }, + ]), + network_policy: NetworkSandboxPolicy::Restricted, + sandbox: SandboxType::WindowsRestrictedToken, + enforce_managed_network: false, + network: None, + sandbox_policy_cwd: temp_dir.path(), + #[cfg(target_os = "macos")] + macos_seatbelt_profile_extensions: None, + codex_linux_sandbox_exe: None, + use_legacy_landlock: false, + windows_sandbox_level: WindowsSandboxLevel::RestrictedToken, + }) + .expect_err("unsupported split-only windows policy should fail closed"); + + assert!(matches!( + err, + super::SandboxTransformError::UnsupportedWindowsRestrictedToken(_) + )); +} + #[test] fn normalize_additional_permissions_preserves_network() { let temp_dir = TempDir::new().expect("create temp dir"); diff --git a/codex-rs/core/src/tasks/user_shell.rs b/codex-rs/core/src/tasks/user_shell.rs index 51806ec3eec..83368efc950 100644 --- a/codex-rs/core/src/tasks/user_shell.rs +++ b/codex-rs/core/src/tasks/user_shell.rs @@ -157,7 +157,6 @@ pub(crate) async fn execute_user_shell_command( let exec_env = ExecRequest { command: exec_command.clone(), cwd: cwd.clone(), - sandbox_policy_cwd: cwd.clone(), env: create_env( &turn_context.shell_environment_policy, Some(session.conversation_id), diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs index e52c87bb352..89bf4d424c3 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs @@ -121,7 +121,6 @@ pub(super) async fn try_run_zsh_fork( let crate::sandboxing::ExecRequest { command, cwd: sandbox_cwd, - sandbox_policy_cwd, env: sandbox_env, network: sandbox_network, expiration: _sandbox_expiration, @@ -156,7 +155,7 @@ pub(super) async fn try_run_zsh_fork( sandbox_permissions, justification, arg0, - sandbox_policy_cwd, + sandbox_policy_cwd: ctx.turn.cwd.clone(), macos_seatbelt_profile_extensions: ctx .turn .config @@ -262,7 +261,7 @@ pub(crate) async fn prepare_unified_exec_zsh_fork( sandbox_permissions: exec_request.sandbox_permissions, justification: exec_request.justification.clone(), arg0: exec_request.arg0.clone(), - sandbox_policy_cwd: exec_request.sandbox_policy_cwd.clone(), + sandbox_policy_cwd: ctx.turn.cwd.clone(), macos_seatbelt_profile_extensions: ctx .turn .config @@ -901,7 +900,6 @@ impl ShellCommandExecutor for CoreShellCommandExecutor { crate::sandboxing::ExecRequest { command: self.command.clone(), cwd: self.cwd.clone(), - sandbox_policy_cwd: self.sandbox_policy_cwd.clone(), env: exec_env, network: self.network.clone(), expiration: ExecExpiration::Cancellation(cancel_rx), diff --git a/codex-rs/windows-sandbox-rs/src/allow.rs b/codex-rs/windows-sandbox-rs/src/allow.rs index 1f8b2253694..b40532cda85 100644 --- a/codex-rs/windows-sandbox-rs/src/allow.rs +++ b/codex-rs/windows-sandbox-rs/src/allow.rs @@ -127,37 +127,6 @@ mod tests { assert!(paths.deny.is_empty(), "no deny paths expected"); } - #[test] - fn resolves_relative_writable_roots_against_policy_cwd() { - let tmp = TempDir::new().expect("tempdir"); - let policy_cwd = tmp.path().join("policy"); - let command_cwd = tmp.path().join("command"); - let relative_root = PathBuf::from("shared"); - let shared_from_policy = policy_cwd.join(&relative_root); - let shared_from_command = command_cwd.join(&relative_root); - let _ = fs::create_dir_all(&policy_cwd); - let _ = fs::create_dir_all(&command_cwd); - let _ = fs::create_dir_all(&shared_from_policy); - let _ = fs::create_dir_all(&shared_from_command); - - let policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![AbsolutePathBuf::try_from(relative_root.as_path()).unwrap()], - read_only_access: Default::default(), - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: false, - }; - - let paths = compute_allow_paths(&policy, &policy_cwd, &command_cwd, &HashMap::new()); - - assert!(paths - .allow - .contains(&dunce::canonicalize(&shared_from_policy).unwrap())); - assert!(!paths - .allow - .contains(&dunce::canonicalize(&shared_from_command).unwrap())); - } - #[test] fn excludes_tmp_env_vars_when_requested() { let tmp = TempDir::new().expect("tempdir"); From a85ba0f62d0c78a0ae06cbfbc448cce1b33ed3bf Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Thu, 12 Mar 2026 15:39:15 -0700 Subject: [PATCH 06/17] fix: clean up windows exec clippy args --- codex-rs/core/src/exec.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index 0c30dc8b521..e89ea441619 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -756,19 +756,18 @@ impl Default for ExecToolCallOutput { } } -#[cfg_attr(not(target_os = "windows"), allow(unused_variables))] async fn exec( params: ExecParams, - sandbox: SandboxType, - sandbox_policy: &SandboxPolicy, - file_system_sandbox_policy: &FileSystemSandboxPolicy, + _sandbox: SandboxType, + _sandbox_policy: &SandboxPolicy, + _file_system_sandbox_policy: &FileSystemSandboxPolicy, network_sandbox_policy: NetworkSandboxPolicy, stdout_stream: Option, after_spawn: Option>, ) -> Result { #[cfg(target_os = "windows")] - if sandbox == SandboxType::WindowsRestrictedToken { - return exec_windows_sandbox(params, sandbox_policy).await; + if _sandbox == SandboxType::WindowsRestrictedToken { + return exec_windows_sandbox(params, _sandbox_policy).await; } let ExecParams { command, From e17abcb412e8335fe87426cc6edd05757dbd2a0c Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Thu, 12 Mar 2026 20:38:53 -0700 Subject: [PATCH 07/17] fix: address windows sandbox review nits --- codex-rs/core/src/exec.rs | 36 ++++++++++++++++----------------- codex-rs/core/src/exec_tests.rs | 11 ++++++---- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index e89ea441619..c45a679b778 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -831,33 +831,33 @@ pub(crate) fn unsupported_windows_restricted_token_sandbox_reason( network_sandbox_policy: NetworkSandboxPolicy, sandbox_policy_cwd: &Path, ) -> Option { + if sandbox != SandboxType::WindowsRestrictedToken { + return None; + } + + let needs_direct_runtime_enforcement = file_system_sandbox_policy + .needs_direct_runtime_enforcement(network_sandbox_policy, sandbox_policy_cwd); + if should_use_windows_restricted_token_sandbox( sandbox, sandbox_policy, file_system_sandbox_policy, - ) && !file_system_sandbox_policy - .needs_direct_runtime_enforcement(network_sandbox_policy, sandbox_policy_cwd) + ) && !needs_direct_runtime_enforcement { return None; } - if sandbox != SandboxType::WindowsRestrictedToken { - return None; - } - - if file_system_sandbox_policy - .needs_direct_runtime_enforcement(network_sandbox_policy, sandbox_policy_cwd) - { - return Some( - "windows sandbox backend cannot enforce split filesystem permissions directly; refusing to run unsandboxed" - .to_string(), - ); - } + let reason = if needs_direct_runtime_enforcement { + "windows sandbox backend cannot enforce split filesystem permissions directly; refusing to run unsandboxed" + .to_string() + } else { + format!( + "windows sandbox backend cannot enforce file_system={:?}, network={network_sandbox_policy:?}, legacy_policy={sandbox_policy:?}; refusing to run unsandboxed", + file_system_sandbox_policy.kind, + ) + }; - Some(format!( - "windows sandbox backend cannot enforce file_system={:?}, network={network_sandbox_policy:?}, legacy_policy={sandbox_policy:?}; refusing to run unsandboxed", - file_system_sandbox_policy.kind, - )) + Some(reason) } /// Consumes the output of a child process, truncating it so it is suitable for /// use as the output of a `shell` tool call. Also enforces specified timeout. diff --git a/codex-rs/core/src/exec_tests.rs b/codex-rs/core/src/exec_tests.rs index 61d922a56e2..28c66632b6d 100644 --- a/codex-rs/core/src/exec_tests.rs +++ b/codex-rs/core/src/exec_tests.rs @@ -218,6 +218,7 @@ fn windows_restricted_token_rejects_network_only_restrictions() { network_access: codex_protocol::protocol::NetworkAccess::Restricted, }; let file_system_policy = FileSystemSandboxPolicy::unrestricted(); + let sandbox_policy_cwd = std::env::current_dir().expect("cwd"); assert_eq!( unsupported_windows_restricted_token_sandbox_reason( @@ -225,7 +226,7 @@ fn windows_restricted_token_rejects_network_only_restrictions() { &policy, &file_system_policy, NetworkSandboxPolicy::Restricted, - Path::new("/tmp"), + &sandbox_policy_cwd, ), Some( "windows sandbox backend cannot enforce file_system=Unrestricted, network=Restricted, legacy_policy=ExternalSandbox { network_access: Restricted }; refusing to run unsandboxed".to_string() @@ -237,6 +238,7 @@ fn windows_restricted_token_rejects_network_only_restrictions() { fn windows_restricted_token_allows_legacy_restricted_policies() { let policy = SandboxPolicy::new_read_only_policy(); let file_system_policy = FileSystemSandboxPolicy::from(&policy); + let sandbox_policy_cwd = std::env::current_dir().expect("cwd"); assert_eq!( unsupported_windows_restricted_token_sandbox_reason( @@ -244,7 +246,7 @@ fn windows_restricted_token_allows_legacy_restricted_policies() { &policy, &file_system_policy, NetworkSandboxPolicy::Restricted, - Path::new("/tmp"), + &sandbox_policy_cwd, ), None ); @@ -260,6 +262,7 @@ fn windows_restricted_token_allows_legacy_workspace_write_policies() { exclude_slash_tmp: false, }; let file_system_policy = FileSystemSandboxPolicy::from(&policy); + let sandbox_policy_cwd = std::env::current_dir().expect("cwd"); assert_eq!( unsupported_windows_restricted_token_sandbox_reason( @@ -267,7 +270,7 @@ fn windows_restricted_token_allows_legacy_workspace_write_policies() { &policy, &file_system_policy, NetworkSandboxPolicy::Restricted, - Path::new("/tmp"), + &sandbox_policy_cwd, ), None ); @@ -403,7 +406,7 @@ async fn kill_child_process_group_kills_grandchildren_on_timeout() -> Result<()> let env: HashMap = std::env::vars().collect(); let params = ExecParams { command, - cwd: cwd.clone(), + cwd, expiration: 500.into(), env, network: None, From d061fa342c20c8bc3db1dc7363d433021725817e Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Fri, 13 Mar 2026 00:09:48 -0700 Subject: [PATCH 08/17] fix: support split carveouts in windows restricted token --- codex-rs/core/README.md | 19 +- codex-rs/core/src/exec.rs | 165 ++++++++++++++++-- codex-rs/core/src/exec_tests.rs | 117 ++++++++++++- codex-rs/core/src/sandboxing/mod.rs | 27 +-- codex-rs/core/src/sandboxing/mod_tests.rs | 135 +++++++++++++- codex-rs/core/src/tasks/user_shell.rs | 1 + .../tools/runtimes/shell/unix_escalation.rs | 2 + codex-rs/windows-sandbox-rs/src/lib.rs | 31 +++- 8 files changed, 457 insertions(+), 40 deletions(-) diff --git a/codex-rs/core/README.md b/codex-rs/core/README.md index 7bca5a2a189..c93208419a3 100644 --- a/codex-rs/core/README.md +++ b/codex-rs/core/README.md @@ -73,11 +73,20 @@ path instead of printing directly from the sandbox helper. ### Windows -The restricted-token sandbox currently enforces only the subset of filesystem -and network restrictions that round-trip through the legacy `SandboxPolicy` -model. Split-only filesystem carveouts that require direct -`FileSystemSandboxPolicy` enforcement are rejected instead of running with a -weaker sandbox. +The unelevated restricted-token sandbox supports: + +- legacy `ReadOnly` and `WorkspaceWrite` behavior +- full-read split filesystem policies whose writable roots still match the + legacy `WorkspaceWrite` root set, but need extra read-only carveouts under + those writable roots + +The elevated Windows sandbox still supports only the legacy +`SandboxPolicy`-equivalent path. + +Windows still rejects split-only filesystem policies that would require direct +read restriction, explicit unreadable carveouts, reopened writable descendants +under read-only carveouts, or elevated setup/runner support, instead of running +with a weaker sandbox. ### All Platforms diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index c45a679b778..7c5151fb74a 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -1,6 +1,7 @@ #[cfg(unix)] use std::os::unix::process::ExitStatusExt; +use std::collections::BTreeSet; use std::collections::HashMap; use std::io; use std::path::Path; @@ -34,6 +35,7 @@ use crate::spawn::spawn_child_async; use crate::text_encoding::bytes_to_string_smart; use crate::tools::sandboxing::SandboxablePreference; use codex_network_proxy::NetworkProxy; +use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::permissions::FileSystemSandboxKind; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; @@ -86,6 +88,17 @@ pub struct ExecParams { pub arg0: Option, } +/// Extra filesystem deny-write carveouts for the non-elevated Windows +/// restricted-token backend. +/// +/// These are applied on top of the legacy `WorkspaceWrite` allow set, so we +/// can support a narrow split-policy subset without changing legacy Windows +/// sandbox semantics. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct WindowsRestrictedTokenFilesystemOverlay { + pub(crate) additional_deny_write_paths: Vec, +} + fn select_process_exec_tool_sandbox_type( file_system_sandbox_policy: &FileSystemSandboxPolicy, network_sandbox_policy: NetworkSandboxPolicy, @@ -298,6 +311,7 @@ pub(crate) async fn execute_exec_request( sandbox_policy: _sandbox_policy_from_env, file_system_sandbox_policy, network_sandbox_policy, + windows_restricted_token_filesystem_overlay, justification, arg0, } = exec_request; @@ -322,6 +336,7 @@ pub(crate) async fn execute_exec_request( sandbox, sandbox_policy, &file_system_sandbox_policy, + windows_restricted_token_filesystem_overlay.as_ref(), network_sandbox_policy, stdout_stream, after_spawn, @@ -401,11 +416,11 @@ fn record_windows_sandbox_spawn_failure( async fn exec_windows_sandbox( params: ExecParams, sandbox_policy: &SandboxPolicy, + windows_restricted_token_filesystem_overlay: Option<&WindowsRestrictedTokenFilesystemOverlay>, ) -> Result { use crate::config::find_codex_home; - use codex_protocol::config_types::WindowsSandboxLevel; - use codex_windows_sandbox::run_windows_sandbox_capture; use codex_windows_sandbox::run_windows_sandbox_capture_elevated; + use codex_windows_sandbox::run_windows_sandbox_capture_with_extra_deny_write_paths; let ExecParams { command, @@ -439,6 +454,9 @@ async fn exec_windows_sandbox( let command_path = command.first().cloned(); let sandbox_level = windows_sandbox_level; let use_elevated = matches!(sandbox_level, WindowsSandboxLevel::Elevated); + let additional_deny_write_paths = windows_restricted_token_filesystem_overlay + .map(|overlay| overlay.additional_deny_write_paths.clone()) + .unwrap_or_default(); let spawn_res = tokio::task::spawn_blocking(move || { if use_elevated { run_windows_sandbox_capture_elevated( @@ -452,7 +470,7 @@ async fn exec_windows_sandbox( windows_sandbox_private_desktop, ) } else { - run_windows_sandbox_capture( + run_windows_sandbox_capture_with_extra_deny_write_paths( policy_str.as_str(), &sandbox_cwd, codex_home.as_ref(), @@ -460,6 +478,7 @@ async fn exec_windows_sandbox( &cwd, env, timeout_ms, + &additional_deny_write_paths, windows_sandbox_private_desktop, ) } @@ -756,18 +775,25 @@ impl Default for ExecToolCallOutput { } } +#[allow(clippy::too_many_arguments)] async fn exec( params: ExecParams, _sandbox: SandboxType, _sandbox_policy: &SandboxPolicy, _file_system_sandbox_policy: &FileSystemSandboxPolicy, + _windows_restricted_token_filesystem_overlay: Option<&WindowsRestrictedTokenFilesystemOverlay>, network_sandbox_policy: NetworkSandboxPolicy, stdout_stream: Option, after_spawn: Option>, ) -> Result { #[cfg(target_os = "windows")] if _sandbox == SandboxType::WindowsRestrictedToken { - return exec_windows_sandbox(params, _sandbox_policy).await; + return exec_windows_sandbox( + params, + _sandbox_policy, + _windows_restricted_token_filesystem_overlay, + ) + .await; } let ExecParams { command, @@ -824,15 +850,36 @@ fn should_use_windows_restricted_token_sandbox( ) } +#[cfg_attr(not(test), allow(dead_code))] pub(crate) fn unsupported_windows_restricted_token_sandbox_reason( sandbox: SandboxType, sandbox_policy: &SandboxPolicy, file_system_sandbox_policy: &FileSystemSandboxPolicy, network_sandbox_policy: NetworkSandboxPolicy, sandbox_policy_cwd: &Path, + windows_sandbox_level: WindowsSandboxLevel, ) -> Option { + resolve_windows_restricted_token_filesystem_overlay( + sandbox, + sandbox_policy, + file_system_sandbox_policy, + network_sandbox_policy, + sandbox_policy_cwd, + windows_sandbox_level, + ) + .err() +} + +pub(crate) fn resolve_windows_restricted_token_filesystem_overlay( + sandbox: SandboxType, + sandbox_policy: &SandboxPolicy, + file_system_sandbox_policy: &FileSystemSandboxPolicy, + network_sandbox_policy: NetworkSandboxPolicy, + sandbox_policy_cwd: &Path, + windows_sandbox_level: WindowsSandboxLevel, +) -> std::result::Result, String> { if sandbox != SandboxType::WindowsRestrictedToken { - return None; + return Ok(None); } let needs_direct_runtime_enforcement = file_system_sandbox_policy @@ -844,20 +891,110 @@ pub(crate) fn unsupported_windows_restricted_token_sandbox_reason( file_system_sandbox_policy, ) && !needs_direct_runtime_enforcement { - return None; + return Ok(None); } - let reason = if needs_direct_runtime_enforcement { - "windows sandbox backend cannot enforce split filesystem permissions directly; refusing to run unsandboxed" - .to_string() - } else { - format!( + if !should_use_windows_restricted_token_sandbox( + sandbox, + sandbox_policy, + file_system_sandbox_policy, + ) { + return Err(format!( "windows sandbox backend cannot enforce file_system={:?}, network={network_sandbox_policy:?}, legacy_policy={sandbox_policy:?}; refusing to run unsandboxed", file_system_sandbox_policy.kind, - ) - }; + )); + } + + if windows_sandbox_level != WindowsSandboxLevel::RestrictedToken { + return Err( + "windows elevated sandbox backend cannot enforce split filesystem permissions directly; refusing to run unsandboxed" + .to_string(), + ); + } + + if !file_system_sandbox_policy.has_full_disk_read_access() { + return Err( + "windows unelevated restricted-token sandbox cannot enforce split filesystem read restrictions directly; refusing to run unsandboxed" + .to_string(), + ); + } + + if !file_system_sandbox_policy + .get_unreadable_roots_with_cwd(sandbox_policy_cwd) + .is_empty() + { + return Err( + "windows unelevated restricted-token sandbox cannot enforce unreadable split filesystem carveouts directly; refusing to run unsandboxed" + .to_string(), + ); + } + + let legacy_writable_roots = sandbox_policy.get_writable_roots_with_cwd(sandbox_policy_cwd); + let split_writable_roots = + file_system_sandbox_policy.get_writable_roots_with_cwd(sandbox_policy_cwd); + let legacy_root_paths: BTreeSet = legacy_writable_roots + .iter() + .map(|root| root.root.to_path_buf()) + .collect(); + let split_root_paths: BTreeSet = split_writable_roots + .iter() + .map(|root| root.root.to_path_buf()) + .collect(); + + if legacy_root_paths != split_root_paths { + return Err( + "windows unelevated restricted-token sandbox cannot enforce split writable root sets directly; refusing to run unsandboxed" + .to_string(), + ); + } + + for writable_root in &split_writable_roots { + for read_only_subpath in &writable_root.read_only_subpaths { + if split_writable_roots.iter().any(|candidate| { + candidate.root.as_path() != writable_root.root.as_path() + && candidate + .root + .as_path() + .starts_with(read_only_subpath.as_path()) + }) { + return Err( + "windows unelevated restricted-token sandbox cannot reopen writable descendants under read-only carveouts directly; refusing to run unsandboxed" + .to_string(), + ); + } + } + } + + let mut additional_deny_write_paths = BTreeSet::new(); + for split_root in &split_writable_roots { + let Some(legacy_root) = legacy_writable_roots + .iter() + .find(|candidate| candidate.root == split_root.root) + else { + return Err( + "windows unelevated restricted-token sandbox cannot enforce split writable root sets directly; refusing to run unsandboxed" + .to_string(), + ); + }; + + for read_only_subpath in &split_root.read_only_subpaths { + if !legacy_root + .read_only_subpaths + .iter() + .any(|candidate| candidate == read_only_subpath) + { + additional_deny_write_paths.insert(read_only_subpath.to_path_buf()); + } + } + } + + if additional_deny_write_paths.is_empty() { + return Ok(None); + } - Some(reason) + Ok(Some(WindowsRestrictedTokenFilesystemOverlay { + additional_deny_write_paths: additional_deny_write_paths.into_iter().collect(), + })) } /// Consumes the output of a child process, truncating it so it is suitable for /// use as the output of a `shell` tool call. Also enforces specified timeout. diff --git a/codex-rs/core/src/exec_tests.rs b/codex-rs/core/src/exec_tests.rs index 28c66632b6d..219110bb562 100644 --- a/codex-rs/core/src/exec_tests.rs +++ b/codex-rs/core/src/exec_tests.rs @@ -227,6 +227,7 @@ fn windows_restricted_token_rejects_network_only_restrictions() { &file_system_policy, NetworkSandboxPolicy::Restricted, &sandbox_policy_cwd, + WindowsSandboxLevel::RestrictedToken, ), Some( "windows sandbox backend cannot enforce file_system=Unrestricted, network=Restricted, legacy_policy=ExternalSandbox { network_access: Restricted }; refusing to run unsandboxed".to_string() @@ -247,6 +248,7 @@ fn windows_restricted_token_allows_legacy_restricted_policies() { &file_system_policy, NetworkSandboxPolicy::Restricted, &sandbox_policy_cwd, + WindowsSandboxLevel::RestrictedToken, ), None ); @@ -258,8 +260,8 @@ fn windows_restricted_token_allows_legacy_workspace_write_policies() { writable_roots: vec![], read_only_access: codex_protocol::protocol::ReadOnlyAccess::FullAccess, network_access: false, - exclude_tmpdir_env_var: false, - exclude_slash_tmp: false, + exclude_tmpdir_env_var: true, + exclude_slash_tmp: true, }; let file_system_policy = FileSystemSandboxPolicy::from(&policy); let sandbox_policy_cwd = std::env::current_dir().expect("cwd"); @@ -271,6 +273,7 @@ fn windows_restricted_token_allows_legacy_workspace_write_policies() { &file_system_policy, NetworkSandboxPolicy::Restricted, &sandbox_policy_cwd, + WindowsSandboxLevel::RestrictedToken, ), None ); @@ -285,8 +288,8 @@ fn windows_restricted_token_rejects_split_only_filesystem_policies() { writable_roots: vec![], read_only_access: codex_protocol::protocol::ReadOnlyAccess::FullAccess, network_access: false, - exclude_tmpdir_env_var: false, - exclude_slash_tmp: false, + exclude_tmpdir_env_var: true, + exclude_slash_tmp: true, }; let file_system_policy = FileSystemSandboxPolicy::restricted(vec![ codex_protocol::permissions::FileSystemSandboxEntry { @@ -311,9 +314,10 @@ fn windows_restricted_token_rejects_split_only_filesystem_policies() { &file_system_policy, NetworkSandboxPolicy::Restricted, temp_dir.path(), + WindowsSandboxLevel::RestrictedToken, ), Some( - "windows sandbox backend cannot enforce split filesystem permissions directly; refusing to run unsandboxed" + "windows unelevated restricted-token sandbox cannot enforce split filesystem read restrictions directly; refusing to run unsandboxed" .to_string() ) ); @@ -321,6 +325,99 @@ fn windows_restricted_token_rejects_split_only_filesystem_policies() { #[test] fn windows_restricted_token_rejects_root_write_read_only_carveouts() { + let temp_dir = tempfile::TempDir::new().expect("tempdir"); + let docs = temp_dir.path().join("docs"); + std::fs::create_dir_all(&docs).expect("create docs"); + let policy = SandboxPolicy::WorkspaceWrite { + writable_roots: vec![], + read_only_access: codex_protocol::protocol::ReadOnlyAccess::FullAccess, + network_access: false, + exclude_tmpdir_env_var: true, + exclude_slash_tmp: true, + }; + let file_system_policy = FileSystemSandboxPolicy::restricted(vec![ + codex_protocol::permissions::FileSystemSandboxEntry { + path: codex_protocol::permissions::FileSystemPath::Special { + value: codex_protocol::permissions::FileSystemSpecialPath::Root, + }, + access: codex_protocol::permissions::FileSystemAccessMode::Write, + }, + codex_protocol::permissions::FileSystemSandboxEntry { + path: codex_protocol::permissions::FileSystemPath::Path { + path: codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(&docs) + .expect("absolute docs"), + }, + access: codex_protocol::permissions::FileSystemAccessMode::Read, + }, + ]); + + assert_eq!( + unsupported_windows_restricted_token_sandbox_reason( + SandboxType::WindowsRestrictedToken, + &policy, + &file_system_policy, + NetworkSandboxPolicy::Restricted, + temp_dir.path(), + WindowsSandboxLevel::RestrictedToken, + ), + Some( + "windows unelevated restricted-token sandbox cannot enforce split writable root sets directly; refusing to run unsandboxed" + .to_string() + ) + ); +} + +#[test] +fn windows_restricted_token_supports_full_read_split_write_read_carveouts() { + let temp_dir = tempfile::TempDir::new().expect("tempdir"); + let docs = temp_dir.path().join("docs"); + std::fs::create_dir_all(&docs).expect("create docs"); + let policy = SandboxPolicy::WorkspaceWrite { + writable_roots: vec![], + read_only_access: codex_protocol::protocol::ReadOnlyAccess::FullAccess, + network_access: false, + exclude_tmpdir_env_var: true, + exclude_slash_tmp: true, + }; + let file_system_policy = FileSystemSandboxPolicy::restricted(vec![ + codex_protocol::permissions::FileSystemSandboxEntry { + path: codex_protocol::permissions::FileSystemPath::Special { + value: codex_protocol::permissions::FileSystemSpecialPath::Root, + }, + access: codex_protocol::permissions::FileSystemAccessMode::Read, + }, + codex_protocol::permissions::FileSystemSandboxEntry { + path: codex_protocol::permissions::FileSystemPath::Special { + value: codex_protocol::permissions::FileSystemSpecialPath::CurrentWorkingDirectory, + }, + access: codex_protocol::permissions::FileSystemAccessMode::Write, + }, + codex_protocol::permissions::FileSystemSandboxEntry { + path: codex_protocol::permissions::FileSystemPath::Path { + path: codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(&docs) + .expect("absolute docs"), + }, + access: codex_protocol::permissions::FileSystemAccessMode::Read, + }, + ]); + + assert_eq!( + resolve_windows_restricted_token_filesystem_overlay( + SandboxType::WindowsRestrictedToken, + &policy, + &file_system_policy, + NetworkSandboxPolicy::Restricted, + temp_dir.path(), + WindowsSandboxLevel::RestrictedToken, + ), + Ok(Some(WindowsRestrictedTokenFilesystemOverlay { + additional_deny_write_paths: vec![docs], + })) + ); +} + +#[test] +fn windows_elevated_rejects_split_write_read_carveouts() { let temp_dir = tempfile::TempDir::new().expect("tempdir"); let docs = temp_dir.path().join("docs"); std::fs::create_dir_all(&docs).expect("create docs"); @@ -336,6 +433,12 @@ fn windows_restricted_token_rejects_root_write_read_only_carveouts() { path: codex_protocol::permissions::FileSystemPath::Special { value: codex_protocol::permissions::FileSystemSpecialPath::Root, }, + access: codex_protocol::permissions::FileSystemAccessMode::Read, + }, + codex_protocol::permissions::FileSystemSandboxEntry { + path: codex_protocol::permissions::FileSystemPath::Special { + value: codex_protocol::permissions::FileSystemSpecialPath::CurrentWorkingDirectory, + }, access: codex_protocol::permissions::FileSystemAccessMode::Write, }, codex_protocol::permissions::FileSystemSandboxEntry { @@ -354,9 +457,10 @@ fn windows_restricted_token_rejects_root_write_read_only_carveouts() { &file_system_policy, NetworkSandboxPolicy::Restricted, temp_dir.path(), + WindowsSandboxLevel::Elevated, ), Some( - "windows sandbox backend cannot enforce split filesystem permissions directly; refusing to run unsandboxed" + "windows elevated sandbox backend cannot enforce split filesystem permissions directly; refusing to run unsandboxed" .to_string() ) ); @@ -422,6 +526,7 @@ async fn kill_child_process_group_kills_grandchildren_on_timeout() -> Result<()> SandboxType::None, &SandboxPolicy::new_read_only_policy(), &FileSystemSandboxPolicy::from(&SandboxPolicy::new_read_only_policy()), + None, NetworkSandboxPolicy::Restricted, None, None, diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs index ef35da038f1..bc5c04ea312 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -12,8 +12,9 @@ use crate::exec::ExecExpiration; use crate::exec::ExecToolCallOutput; use crate::exec::SandboxType; use crate::exec::StdoutStream; +use crate::exec::WindowsRestrictedTokenFilesystemOverlay; use crate::exec::execute_exec_request; -use crate::exec::unsupported_windows_restricted_token_sandbox_reason; +use crate::exec::resolve_windows_restricted_token_filesystem_overlay; use crate::landlock::allow_network_for_proxy; use crate::landlock::create_linux_sandbox_command_args_for_policies; use crate::protocol::SandboxPolicy; @@ -75,6 +76,8 @@ pub struct ExecRequest { pub sandbox_policy: SandboxPolicy, pub file_system_sandbox_policy: FileSystemSandboxPolicy, pub network_sandbox_policy: NetworkSandboxPolicy, + pub(crate) windows_restricted_token_filesystem_overlay: + Option, pub justification: Option, pub arg0: Option, } @@ -635,17 +638,16 @@ impl SandboxManager { } else { (file_system_policy.clone(), network_policy) }; - if let Some(reason) = unsupported_windows_restricted_token_sandbox_reason( - sandbox, - &effective_policy, - &effective_file_system_policy, - effective_network_policy, - sandbox_policy_cwd, - ) { - return Err(SandboxTransformError::UnsupportedWindowsRestrictedToken( - reason, - )); - } + let windows_restricted_token_filesystem_overlay = + resolve_windows_restricted_token_filesystem_overlay( + sandbox, + &effective_policy, + &effective_file_system_policy, + effective_network_policy, + sandbox_policy_cwd, + windows_sandbox_level, + ) + .map_err(SandboxTransformError::UnsupportedWindowsRestrictedToken)?; let mut env = spec.env; if !effective_network_policy.is_enabled() { env.insert( @@ -728,6 +730,7 @@ impl SandboxManager { sandbox_policy: effective_policy, file_system_sandbox_policy: effective_file_system_policy, network_sandbox_policy: effective_network_policy, + windows_restricted_token_filesystem_overlay, justification: spec.justification, arg0: arg0_override, }) diff --git a/codex-rs/core/src/sandboxing/mod_tests.rs b/codex-rs/core/src/sandboxing/mod_tests.rs index 73dd4f2265e..84f742bb411 100644 --- a/codex-rs/core/src/sandboxing/mod_tests.rs +++ b/codex-rs/core/src/sandboxing/mod_tests.rs @@ -9,6 +9,7 @@ use super::normalize_additional_permissions; use super::sandbox_policy_with_additional_permissions; use super::should_require_platform_sandbox; use crate::exec::SandboxType; +use crate::exec::WindowsRestrictedTokenFilesystemOverlay; use crate::protocol::NetworkAccess; use crate::protocol::ReadOnlyAccess; use crate::protocol::SandboxPolicy; @@ -212,8 +213,8 @@ fn transform_rejects_unsupported_windows_split_only_filesystem_policies() { writable_roots: vec![], read_only_access: ReadOnlyAccess::FullAccess, network_access: false, - exclude_tmpdir_env_var: false, - exclude_slash_tmp: false, + exclude_tmpdir_env_var: true, + exclude_slash_tmp: true, }, file_system_policy: &FileSystemSandboxPolicy::restricted(vec![ FileSystemSandboxEntry { @@ -246,6 +247,136 @@ fn transform_rejects_unsupported_windows_split_only_filesystem_policies() { )); } +#[test] +fn transform_allows_supported_windows_split_write_read_carveouts() { + let manager = SandboxManager::new(); + let temp_dir = TempDir::new().expect("create temp dir"); + let docs = AbsolutePathBuf::from_absolute_path(temp_dir.path().join("docs")) + .expect("absolute docs path"); + std::fs::create_dir_all(docs.as_path()).expect("create docs"); + let exec_request = manager + .transform(super::SandboxTransformRequest { + spec: super::CommandSpec { + program: "true".to_string(), + args: Vec::new(), + cwd: temp_dir.path().to_path_buf(), + env: HashMap::new(), + expiration: crate::exec::ExecExpiration::DefaultTimeout, + sandbox_permissions: super::SandboxPermissions::UseDefault, + additional_permissions: None, + justification: None, + }, + policy: &SandboxPolicy::WorkspaceWrite { + writable_roots: vec![], + read_only_access: ReadOnlyAccess::FullAccess, + network_access: false, + exclude_tmpdir_env_var: true, + exclude_slash_tmp: true, + }, + file_system_policy: &FileSystemSandboxPolicy::restricted(vec![ + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Root, + }, + access: FileSystemAccessMode::Read, + }, + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::CurrentWorkingDirectory, + }, + access: FileSystemAccessMode::Write, + }, + FileSystemSandboxEntry { + path: FileSystemPath::Path { path: docs.clone() }, + access: FileSystemAccessMode::Read, + }, + ]), + network_policy: NetworkSandboxPolicy::Restricted, + sandbox: SandboxType::WindowsRestrictedToken, + enforce_managed_network: false, + network: None, + sandbox_policy_cwd: temp_dir.path(), + #[cfg(target_os = "macos")] + macos_seatbelt_profile_extensions: None, + codex_linux_sandbox_exe: None, + use_legacy_landlock: false, + windows_sandbox_level: WindowsSandboxLevel::RestrictedToken, + }) + .expect("supported split write/read carveout should transform"); + + assert_eq!( + exec_request + .windows_restricted_token_filesystem_overlay + .expect("windows overlay"), + WindowsRestrictedTokenFilesystemOverlay { + additional_deny_write_paths: vec![docs.to_path_buf()], + } + ); +} + +#[test] +fn transform_rejects_windows_elevated_split_write_read_carveouts() { + let manager = SandboxManager::new(); + let temp_dir = TempDir::new().expect("create temp dir"); + let docs = AbsolutePathBuf::from_absolute_path(temp_dir.path().join("docs")) + .expect("absolute docs path"); + std::fs::create_dir_all(docs.as_path()).expect("create docs"); + let err = manager + .transform(super::SandboxTransformRequest { + spec: super::CommandSpec { + program: "true".to_string(), + args: Vec::new(), + cwd: temp_dir.path().to_path_buf(), + env: HashMap::new(), + expiration: crate::exec::ExecExpiration::DefaultTimeout, + sandbox_permissions: super::SandboxPermissions::UseDefault, + additional_permissions: None, + justification: None, + }, + policy: &SandboxPolicy::WorkspaceWrite { + writable_roots: vec![], + read_only_access: ReadOnlyAccess::FullAccess, + network_access: false, + exclude_tmpdir_env_var: false, + exclude_slash_tmp: false, + }, + file_system_policy: &FileSystemSandboxPolicy::restricted(vec![ + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Root, + }, + access: FileSystemAccessMode::Read, + }, + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::CurrentWorkingDirectory, + }, + access: FileSystemAccessMode::Write, + }, + FileSystemSandboxEntry { + path: FileSystemPath::Path { path: docs }, + access: FileSystemAccessMode::Read, + }, + ]), + network_policy: NetworkSandboxPolicy::Restricted, + sandbox: SandboxType::WindowsRestrictedToken, + enforce_managed_network: false, + network: None, + sandbox_policy_cwd: temp_dir.path(), + #[cfg(target_os = "macos")] + macos_seatbelt_profile_extensions: None, + codex_linux_sandbox_exe: None, + use_legacy_landlock: false, + windows_sandbox_level: WindowsSandboxLevel::Elevated, + }) + .expect_err("elevated split write/read carveout should fail closed"); + + assert!(matches!( + err, + super::SandboxTransformError::UnsupportedWindowsRestrictedToken(_) + )); +} + #[test] fn normalize_additional_permissions_preserves_network() { let temp_dir = TempDir::new().expect("create temp dir"); diff --git a/codex-rs/core/src/tasks/user_shell.rs b/codex-rs/core/src/tasks/user_shell.rs index 83368efc950..c3ff8f95a0c 100644 --- a/codex-rs/core/src/tasks/user_shell.rs +++ b/codex-rs/core/src/tasks/user_shell.rs @@ -175,6 +175,7 @@ pub(crate) async fn execute_user_shell_command( sandbox_policy: sandbox_policy.clone(), file_system_sandbox_policy: FileSystemSandboxPolicy::from(&sandbox_policy), network_sandbox_policy: NetworkSandboxPolicy::from(&sandbox_policy), + windows_restricted_token_filesystem_overlay: None, justification: None, arg0: None, }; diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs index 89bf4d424c3..fe31a901587 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs @@ -131,6 +131,7 @@ pub(super) async fn try_run_zsh_fork( sandbox_policy, file_system_sandbox_policy, network_sandbox_policy, + windows_restricted_token_filesystem_overlay: _windows_restricted_token_filesystem_overlay, justification, arg0, } = sandbox_exec_request; @@ -910,6 +911,7 @@ impl ShellCommandExecutor for CoreShellCommandExecutor { sandbox_policy: self.sandbox_policy.clone(), file_system_sandbox_policy: self.file_system_sandbox_policy.clone(), network_sandbox_policy: self.network_sandbox_policy, + windows_restricted_token_filesystem_overlay: None, justification: self.justification.clone(), arg0: self.arg0.clone(), }, diff --git a/codex-rs/windows-sandbox-rs/src/lib.rs b/codex-rs/windows-sandbox-rs/src/lib.rs index 51751b9cb92..808b0973943 100644 --- a/codex-rs/windows-sandbox-rs/src/lib.rs +++ b/codex-rs/windows-sandbox-rs/src/lib.rs @@ -259,6 +259,29 @@ mod windows_impl { #[allow(clippy::too_many_arguments)] pub fn run_windows_sandbox_capture( + policy_json_or_preset: &str, + sandbox_policy_cwd: &Path, + codex_home: &Path, + command: Vec, + cwd: &Path, + env_map: HashMap, + timeout_ms: Option, + use_private_desktop: bool, + ) -> Result { + run_windows_sandbox_capture_with_extra_deny_write_paths( + policy_json_or_preset, + sandbox_policy_cwd, + codex_home, + command, + cwd, + env_map, + timeout_ms, + &[], + use_private_desktop, + ) + } + + pub fn run_windows_sandbox_capture_with_extra_deny_write_paths( policy_json_or_preset: &str, sandbox_policy_cwd: &Path, codex_home: &Path, @@ -266,6 +289,7 @@ mod windows_impl { cwd: &Path, mut env_map: HashMap, timeout_ms: Option, + additional_deny_write_paths: &[PathBuf], use_private_desktop: bool, ) -> Result { let policy = parse_policy(policy_json_or_preset)?; @@ -335,8 +359,13 @@ mod windows_impl { } let persist_aces = is_workspace_write; - let AllowDenyPaths { allow, deny } = + let AllowDenyPaths { allow, mut deny } = compute_allow_paths(&policy, sandbox_policy_cwd, ¤t_dir, &env_map); + for path in additional_deny_write_paths { + if path.exists() { + deny.insert(path.clone()); + } + } let canonical_cwd = canonicalize_path(¤t_dir); let mut guards: Vec<(PathBuf, *mut c_void)> = Vec::new(); unsafe { From 8220e205a253ef745e3a46d1b7c8ff04306d5611 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Tue, 17 Mar 2026 20:00:12 -0700 Subject: [PATCH 09/17] fix: allow windows sandbox helper args --- codex-rs/windows-sandbox-rs/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/codex-rs/windows-sandbox-rs/src/lib.rs b/codex-rs/windows-sandbox-rs/src/lib.rs index be5c95f1e9b..42c066d335e 100644 --- a/codex-rs/windows-sandbox-rs/src/lib.rs +++ b/codex-rs/windows-sandbox-rs/src/lib.rs @@ -281,6 +281,7 @@ mod windows_impl { ) } + #[allow(clippy::too_many_arguments)] pub fn run_windows_sandbox_capture_with_extra_deny_write_paths( policy_json_or_preset: &str, sandbox_policy_cwd: &Path, From ac18b9d3282eb711567cf387402cbc086c315b64 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Tue, 17 Mar 2026 20:34:26 -0700 Subject: [PATCH 10/17] fix: refresh windows sandbox exec fixtures --- codex-rs/app-server/src/command_exec.rs | 10 ++-- codex-rs/core/src/exec_tests.rs | 5 +- codex-rs/core/src/sandboxing/mod.rs | 56 +++++++++++++++++++++++ codex-rs/core/src/sandboxing/mod_tests.rs | 27 ++++++----- 4 files changed, 80 insertions(+), 18 deletions(-) diff --git a/codex-rs/app-server/src/command_exec.rs b/codex-rs/app-server/src/command_exec.rs index f761b18c962..3499907aa80 100644 --- a/codex-rs/app-server/src/command_exec.rs +++ b/codex-rs/app-server/src/command_exec.rs @@ -716,6 +716,8 @@ mod tests { #[cfg(not(target_os = "windows"))] use tokio_util::sync::CancellationToken; + use codex_core::sandboxing::ExecRequestArgs; + use super::*; #[cfg(not(target_os = "windows"))] use crate::outgoing_message::OutgoingEnvelope; @@ -727,7 +729,7 @@ mod tests { access: ReadOnlyAccess::FullAccess, network_access: false, }; - ExecRequest { + ExecRequest::new(ExecRequestArgs { command: vec!["cmd".to_string()], cwd: PathBuf::from("."), env: HashMap::new(), @@ -742,7 +744,7 @@ mod tests { network_sandbox_policy: NetworkSandboxPolicy::from(&sandbox_policy), justification: None, arg0: None, - } + }) } #[tokio::test] @@ -839,7 +841,7 @@ mod tests { outgoing: Arc::new(OutgoingMessageSender::new(tx)), request_id: request_id.clone(), process_id: Some("proc-100".to_string()), - exec_request: ExecRequest { + exec_request: ExecRequest::new(ExecRequestArgs { command: vec!["sh".to_string(), "-lc".to_string(), "sleep 30".to_string()], cwd: PathBuf::from("."), env: HashMap::new(), @@ -854,7 +856,7 @@ mod tests { network_sandbox_policy: NetworkSandboxPolicy::from(&sandbox_policy), justification: None, arg0: None, - }, + }), started_network_proxy: None, tty: false, stream_stdin: false, diff --git a/codex-rs/core/src/exec_tests.rs b/codex-rs/core/src/exec_tests.rs index 219110bb562..b9bffaafb0f 100644 --- a/codex-rs/core/src/exec_tests.rs +++ b/codex-rs/core/src/exec_tests.rs @@ -370,7 +370,8 @@ fn windows_restricted_token_rejects_root_write_read_only_carveouts() { #[test] fn windows_restricted_token_supports_full_read_split_write_read_carveouts() { let temp_dir = tempfile::TempDir::new().expect("tempdir"); - let docs = temp_dir.path().join("docs"); + let cwd = dunce::canonicalize(temp_dir.path()).expect("canonicalize temp dir"); + let docs = cwd.join("docs"); std::fs::create_dir_all(&docs).expect("create docs"); let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], @@ -407,7 +408,7 @@ fn windows_restricted_token_supports_full_read_split_write_read_carveouts() { &policy, &file_system_policy, NetworkSandboxPolicy::Restricted, - temp_dir.path(), + &cwd, WindowsSandboxLevel::RestrictedToken, ), Ok(Some(WindowsRestrictedTokenFilesystemOverlay { diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs index bc5c04ea312..1b479fe02b7 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -82,6 +82,62 @@ pub struct ExecRequest { pub arg0: Option, } +pub struct ExecRequestArgs { + pub command: Vec, + pub cwd: PathBuf, + pub env: HashMap, + pub network: Option, + pub expiration: ExecExpiration, + pub sandbox: SandboxType, + pub windows_sandbox_level: WindowsSandboxLevel, + pub windows_sandbox_private_desktop: bool, + pub sandbox_permissions: SandboxPermissions, + pub sandbox_policy: SandboxPolicy, + pub file_system_sandbox_policy: FileSystemSandboxPolicy, + pub network_sandbox_policy: NetworkSandboxPolicy, + pub justification: Option, + pub arg0: Option, +} + +impl ExecRequest { + pub fn new(args: ExecRequestArgs) -> Self { + let ExecRequestArgs { + command, + cwd, + env, + network, + expiration, + sandbox, + windows_sandbox_level, + windows_sandbox_private_desktop, + sandbox_permissions, + sandbox_policy, + file_system_sandbox_policy, + network_sandbox_policy, + justification, + arg0, + } = args; + + Self { + command, + cwd, + env, + network, + expiration, + sandbox, + windows_sandbox_level, + windows_sandbox_private_desktop, + sandbox_permissions, + sandbox_policy, + file_system_sandbox_policy, + network_sandbox_policy, + windows_restricted_token_filesystem_overlay: None, + justification, + arg0, + } + } +} + /// Bundled arguments for sandbox transformation. /// /// This keeps call sites self-documenting when several fields are optional. diff --git a/codex-rs/core/src/sandboxing/mod_tests.rs b/codex-rs/core/src/sandboxing/mod_tests.rs index 84f742bb411..074ec1163bc 100644 --- a/codex-rs/core/src/sandboxing/mod_tests.rs +++ b/codex-rs/core/src/sandboxing/mod_tests.rs @@ -195,14 +195,14 @@ fn transform_preserves_unrestricted_file_system_policy_for_restricted_network() fn transform_rejects_unsupported_windows_split_only_filesystem_policies() { let manager = SandboxManager::new(); let temp_dir = TempDir::new().expect("create temp dir"); - let docs = AbsolutePathBuf::from_absolute_path(temp_dir.path().join("docs")) - .expect("absolute docs path"); + let cwd = canonicalize(temp_dir.path()).expect("canonicalize temp dir"); + let docs = AbsolutePathBuf::from_absolute_path(cwd.join("docs")).expect("absolute docs path"); let err = manager .transform(super::SandboxTransformRequest { spec: super::CommandSpec { program: "true".to_string(), args: Vec::new(), - cwd: temp_dir.path().to_path_buf(), + cwd: cwd.clone(), env: HashMap::new(), expiration: crate::exec::ExecExpiration::DefaultTimeout, sandbox_permissions: super::SandboxPermissions::UseDefault, @@ -232,12 +232,13 @@ fn transform_rejects_unsupported_windows_split_only_filesystem_policies() { sandbox: SandboxType::WindowsRestrictedToken, enforce_managed_network: false, network: None, - sandbox_policy_cwd: temp_dir.path(), + sandbox_policy_cwd: &cwd, #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions: None, codex_linux_sandbox_exe: None, use_legacy_landlock: false, windows_sandbox_level: WindowsSandboxLevel::RestrictedToken, + windows_sandbox_private_desktop: false, }) .expect_err("unsupported split-only windows policy should fail closed"); @@ -251,15 +252,15 @@ fn transform_rejects_unsupported_windows_split_only_filesystem_policies() { fn transform_allows_supported_windows_split_write_read_carveouts() { let manager = SandboxManager::new(); let temp_dir = TempDir::new().expect("create temp dir"); - let docs = AbsolutePathBuf::from_absolute_path(temp_dir.path().join("docs")) - .expect("absolute docs path"); + let cwd = canonicalize(temp_dir.path()).expect("canonicalize temp dir"); + let docs = AbsolutePathBuf::from_absolute_path(cwd.join("docs")).expect("absolute docs path"); std::fs::create_dir_all(docs.as_path()).expect("create docs"); let exec_request = manager .transform(super::SandboxTransformRequest { spec: super::CommandSpec { program: "true".to_string(), args: Vec::new(), - cwd: temp_dir.path().to_path_buf(), + cwd: cwd.clone(), env: HashMap::new(), expiration: crate::exec::ExecExpiration::DefaultTimeout, sandbox_permissions: super::SandboxPermissions::UseDefault, @@ -295,12 +296,13 @@ fn transform_allows_supported_windows_split_write_read_carveouts() { sandbox: SandboxType::WindowsRestrictedToken, enforce_managed_network: false, network: None, - sandbox_policy_cwd: temp_dir.path(), + sandbox_policy_cwd: &cwd, #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions: None, codex_linux_sandbox_exe: None, use_legacy_landlock: false, windows_sandbox_level: WindowsSandboxLevel::RestrictedToken, + windows_sandbox_private_desktop: false, }) .expect("supported split write/read carveout should transform"); @@ -318,15 +320,15 @@ fn transform_allows_supported_windows_split_write_read_carveouts() { fn transform_rejects_windows_elevated_split_write_read_carveouts() { let manager = SandboxManager::new(); let temp_dir = TempDir::new().expect("create temp dir"); - let docs = AbsolutePathBuf::from_absolute_path(temp_dir.path().join("docs")) - .expect("absolute docs path"); + let cwd = canonicalize(temp_dir.path()).expect("canonicalize temp dir"); + let docs = AbsolutePathBuf::from_absolute_path(cwd.join("docs")).expect("absolute docs path"); std::fs::create_dir_all(docs.as_path()).expect("create docs"); let err = manager .transform(super::SandboxTransformRequest { spec: super::CommandSpec { program: "true".to_string(), args: Vec::new(), - cwd: temp_dir.path().to_path_buf(), + cwd: cwd.clone(), env: HashMap::new(), expiration: crate::exec::ExecExpiration::DefaultTimeout, sandbox_permissions: super::SandboxPermissions::UseDefault, @@ -362,12 +364,13 @@ fn transform_rejects_windows_elevated_split_write_read_carveouts() { sandbox: SandboxType::WindowsRestrictedToken, enforce_managed_network: false, network: None, - sandbox_policy_cwd: temp_dir.path(), + sandbox_policy_cwd: &cwd, #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions: None, codex_linux_sandbox_exe: None, use_legacy_landlock: false, windows_sandbox_level: WindowsSandboxLevel::Elevated, + windows_sandbox_private_desktop: false, }) .expect_err("elevated split write/read carveout should fail closed"); From b3ef8adef93d10ea780f6932cdaf67747d87f5c8 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Wed, 18 Mar 2026 00:02:06 -0700 Subject: [PATCH 11/17] fix: export windows sandbox overlay helper --- codex-rs/windows-sandbox-rs/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/codex-rs/windows-sandbox-rs/src/lib.rs b/codex-rs/windows-sandbox-rs/src/lib.rs index 42c066d335e..73193a207da 100644 --- a/codex-rs/windows-sandbox-rs/src/lib.rs +++ b/codex-rs/windows-sandbox-rs/src/lib.rs @@ -144,6 +144,8 @@ pub use token::get_current_token_for_restriction; #[cfg(target_os = "windows")] pub use windows_impl::run_windows_sandbox_capture; #[cfg(target_os = "windows")] +pub use windows_impl::run_windows_sandbox_capture_with_extra_deny_write_paths; +#[cfg(target_os = "windows")] pub use windows_impl::run_windows_sandbox_legacy_preflight; #[cfg(target_os = "windows")] pub use windows_impl::CaptureResult; From e7279be90ba20ca0df182a47ef665062ed41dd62 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Wed, 18 Mar 2026 00:55:29 -0700 Subject: [PATCH 12/17] refactor: simplify exec request construction --- codex-rs/app-server/src/command_exec.rs | 66 ++++++++++++------------- codex-rs/core/src/sandboxing/mod.rs | 52 +++++++------------ 2 files changed, 49 insertions(+), 69 deletions(-) diff --git a/codex-rs/app-server/src/command_exec.rs b/codex-rs/app-server/src/command_exec.rs index 3499907aa80..2b4be9a8241 100644 --- a/codex-rs/app-server/src/command_exec.rs +++ b/codex-rs/app-server/src/command_exec.rs @@ -716,8 +716,6 @@ mod tests { #[cfg(not(target_os = "windows"))] use tokio_util::sync::CancellationToken; - use codex_core::sandboxing::ExecRequestArgs; - use super::*; #[cfg(not(target_os = "windows"))] use crate::outgoing_message::OutgoingEnvelope; @@ -729,22 +727,22 @@ mod tests { access: ReadOnlyAccess::FullAccess, network_access: false, }; - ExecRequest::new(ExecRequestArgs { - command: vec!["cmd".to_string()], - cwd: PathBuf::from("."), - env: HashMap::new(), - network: None, - expiration: ExecExpiration::DefaultTimeout, - sandbox: SandboxType::WindowsRestrictedToken, - windows_sandbox_level: WindowsSandboxLevel::Disabled, - windows_sandbox_private_desktop: false, - sandbox_permissions: codex_core::sandboxing::SandboxPermissions::UseDefault, - sandbox_policy: sandbox_policy.clone(), - file_system_sandbox_policy: FileSystemSandboxPolicy::from(&sandbox_policy), - network_sandbox_policy: NetworkSandboxPolicy::from(&sandbox_policy), - justification: None, - arg0: None, - }) + ExecRequest::new( + vec!["cmd".to_string()], + PathBuf::from("."), + HashMap::new(), + None, + ExecExpiration::DefaultTimeout, + SandboxType::WindowsRestrictedToken, + WindowsSandboxLevel::Disabled, + false, + codex_core::sandboxing::SandboxPermissions::UseDefault, + sandbox_policy.clone(), + FileSystemSandboxPolicy::from(&sandbox_policy), + NetworkSandboxPolicy::from(&sandbox_policy), + None, + None, + ) } #[tokio::test] @@ -841,22 +839,22 @@ mod tests { outgoing: Arc::new(OutgoingMessageSender::new(tx)), request_id: request_id.clone(), process_id: Some("proc-100".to_string()), - exec_request: ExecRequest::new(ExecRequestArgs { - command: vec!["sh".to_string(), "-lc".to_string(), "sleep 30".to_string()], - cwd: PathBuf::from("."), - env: HashMap::new(), - network: None, - expiration: ExecExpiration::Cancellation(CancellationToken::new()), - sandbox: SandboxType::None, - windows_sandbox_level: WindowsSandboxLevel::Disabled, - windows_sandbox_private_desktop: false, - sandbox_permissions: codex_core::sandboxing::SandboxPermissions::UseDefault, - sandbox_policy: sandbox_policy.clone(), - file_system_sandbox_policy: FileSystemSandboxPolicy::from(&sandbox_policy), - network_sandbox_policy: NetworkSandboxPolicy::from(&sandbox_policy), - justification: None, - arg0: None, - }), + exec_request: ExecRequest::new( + vec!["sh".to_string(), "-lc".to_string(), "sleep 30".to_string()], + PathBuf::from("."), + HashMap::new(), + None, + ExecExpiration::Cancellation(CancellationToken::new()), + SandboxType::None, + WindowsSandboxLevel::Disabled, + false, + codex_core::sandboxing::SandboxPermissions::UseDefault, + sandbox_policy.clone(), + FileSystemSandboxPolicy::from(&sandbox_policy), + NetworkSandboxPolicy::from(&sandbox_policy), + None, + None, + ), started_network_proxy: None, tty: false, stream_stdin: false, diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs index 1b479fe02b7..9b839de0161 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -82,42 +82,24 @@ pub struct ExecRequest { pub arg0: Option, } -pub struct ExecRequestArgs { - pub command: Vec, - pub cwd: PathBuf, - pub env: HashMap, - pub network: Option, - pub expiration: ExecExpiration, - pub sandbox: SandboxType, - pub windows_sandbox_level: WindowsSandboxLevel, - pub windows_sandbox_private_desktop: bool, - pub sandbox_permissions: SandboxPermissions, - pub sandbox_policy: SandboxPolicy, - pub file_system_sandbox_policy: FileSystemSandboxPolicy, - pub network_sandbox_policy: NetworkSandboxPolicy, - pub justification: Option, - pub arg0: Option, -} - impl ExecRequest { - pub fn new(args: ExecRequestArgs) -> Self { - let ExecRequestArgs { - command, - cwd, - env, - network, - expiration, - sandbox, - windows_sandbox_level, - windows_sandbox_private_desktop, - sandbox_permissions, - sandbox_policy, - file_system_sandbox_policy, - network_sandbox_policy, - justification, - arg0, - } = args; - + #[allow(clippy::too_many_arguments)] + pub fn new( + command: Vec, + cwd: PathBuf, + env: HashMap, + network: Option, + expiration: ExecExpiration, + sandbox: SandboxType, + windows_sandbox_level: WindowsSandboxLevel, + windows_sandbox_private_desktop: bool, + sandbox_permissions: SandboxPermissions, + sandbox_policy: SandboxPolicy, + file_system_sandbox_policy: FileSystemSandboxPolicy, + network_sandbox_policy: NetworkSandboxPolicy, + justification: Option, + arg0: Option, + ) -> Self { Self { command, cwd, From 4480c006cd9b087aafdce7b417ba1b144d47d8e3 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Wed, 18 Mar 2026 01:12:08 -0700 Subject: [PATCH 13/17] style: annotate exec request literal args --- codex-rs/app-server/src/command_exec.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/codex-rs/app-server/src/command_exec.rs b/codex-rs/app-server/src/command_exec.rs index 2b4be9a8241..cf07d3c1e32 100644 --- a/codex-rs/app-server/src/command_exec.rs +++ b/codex-rs/app-server/src/command_exec.rs @@ -731,17 +731,17 @@ mod tests { vec!["cmd".to_string()], PathBuf::from("."), HashMap::new(), - None, + /*network*/ None, ExecExpiration::DefaultTimeout, SandboxType::WindowsRestrictedToken, WindowsSandboxLevel::Disabled, - false, + /*windows_sandbox_private_desktop*/ false, codex_core::sandboxing::SandboxPermissions::UseDefault, sandbox_policy.clone(), FileSystemSandboxPolicy::from(&sandbox_policy), NetworkSandboxPolicy::from(&sandbox_policy), - None, - None, + /*justification*/ None, + /*arg0*/ None, ) } @@ -843,17 +843,17 @@ mod tests { vec!["sh".to_string(), "-lc".to_string(), "sleep 30".to_string()], PathBuf::from("."), HashMap::new(), - None, + /*network*/ None, ExecExpiration::Cancellation(CancellationToken::new()), SandboxType::None, WindowsSandboxLevel::Disabled, - false, + /*windows_sandbox_private_desktop*/ false, codex_core::sandboxing::SandboxPermissions::UseDefault, sandbox_policy.clone(), FileSystemSandboxPolicy::from(&sandbox_policy), NetworkSandboxPolicy::from(&sandbox_policy), - None, - None, + /*justification*/ None, + /*arg0*/ None, ), started_network_proxy: None, tty: false, From 1cbecd2e29dc4493a9ce2885bdb6037a87b95d15 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Tue, 24 Mar 2026 18:47:10 -0700 Subject: [PATCH 14/17] fix: normalize windows restricted-token overlay roots --- codex-rs/core/src/exec.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index 0f4dfbfda6f..89767d1a7fe 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -973,11 +973,11 @@ pub(crate) fn resolve_windows_restricted_token_filesystem_overlay( file_system_sandbox_policy.get_writable_roots_with_cwd(sandbox_policy_cwd); let legacy_root_paths: BTreeSet = legacy_writable_roots .iter() - .map(|root| root.root.to_path_buf()) + .map(|root| normalize_windows_overlay_path(root.root.as_path())) .collect(); let split_root_paths: BTreeSet = split_writable_roots .iter() - .map(|root| root.root.to_path_buf()) + .map(|root| normalize_windows_overlay_path(root.root.as_path())) .collect(); if legacy_root_paths != split_root_paths { @@ -1006,10 +1006,10 @@ pub(crate) fn resolve_windows_restricted_token_filesystem_overlay( let mut additional_deny_write_paths = BTreeSet::new(); for split_root in &split_writable_roots { - let Some(legacy_root) = legacy_writable_roots - .iter() - .find(|candidate| candidate.root == split_root.root) - else { + let Some(legacy_root) = legacy_writable_roots.iter().find(|candidate| { + normalize_windows_overlay_path(candidate.root.as_path()) + == normalize_windows_overlay_path(split_root.root.as_path()) + }) else { return Err( "windows unelevated restricted-token sandbox cannot enforce split writable root sets directly; refusing to run unsandboxed" .to_string(), @@ -1036,6 +1036,10 @@ pub(crate) fn resolve_windows_restricted_token_filesystem_overlay( })) } +fn normalize_windows_overlay_path(path: &Path) -> PathBuf { + dunce::simplified(path).to_path_buf() +} + /// Consumes the output of a child process according to the configured capture /// policy. async fn consume_output( From ac7308779c1b3c72782b6d3787bc1224e24c4f2d Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Tue, 24 Mar 2026 20:11:56 -0700 Subject: [PATCH 15/17] fix: simplify windows overlay deny paths --- codex-rs/core/src/exec.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index 89767d1a7fe..4248103870d 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -1022,7 +1022,7 @@ pub(crate) fn resolve_windows_restricted_token_filesystem_overlay( .iter() .any(|candidate| candidate == read_only_subpath) { - additional_deny_write_paths.insert(read_only_subpath.to_path_buf()); + additional_deny_write_paths.insert(normalize_windows_overlay_path(read_only_subpath.as_path())); } } } From b11a0957c4dae8637223d18489dc9fce5f593b54 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Tue, 24 Mar 2026 21:10:25 -0700 Subject: [PATCH 16/17] fix: format windows overlay path normalization --- codex-rs/core/src/exec.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index 4248103870d..c3cba357183 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -1022,7 +1022,8 @@ pub(crate) fn resolve_windows_restricted_token_filesystem_overlay( .iter() .any(|candidate| candidate == read_only_subpath) { - additional_deny_write_paths.insert(normalize_windows_overlay_path(read_only_subpath.as_path())); + additional_deny_write_paths + .insert(normalize_windows_overlay_path(read_only_subpath.as_path())); } } } From f7927159088ca0641fd725fe740f325f54a3dd5e Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Tue, 24 Mar 2026 22:14:06 -0700 Subject: [PATCH 17/17] refactor: tighten windows overlay path type --- codex-rs/core/src/exec.rs | 31 ++++++++++++++++++++++--------- codex-rs/core/src/exec_tests.rs | 5 ++++- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index c3cba357183..a4399ddc46b 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -42,6 +42,7 @@ use codex_sandboxing::SandboxManager; use codex_sandboxing::SandboxTransformRequest; use codex_sandboxing::SandboxType; use codex_sandboxing::SandboxablePreference; +use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_pty::DEFAULT_OUTPUT_BYTES_CAP; use codex_utils_pty::process_group::kill_child_process_group; @@ -100,7 +101,7 @@ pub struct ExecParams { /// sandbox semantics. #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct WindowsRestrictedTokenFilesystemOverlay { - pub(crate) additional_deny_write_paths: Vec, + pub(crate) additional_deny_write_paths: Vec, } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] @@ -481,7 +482,13 @@ async fn exec_windows_sandbox( let sandbox_level = windows_sandbox_level; let use_elevated = matches!(sandbox_level, WindowsSandboxLevel::Elevated); let additional_deny_write_paths = windows_restricted_token_filesystem_overlay - .map(|overlay| overlay.additional_deny_write_paths.clone()) + .map(|overlay| { + overlay + .additional_deny_write_paths + .iter() + .map(AbsolutePathBuf::to_path_buf) + .collect::>() + }) .unwrap_or_default(); let spawn_res = tokio::task::spawn_blocking(move || { if use_elevated { @@ -974,11 +981,11 @@ pub(crate) fn resolve_windows_restricted_token_filesystem_overlay( let legacy_root_paths: BTreeSet = legacy_writable_roots .iter() .map(|root| normalize_windows_overlay_path(root.root.as_path())) - .collect(); + .collect::>()?; let split_root_paths: BTreeSet = split_writable_roots .iter() .map(|root| normalize_windows_overlay_path(root.root.as_path())) - .collect(); + .collect::>()?; if legacy_root_paths != split_root_paths { return Err( @@ -1006,9 +1013,10 @@ pub(crate) fn resolve_windows_restricted_token_filesystem_overlay( let mut additional_deny_write_paths = BTreeSet::new(); for split_root in &split_writable_roots { + let split_root_path = normalize_windows_overlay_path(split_root.root.as_path())?; let Some(legacy_root) = legacy_writable_roots.iter().find(|candidate| { normalize_windows_overlay_path(candidate.root.as_path()) - == normalize_windows_overlay_path(split_root.root.as_path()) + .is_ok_and(|candidate_path| candidate_path == split_root_path) }) else { return Err( "windows unelevated restricted-token sandbox cannot enforce split writable root sets directly; refusing to run unsandboxed" @@ -1023,7 +1031,7 @@ pub(crate) fn resolve_windows_restricted_token_filesystem_overlay( .any(|candidate| candidate == read_only_subpath) { additional_deny_write_paths - .insert(normalize_windows_overlay_path(read_only_subpath.as_path())); + .insert(normalize_windows_overlay_path(read_only_subpath.as_path())?); } } } @@ -1033,12 +1041,17 @@ pub(crate) fn resolve_windows_restricted_token_filesystem_overlay( } Ok(Some(WindowsRestrictedTokenFilesystemOverlay { - additional_deny_write_paths: additional_deny_write_paths.into_iter().collect(), + additional_deny_write_paths: additional_deny_write_paths + .into_iter() + .map(|path| AbsolutePathBuf::from_absolute_path(path).map_err(|err| err.to_string())) + .collect::>()?, })) } -fn normalize_windows_overlay_path(path: &Path) -> PathBuf { - dunce::simplified(path).to_path_buf() +fn normalize_windows_overlay_path(path: &Path) -> std::result::Result { + AbsolutePathBuf::from_absolute_path(dunce::simplified(path)) + .map(AbsolutePathBuf::into_path_buf) + .map_err(|err| err.to_string()) } /// Consumes the output of a child process according to the configured capture diff --git a/codex-rs/core/src/exec_tests.rs b/codex-rs/core/src/exec_tests.rs index c2702ffae9d..074db95e1cb 100644 --- a/codex-rs/core/src/exec_tests.rs +++ b/codex-rs/core/src/exec_tests.rs @@ -605,7 +605,10 @@ fn windows_restricted_token_supports_full_read_split_write_read_carveouts() { WindowsSandboxLevel::RestrictedToken, ), Ok(Some(WindowsRestrictedTokenFilesystemOverlay { - additional_deny_write_paths: vec![docs], + additional_deny_write_paths: vec![ + codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(&docs) + .expect("absolute docs"), + ], })) ); }