From 89616178a64a2b8d7463ff161bf7774b2c68e354 Mon Sep 17 00:00:00 2001 From: David Wiesen Date: Thu, 23 Apr 2026 15:07:17 -0700 Subject: [PATCH] fix(windows): clear stale protected ACLs in full access --- codex-rs/core/src/exec.rs | 31 +++++++++++++ codex-rs/core/src/windows_sandbox.rs | 16 +++++++ codex-rs/windows-sandbox-rs/src/lib.rs | 2 + .../src/setup_orchestrator.rs | 44 +++++++++++++++++++ 4 files changed, 93 insertions(+) diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index fde8782f08..fe5c86fe40 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -350,6 +350,13 @@ pub(crate) async fn execute_exec_request( arg0, }; + #[cfg(target_os = "windows")] + maybe_cleanup_stale_windows_sandbox_denies( + sandbox_policy, + params.windows_sandbox_level, + ¶ms.cwd, + )?; + let start = Instant::now(); let raw_output_result = exec( params, @@ -379,6 +386,30 @@ fn extract_create_process_as_user_error_code(err: &str) -> Option { } } +#[cfg(target_os = "windows")] +fn maybe_cleanup_stale_windows_sandbox_denies( + sandbox_policy: &SandboxPolicy, + windows_sandbox_level: WindowsSandboxLevel, + cwd: &Path, +) -> Result<()> { + if !matches!(windows_sandbox_level, WindowsSandboxLevel::Elevated) + || !matches!( + sandbox_policy, + SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } + ) + { + return Ok(()); + } + + let codex_home = crate::config::find_codex_home().map_err(|err| { + CodexErr::Io(io::Error::other(format!( + "windows sandbox: failed to resolve codex_home for ACL cleanup: {err}" + ))) + })?; + crate::windows_sandbox::cleanup_stale_protected_write_denies(cwd, codex_home.as_ref()) + .map_err(|err| CodexErr::Io(io::Error::other(format!("windows sandbox: {err}")))) +} + #[cfg(target_os = "windows")] fn windowsapps_path_kind(path: &str) -> &'static str { let lower = path.to_ascii_lowercase(); diff --git a/codex-rs/core/src/windows_sandbox.rs b/codex-rs/core/src/windows_sandbox.rs index ee3a040a69..1df8ba3f5b 100644 --- a/codex-rs/core/src/windows_sandbox.rs +++ b/codex-rs/core/src/windows_sandbox.rs @@ -192,6 +192,22 @@ pub fn run_elevated_setup( ) } +#[cfg(target_os = "windows")] +pub fn cleanup_stale_protected_write_denies( + command_cwd: &Path, + codex_home: &Path, +) -> anyhow::Result<()> { + codex_windows_sandbox::cleanup_stale_protected_write_denies(command_cwd, codex_home) +} + +#[cfg(not(target_os = "windows"))] +pub fn cleanup_stale_protected_write_denies( + _command_cwd: &Path, + _codex_home: &Path, +) -> anyhow::Result<()> { + anyhow::bail!("Windows sandbox ACL cleanup is only supported on Windows") +} + #[cfg(not(target_os = "windows"))] pub fn run_elevated_setup( _policy: &SandboxPolicy, diff --git a/codex-rs/windows-sandbox-rs/src/lib.rs b/codex-rs/windows-sandbox-rs/src/lib.rs index b0de8c2e02..c93a1c170d 100644 --- a/codex-rs/windows-sandbox-rs/src/lib.rs +++ b/codex-rs/windows-sandbox-rs/src/lib.rs @@ -116,6 +116,8 @@ pub use setup::SandboxSetupRequest; #[cfg(target_os = "windows")] pub use setup::SetupRootOverrides; #[cfg(target_os = "windows")] +pub use setup::cleanup_stale_protected_write_denies; +#[cfg(target_os = "windows")] pub use setup::run_elevated_setup; #[cfg(target_os = "windows")] pub use setup::run_setup_refresh; diff --git a/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs b/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs index f6583e0de4..424f4e8817 100644 --- a/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs +++ b/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs @@ -10,11 +10,16 @@ use std::path::PathBuf; use std::process::Command; use std::process::Stdio; +use crate::acl::revoke_ace; use crate::allow::AllowDenyPaths; use crate::allow::compute_allow_paths; +use crate::cap::load_or_create_cap_sids; +use crate::cap::workspace_cap_sid_for_cwd; +use crate::convert_string_sid_to_sid; use crate::helper_materialization::helper_bin_dir; use crate::logging::log_note; use crate::path_normalization::canonical_path_key; +use crate::path_normalization::canonicalize_path; use crate::policy::SandboxPolicy; use crate::setup_error::SetupErrorCode; use crate::setup_error::SetupFailure; @@ -29,6 +34,8 @@ use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; use windows_sys::Win32::Foundation::CloseHandle; use windows_sys::Win32::Foundation::GetLastError; +use windows_sys::Win32::Foundation::HLOCAL; +use windows_sys::Win32::Foundation::LocalFree; use windows_sys::Win32::Security::AllocateAndInitializeSid; use windows_sys::Win32::Security::CheckTokenMembership; use windows_sys::Win32::Security::FreeSid; @@ -142,6 +149,43 @@ pub fn run_setup_refresh_with_extra_read_roots( ) } +pub fn cleanup_stale_protected_write_denies(command_cwd: &Path, codex_home: &Path) -> Result<()> { + let caps = load_or_create_cap_sids(codex_home)?; + let cap_psid = unsafe { + convert_string_sid_to_sid(&caps.workspace) + .ok_or_else(|| anyhow!("convert capability SID {} failed", caps.workspace))? + }; + let workspace_sid = workspace_cap_sid_for_cwd(codex_home, command_cwd)?; + let workspace_psid = unsafe { + convert_string_sid_to_sid(&workspace_sid) + .ok_or_else(|| anyhow!("convert workspace capability SID {workspace_sid} failed"))? + }; + let canonical_command_cwd = canonicalize_path(command_cwd); + + for protected_subdir in [".git", ".codex", ".agents"] { + let protected_path = canonical_command_cwd.join(protected_subdir); + if !protected_path.exists() { + continue; + } + + unsafe { + revoke_ace(&protected_path, workspace_psid); + revoke_ace(&protected_path, cap_psid); + } + } + + unsafe { + if !cap_psid.is_null() { + LocalFree(cap_psid as HLOCAL); + } + if !workspace_psid.is_null() { + LocalFree(workspace_psid as HLOCAL); + } + } + + Ok(()) +} + fn run_setup_refresh_inner( request: SandboxSetupRequest<'_>, overrides: SetupRootOverrides,