Compare commits

...

14 Commits

Author SHA1 Message Date
Eva Wong
b4774591d3 Inject Git safe directory for Windows legacy sandbox 2026-05-04 10:20:51 -07:00
Eva Wong
d2df5e4f64 Grant Windows legacy Git read roots 2026-05-04 10:20:51 -07:00
Eva Wong
0376e03e53 Add Windows legacy Git read root helpers 2026-05-04 10:20:51 -07:00
Eva Wong
3e52598b46 Allow Windows sandbox Git signal pipes 2026-05-04 10:20:51 -07:00
Eva Wong
7efd618304 Use Windows metadata targets in debug sandbox 2026-05-04 10:20:51 -07:00
Eva Wong
83c952f1a0 Deny Windows protected metadata symlink targets 2026-05-04 10:20:51 -07:00
Eva Wong
56c383d9bf Enforce Windows protected metadata targets 2026-05-04 10:20:50 -07:00
Eva Wong
f21dcba492 Thread Windows metadata targets through sessions 2026-05-04 10:20:50 -07:00
Eva Wong
72abfcabcc Pass Windows metadata targets to direct exec 2026-05-04 10:20:50 -07:00
Eva Wong
d4a25eaa37 Thread Windows metadata targets through setup request 2026-05-04 10:20:50 -07:00
Eva Wong
1fef4bd159 Plan Windows metadata targets from filesystem policy 2026-05-04 10:20:50 -07:00
Eva Wong
853a8c53c2 Add Windows metadata enforcement guard 2026-05-04 10:20:50 -07:00
Eva Wong
21fd94d594 Add Windows metadata setup target type 2026-05-04 10:20:50 -07:00
Eva Wong
c4a2a4475e Add Windows metadata adapter target type 2026-05-04 10:20:50 -07:00
20 changed files with 1186 additions and 48 deletions

View File

@@ -360,6 +360,11 @@ async fn run_command_under_windows_session(
WindowsSandboxLevel::from_config(config),
WindowsSandboxLevel::Elevated
);
let file_system_sandbox_policy = config.permissions.file_system_sandbox_policy();
let protected_metadata_targets = windows_debug_protected_metadata_targets(
&file_system_sandbox_policy,
sandbox_policy_cwd.as_path(),
);
let spawned = if use_elevated {
spawn_windows_sandbox_session_elevated(
@@ -372,6 +377,7 @@ async fn run_command_under_windows_session(
None,
/*tty*/ false,
/*stdin_open*/ true,
&protected_metadata_targets,
config.permissions.windows_sandbox_private_desktop,
)
.await
@@ -386,6 +392,7 @@ async fn run_command_under_windows_session(
None,
/*tty*/ false,
/*stdin_open*/ true,
&protected_metadata_targets,
config.permissions.windows_sandbox_private_desktop,
)
.await
@@ -459,6 +466,31 @@ async fn run_command_under_windows_session(
std::process::exit(exit_code);
}
#[cfg(target_os = "windows")]
fn windows_debug_protected_metadata_targets(
file_system_sandbox_policy: &codex_protocol::permissions::FileSystemSandboxPolicy,
cwd: &std::path::Path,
) -> Vec<codex_windows_sandbox::ProtectedMetadataTarget> {
let mut targets = Vec::new();
for writable_root in file_system_sandbox_policy.get_writable_roots_with_cwd(cwd) {
for metadata_name in writable_root.protected_metadata_names {
let path = writable_root.root.join(metadata_name);
let mode = if std::fs::symlink_metadata(path.as_path()).is_ok() {
codex_windows_sandbox::ProtectedMetadataMode::ExistingDeny
} else {
codex_windows_sandbox::ProtectedMetadataMode::MissingCreationMonitor
};
targets.push(codex_windows_sandbox::ProtectedMetadataTarget {
path: path.as_path().to_path_buf(),
mode,
});
}
}
targets.sort_by(|a, b| a.path.cmp(&b.path));
targets.dedup_by(|a, b| a.path == b.path && a.mode == b.mode);
targets
}
async fn spawn_debug_sandbox_child(
program: PathBuf,
args: Vec<String>,

View File

@@ -95,7 +95,8 @@ pub struct ExecParams {
pub arg0: Option<String>,
}
/// Resolved filesystem overrides for the Windows sandbox backends.
/// Layer: Windows adapter layer. Resolved filesystem overrides for the Windows
/// sandbox backends.
///
/// The unelevated restricted-token backend only consumes extra deny-write
/// carveouts on top of the legacy `WorkspaceWrite` allow set. The elevated
@@ -109,6 +110,24 @@ pub(crate) struct WindowsSandboxFilesystemOverrides {
pub(crate) read_roots_include_platform_defaults: bool,
pub(crate) write_roots_override: Option<Vec<PathBuf>>,
pub(crate) additional_deny_write_paths: Vec<AbsolutePathBuf>,
pub(crate) protected_metadata_targets: Vec<WindowsProtectedMetadataTarget>,
}
/// Layer: Windows adapter layer. This is the Windows projection of
/// `WritableRoot::protected_metadata_names` from `FileSystemSandboxPolicy`.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) struct WindowsProtectedMetadataTarget {
pub(crate) path: AbsolutePathBuf,
pub(crate) mode: WindowsProtectedMetadataMode,
}
/// Layer: Windows adapter layer. The enforcement layer needs to know why a
/// protected metadata path is absent instead of treating every missing path as
/// an existing filesystem object.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum WindowsProtectedMetadataMode {
ExistingDeny,
MissingCreationMonitor,
}
fn windows_sandbox_uses_elevated_backend(
@@ -388,39 +407,18 @@ pub fn build_exec_request(
ExecRequest::from_sandbox_exec_request(request, options, windows_sandbox_policy_cwd)
})
.map_err(CodexErr::from)?;
let use_windows_elevated_backend = windows_sandbox_uses_elevated_backend(
exec_req.windows_sandbox_level,
exec_req.network.is_some(),
);
let sandbox_policy = exec_req.compatibility_sandbox_policy();
exec_req.windows_sandbox_filesystem_overrides = if use_windows_elevated_backend {
resolve_windows_elevated_filesystem_overrides(
exec_req.sandbox,
&sandbox_policy,
&exec_req.file_system_sandbox_policy,
exec_req.network_sandbox_policy,
sandbox_cwd,
use_windows_elevated_backend,
)
} else {
resolve_windows_restricted_token_filesystem_overrides(
exec_req.sandbox,
&sandbox_policy,
&exec_req.file_system_sandbox_policy,
exec_req.network_sandbox_policy,
sandbox_cwd,
exec_req.windows_sandbox_level,
)
}
.map_err(CodexErr::UnsupportedOperation)?;
ensure_windows_sandbox_filesystem_overrides(&mut exec_req)
.map_err(CodexErr::UnsupportedOperation)?;
Ok(exec_req)
}
pub(crate) async fn execute_exec_request(
exec_request: ExecRequest,
mut exec_request: ExecRequest,
stdout_stream: Option<StdoutStream>,
after_spawn: Option<Box<dyn FnOnce() + Send>>,
) -> Result<ExecToolCallOutput> {
ensure_windows_sandbox_filesystem_overrides(&mut exec_request)
.map_err(CodexErr::UnsupportedOperation)?;
let sandbox_policy = exec_request.compatibility_sandbox_policy();
let ExecRequest {
command,
@@ -470,6 +468,36 @@ pub(crate) async fn execute_exec_request(
finalize_exec_result(raw_output_result, sandbox, duration)
}
pub(crate) fn ensure_windows_sandbox_filesystem_overrides(
exec_req: &mut ExecRequest,
) -> std::result::Result<(), String> {
let use_windows_elevated_backend = windows_sandbox_uses_elevated_backend(
exec_req.windows_sandbox_level,
exec_req.network.is_some(),
);
let sandbox_policy = exec_req.compatibility_sandbox_policy();
exec_req.windows_sandbox_filesystem_overrides = if use_windows_elevated_backend {
resolve_windows_elevated_filesystem_overrides(
exec_req.sandbox,
&sandbox_policy,
&exec_req.file_system_sandbox_policy,
exec_req.network_sandbox_policy,
&exec_req.windows_sandbox_policy_cwd,
use_windows_elevated_backend,
)
} else {
resolve_windows_restricted_token_filesystem_overrides(
exec_req.sandbox,
&sandbox_policy,
&exec_req.file_system_sandbox_policy,
exec_req.network_sandbox_policy,
&exec_req.windows_sandbox_policy_cwd,
exec_req.windows_sandbox_level,
)
}?;
Ok(())
}
async fn get_raw_output_result(
params: ExecParams,
network_sandbox_policy: NetworkSandboxPolicy,
@@ -628,6 +656,28 @@ async fn exec_windows_sandbox(
.collect::<Vec<_>>()
})
.unwrap_or_default();
let protected_metadata_targets = windows_sandbox_filesystem_overrides
.map(|overrides| {
overrides
.protected_metadata_targets
.iter()
.map(|target| {
let mode = match target.mode {
WindowsProtectedMetadataMode::ExistingDeny => {
codex_windows_sandbox::ProtectedMetadataMode::ExistingDeny
}
WindowsProtectedMetadataMode::MissingCreationMonitor => {
codex_windows_sandbox::ProtectedMetadataMode::MissingCreationMonitor
}
};
codex_windows_sandbox::ProtectedMetadataTarget {
path: target.path.to_path_buf(),
mode,
}
})
.collect::<Vec<_>>()
})
.unwrap_or_default();
let spawn_res = tokio::task::spawn_blocking(move || {
if use_elevated {
run_windows_sandbox_capture_elevated(
@@ -646,6 +696,7 @@ async fn exec_windows_sandbox(
elevated_read_roots_include_platform_defaults,
write_roots_override: elevated_write_roots_override.as_deref(),
deny_write_paths_override: &elevated_deny_write_paths,
protected_metadata_targets: &protected_metadata_targets,
},
)
} else {
@@ -658,6 +709,7 @@ async fn exec_windows_sandbox(
env,
timeout_ms,
&additional_deny_write_paths,
&protected_metadata_targets,
windows_sandbox_private_desktop,
)
}
@@ -1131,7 +1183,9 @@ pub(crate) fn resolve_windows_restricted_token_filesystem_overrides(
}
}
if additional_deny_write_paths.is_empty() {
let protected_metadata_targets = windows_protected_metadata_targets(&split_writable_roots)?;
if additional_deny_write_paths.is_empty() && protected_metadata_targets.is_empty() {
return Ok(None);
}
@@ -1143,6 +1197,7 @@ pub(crate) fn resolve_windows_restricted_token_filesystem_overrides(
.into_iter()
.map(|path| AbsolutePathBuf::from_absolute_path(path).map_err(|err| err.to_string()))
.collect::<std::result::Result<_, _>>()?,
protected_metadata_targets,
}))
}
@@ -1263,9 +1318,12 @@ pub(crate) fn resolve_windows_elevated_filesystem_overrides(
Vec::new()
};
let protected_metadata_targets = windows_protected_metadata_targets(&split_writable_roots)?;
if read_roots_override.is_none()
&& write_roots_override.is_none()
&& additional_deny_write_paths.is_empty()
&& protected_metadata_targets.is_empty()
{
return Ok(None);
}
@@ -1276,9 +1334,36 @@ pub(crate) fn resolve_windows_elevated_filesystem_overrides(
read_roots_override,
write_roots_override,
additional_deny_write_paths,
protected_metadata_targets,
}))
}
fn windows_protected_metadata_targets(
writable_roots: &[codex_protocol::protocol::WritableRoot],
) -> std::result::Result<Vec<WindowsProtectedMetadataTarget>, String> {
let mut targets = BTreeSet::new();
for writable_root in writable_roots {
for metadata_name in &writable_root.protected_metadata_names {
let path =
normalize_windows_override_path(writable_root.root.join(metadata_name).as_path())?;
let path = AbsolutePathBuf::from_absolute_path(path).map_err(|err| err.to_string())?;
targets.insert(WindowsProtectedMetadataTarget {
mode: windows_protected_metadata_mode(&path),
path,
});
}
}
Ok(targets.into_iter().collect())
}
fn windows_protected_metadata_mode(path: &AbsolutePathBuf) -> WindowsProtectedMetadataMode {
if std::fs::symlink_metadata(path.as_path()).is_ok() {
return WindowsProtectedMetadataMode::ExistingDeny;
}
WindowsProtectedMetadataMode::MissingCreationMonitor
}
fn has_reopened_writable_descendant(
writable_roots: &[codex_protocol::protocol::WritableRoot],
) -> bool {

View File

@@ -663,6 +663,20 @@ fn windows_restricted_token_supports_full_read_split_write_read_carveouts() {
read_roots_include_platform_defaults: false,
write_roots_override: None,
additional_deny_write_paths: expected_deny_write_paths,
protected_metadata_targets: vec![
WindowsProtectedMetadataTarget {
path: cwd.join(".agents"),
mode: WindowsProtectedMetadataMode::MissingCreationMonitor,
},
WindowsProtectedMetadataTarget {
path: cwd.join(".codex"),
mode: WindowsProtectedMetadataMode::MissingCreationMonitor,
},
WindowsProtectedMetadataTarget {
path: cwd.join(".git"),
mode: WindowsProtectedMetadataMode::MissingCreationMonitor,
},
],
}))
);
}
@@ -700,6 +714,7 @@ fn windows_elevated_supports_split_restricted_read_roots() {
read_roots_include_platform_defaults: false,
write_roots_override: None,
additional_deny_write_paths: vec![],
protected_metadata_targets: vec![],
}))
);
}
@@ -707,6 +722,9 @@ fn windows_elevated_supports_split_restricted_read_roots() {
#[test]
fn windows_elevated_supports_split_write_read_carveouts() {
let temp_dir = tempfile::TempDir::new().expect("tempdir");
let expected_root = dunce::canonicalize(temp_dir.path())
.expect("canonical temp dir")
.abs();
let docs = temp_dir.path().join("docs");
std::fs::create_dir_all(&docs).expect("create docs");
let expected_docs = dunce::canonicalize(&docs).expect("canonical docs");
@@ -757,6 +775,146 @@ fn windows_elevated_supports_split_write_read_carveouts() {
codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(expected_docs)
.expect("absolute docs"),
],
protected_metadata_targets: vec![
WindowsProtectedMetadataTarget {
path: expected_root.join(".agents"),
mode: WindowsProtectedMetadataMode::MissingCreationMonitor,
},
WindowsProtectedMetadataTarget {
path: expected_root.join(".codex"),
mode: WindowsProtectedMetadataMode::MissingCreationMonitor,
},
WindowsProtectedMetadataTarget {
path: expected_root.join(".git"),
mode: WindowsProtectedMetadataMode::MissingCreationMonitor,
},
],
}))
);
}
#[test]
fn windows_metadata_plan_marks_existing_metadata_for_deny() {
let temp_dir = tempfile::TempDir::new().expect("tempdir");
let cwd = dunce::canonicalize(temp_dir.path())
.expect("canonical temp dir")
.abs();
std::fs::create_dir_all(cwd.join(".git").as_path()).expect("create .git");
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
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::project_roots(
/*subpath*/ None,
),
},
access: codex_protocol::permissions::FileSystemAccessMode::Write,
},
]);
assert_eq!(
resolve_windows_elevated_filesystem_overrides(
SandboxType::WindowsRestrictedToken,
&policy,
&file_system_policy,
NetworkSandboxPolicy::Restricted,
&cwd,
/*use_windows_elevated_backend*/ true,
),
Ok(Some(WindowsSandboxFilesystemOverrides {
read_roots_override: None,
read_roots_include_platform_defaults: false,
write_roots_override: None,
additional_deny_write_paths: vec![],
protected_metadata_targets: vec![
WindowsProtectedMetadataTarget {
path: cwd.join(".agents"),
mode: WindowsProtectedMetadataMode::MissingCreationMonitor,
},
WindowsProtectedMetadataTarget {
path: cwd.join(".codex"),
mode: WindowsProtectedMetadataMode::MissingCreationMonitor,
},
WindowsProtectedMetadataTarget {
path: cwd.join(".git"),
mode: WindowsProtectedMetadataMode::ExistingDeny,
},
],
}))
);
}
#[test]
fn windows_metadata_plan_does_not_materialize_nested_missing_git() {
let temp_dir = tempfile::TempDir::new().expect("tempdir");
let repo = dunce::canonicalize(temp_dir.path())
.expect("canonical temp dir")
.abs();
std::fs::create_dir_all(repo.join(".git").as_path()).expect("create parent .git");
let cwd = repo.join("child");
std::fs::create_dir_all(cwd.as_path()).expect("create child workspace");
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
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::project_roots(
/*subpath*/ None,
),
},
access: codex_protocol::permissions::FileSystemAccessMode::Write,
},
]);
assert_eq!(
resolve_windows_elevated_filesystem_overrides(
SandboxType::WindowsRestrictedToken,
&policy,
&file_system_policy,
NetworkSandboxPolicy::Restricted,
&cwd,
/*use_windows_elevated_backend*/ true,
),
Ok(Some(WindowsSandboxFilesystemOverrides {
read_roots_override: None,
read_roots_include_platform_defaults: false,
write_roots_override: None,
additional_deny_write_paths: vec![],
protected_metadata_targets: vec![
WindowsProtectedMetadataTarget {
path: cwd.join(".agents"),
mode: WindowsProtectedMetadataMode::MissingCreationMonitor,
},
WindowsProtectedMetadataTarget {
path: cwd.join(".codex"),
mode: WindowsProtectedMetadataMode::MissingCreationMonitor,
},
WindowsProtectedMetadataTarget {
path: cwd.join(".git"),
mode: WindowsProtectedMetadataMode::MissingCreationMonitor,
},
],
}))
);
}

View File

@@ -41,7 +41,7 @@ pub(crate) struct ExecServerEnvConfig {
pub(crate) local_policy_env: HashMap<String, String>,
}
#[derive(Debug)]
#[derive(Clone, Debug)]
pub struct ExecRequest {
pub command: Vec<String>,
pub cwd: AbsolutePathBuf,

View File

@@ -11,6 +11,8 @@ use tokio::time::Duration;
use tokio::time::Instant;
use tokio_util::sync::CancellationToken;
#[cfg(target_os = "windows")]
use crate::exec::WindowsProtectedMetadataMode;
use crate::exec_env::CODEX_THREAD_ID_ENV_VAR;
use crate::exec_env::create_env;
use crate::exec_policy::ExecApprovalRequest;
@@ -163,6 +165,45 @@ fn exec_server_params_for_request(
}
}
fn prepare_exec_request_for_open_session(
request: &ExecRequest,
) -> Result<ExecRequest, UnifiedExecError> {
let mut request = request.clone();
crate::exec::ensure_windows_sandbox_filesystem_overrides(&mut request)
.map_err(UnifiedExecError::create_process)?;
Ok(request)
}
#[cfg(target_os = "windows")]
fn protected_metadata_targets_for_windows_session(
request: &ExecRequest,
) -> Vec<codex_windows_sandbox::ProtectedMetadataTarget> {
request
.windows_sandbox_filesystem_overrides
.as_ref()
.map(|overrides| {
overrides
.protected_metadata_targets
.iter()
.map(|target| {
let mode = match target.mode {
WindowsProtectedMetadataMode::ExistingDeny => {
codex_windows_sandbox::ProtectedMetadataMode::ExistingDeny
}
WindowsProtectedMetadataMode::MissingCreationMonitor => {
codex_windows_sandbox::ProtectedMetadataMode::MissingCreationMonitor
}
};
codex_windows_sandbox::ProtectedMetadataTarget {
path: target.path.to_path_buf(),
mode,
}
})
.collect()
})
.unwrap_or_default()
}
/// Borrowed process state prepared for a `write_stdin` or poll operation.
struct PreparedProcessHandles {
process: Arc<UnifiedExecProcess>,
@@ -873,6 +914,7 @@ impl UnifiedExecProcessManager {
mut spawn_lifecycle: SpawnLifecycleHandle,
environment: &codex_exec_server::Environment,
) -> Result<UnifiedExecProcess, UnifiedExecError> {
let request = prepare_exec_request_for_open_session(request)?;
let inherited_fds = spawn_lifecycle.inherited_fds();
#[cfg(target_os = "windows")]
@@ -888,6 +930,8 @@ impl UnifiedExecProcessManager {
"windows sandbox: failed to resolve codex_home: {err}"
))
})?;
let protected_metadata_targets =
protected_metadata_targets_for_windows_session(&request);
let spawned = match request.windows_sandbox_level {
codex_protocol::config_types::WindowsSandboxLevel::Elevated => {
codex_windows_sandbox::spawn_windows_sandbox_session_elevated(
@@ -900,6 +944,7 @@ impl UnifiedExecProcessManager {
None,
tty,
tty,
&protected_metadata_targets,
request.windows_sandbox_private_desktop,
)
.await
@@ -916,6 +961,7 @@ impl UnifiedExecProcessManager {
None,
tty,
tty,
&protected_metadata_targets,
request.windows_sandbox_private_desktop,
)
.await
@@ -938,7 +984,7 @@ impl UnifiedExecProcessManager {
let started = environment
.get_exec_backend()
.start(exec_server_params_for_request(process_id, request, tty))
.start(exec_server_params_for_request(process_id, &request, tty))
.await
.map_err(|err| UnifiedExecError::create_process(err.to_string()))?;
spawn_lifecycle.after_spawn();

View File

@@ -135,6 +135,68 @@ fn exec_server_process_id_matches_unified_exec_process_id() {
assert_eq!(exec_server_process_id(/*process_id*/ 4321), "4321");
}
#[test]
fn open_session_prepares_windows_metadata_overrides_for_unified_exec() {
let temp_dir = tempfile::TempDir::new().expect("tempdir");
let cwd: codex_utils_absolute_path::AbsolutePathBuf = dunce::canonicalize(temp_dir.path())
.expect("canonical temp dir")
.try_into()
.expect("absolute temp dir");
let permission_profile = codex_protocol::models::PermissionProfile::workspace_write_with(
&[],
codex_protocol::permissions::NetworkSandboxPolicy::Restricted,
/*exclude_tmpdir_env_var*/ true,
/*exclude_slash_tmp*/ true,
);
let (file_system_sandbox_policy, network_sandbox_policy) =
permission_profile.to_runtime_permissions();
let request = ExecRequest {
command: vec![
"cmd.exe".to_string(),
"/c".to_string(),
"echo ok".to_string(),
],
cwd: cwd.clone(),
env: HashMap::new(),
exec_server_env_config: None,
network: None,
expiration: crate::exec::ExecExpiration::DefaultTimeout,
capture_policy: crate::exec::ExecCapturePolicy::ShellTool,
sandbox: codex_sandboxing::SandboxType::WindowsRestrictedToken,
windows_sandbox_policy_cwd: cwd.clone(),
windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel::RestrictedToken,
windows_sandbox_private_desktop: false,
permission_profile,
file_system_sandbox_policy,
network_sandbox_policy,
windows_sandbox_filesystem_overrides: None,
arg0: None,
};
let prepared = prepare_exec_request_for_open_session(&request).expect("prepare request");
let overrides = prepared
.windows_sandbox_filesystem_overrides
.expect("metadata overrides");
assert_eq!(
overrides.protected_metadata_targets,
vec![
crate::exec::WindowsProtectedMetadataTarget {
path: cwd.join(".agents"),
mode: crate::exec::WindowsProtectedMetadataMode::MissingCreationMonitor,
},
crate::exec::WindowsProtectedMetadataTarget {
path: cwd.join(".codex"),
mode: crate::exec::WindowsProtectedMetadataMode::MissingCreationMonitor,
},
crate::exec::WindowsProtectedMetadataTarget {
path: cwd.join(".git"),
mode: crate::exec::WindowsProtectedMetadataMode::MissingCreationMonitor,
},
]
);
}
#[tokio::test]
async fn network_denial_fallback_message_names_sandbox_network_proxy() {
let message = network_denial_message_for_session(/*session*/ None, /*deferred*/ None).await;

View File

@@ -46,6 +46,7 @@ use windows_sys::Win32::Storage::FileSystem::FILE_WRITE_EA;
use windows_sys::Win32::Storage::FileSystem::OPEN_EXISTING;
use windows_sys::Win32::Storage::FileSystem::READ_CONTROL;
use windows_sys::Win32::Storage::FileSystem::DELETE;
const SE_FILE_OBJECT: u32 = 1;
const SE_KERNEL_OBJECT: u32 = 6;
const INHERIT_ONLY_ACE: u8 = 0x08;
const GENERIC_WRITE_MASK: u32 = 0x4000_0000;
@@ -568,19 +569,20 @@ pub unsafe fn revoke_ace(path: &Path, psid: *mut c_void) {
}
}
/// Grants RX to the null device for the given SID to support stdout/stderr redirection.
///
/// # Safety
/// Caller must ensure `psid` is a valid SID pointer.
pub unsafe fn allow_null_device(psid: *mut c_void) {
unsafe fn allow_opened_object_path(
psid: *mut c_void,
path: &str,
object_type: u32,
flags_and_attributes: u32,
) {
let desired = 0x00020000 | 0x00040000; // READ_CONTROL | WRITE_DAC
let h = CreateFileW(
to_wide(r"\\\\.\\NUL").as_ptr(),
to_wide(path).as_ptr(),
desired,
FILE_SHARE_READ | FILE_SHARE_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
std::ptr::null_mut(),
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
flags_and_attributes,
0,
);
if h == 0 || h == INVALID_HANDLE_VALUE {
@@ -590,7 +592,7 @@ pub unsafe fn allow_null_device(psid: *mut c_void) {
let mut p_dacl: *mut ACL = std::ptr::null_mut();
let code = GetSecurityInfo(
h,
SE_KERNEL_OBJECT as i32,
object_type as i32,
DACL_SECURITY_INFORMATION,
std::ptr::null_mut(),
std::ptr::null_mut(),
@@ -617,7 +619,7 @@ pub unsafe fn allow_null_device(psid: *mut c_void) {
if code2 == ERROR_SUCCESS {
let _ = SetSecurityInfo(
h,
SE_KERNEL_OBJECT as i32,
object_type as i32,
DACL_SECURITY_INFORMATION,
std::ptr::null_mut(),
std::ptr::null_mut(),
@@ -634,5 +636,77 @@ pub unsafe fn allow_null_device(psid: *mut c_void) {
}
CloseHandle(h);
}
unsafe fn allow_named_file_object_path(psid: *mut c_void, path: &str, allow_mask: u32) {
let mut p_sd: *mut c_void = std::ptr::null_mut();
let mut p_dacl: *mut ACL = std::ptr::null_mut();
let code = GetNamedSecurityInfoW(
to_wide(path).as_ptr(),
SE_FILE_OBJECT as i32,
DACL_SECURITY_INFORMATION,
std::ptr::null_mut(),
std::ptr::null_mut(),
&mut p_dacl,
std::ptr::null_mut(),
&mut p_sd,
);
if code != ERROR_SUCCESS {
if !p_sd.is_null() {
LocalFree(p_sd as HLOCAL);
}
return;
}
let trustee = TRUSTEE_W {
pMultipleTrustee: std::ptr::null_mut(),
MultipleTrusteeOperation: 0,
TrusteeForm: TRUSTEE_IS_SID,
TrusteeType: TRUSTEE_IS_UNKNOWN,
ptstrName: psid as *mut u16,
};
let mut explicit: EXPLICIT_ACCESS_W = std::mem::zeroed();
explicit.grfAccessPermissions = allow_mask;
explicit.grfAccessMode = 2; // SET_ACCESS
explicit.grfInheritance = 0;
explicit.Trustee = trustee;
let mut p_new_dacl: *mut ACL = std::ptr::null_mut();
let code2 = SetEntriesInAclW(1, &explicit, p_dacl, &mut p_new_dacl);
if code2 == ERROR_SUCCESS {
let _ = SetNamedSecurityInfoW(
to_wide(path).as_ptr() as *mut u16,
SE_FILE_OBJECT as i32,
DACL_SECURITY_INFORMATION,
std::ptr::null_mut(),
std::ptr::null_mut(),
p_new_dacl,
std::ptr::null_mut(),
);
if !p_new_dacl.is_null() {
LocalFree(p_new_dacl as HLOCAL);
}
}
if !p_sd.is_null() {
LocalFree(p_sd as HLOCAL);
}
}
/// Grants access to the null device for the given SID to support stdout/stderr redirection.
///
/// # Safety
/// Caller must ensure `psid` is a valid SID pointer.
pub unsafe fn allow_null_device(psid: *mut c_void) {
allow_opened_object_path(psid, "\\\\.\\NUL", SE_KERNEL_OBJECT, FILE_ATTRIBUTE_NORMAL);
}
/// Grants access to the named pipe namespace for the given SID.
///
/// MSYS and Git for Windows create signal pipes during process startup. Restricted tokens need an
/// explicit allow on the pipe namespace, otherwise those child processes fail during initialization
/// with `ERROR_ACCESS_DENIED`.
///
/// # Safety
/// Caller must ensure `psid` is a valid SID pointer.
pub unsafe fn allow_named_pipe_device(psid: *mut c_void) {
allow_named_file_object_path(psid, "\\\\.\\pipe\\", FILE_DELETE_CHILD);
}
const CONTAINER_INHERIT_ACE: u32 = 0x2;
const OBJECT_INHERIT_ACE: u32 = 0x1;

View File

@@ -27,6 +27,7 @@ use codex_windows_sandbox::SpawnReady;
use codex_windows_sandbox::SpawnRequest;
use codex_windows_sandbox::StderrMode;
use codex_windows_sandbox::StdinMode;
use codex_windows_sandbox::allow_named_pipe_device;
use codex_windows_sandbox::allow_null_device;
use codex_windows_sandbox::create_readonly_token_with_caps_and_user_from;
use codex_windows_sandbox::create_workspace_write_token_with_caps_and_user_from;
@@ -256,8 +257,10 @@ fn spawn_ipc_process(req: &SpawnRequest) -> Result<IpcSpawnedProcess> {
// These ACL adjustments need the raw SID values, but ownership stays with `cap_psids`.
// We do not manually `LocalFree` anything here; the wrappers handle every return path.
allow_null_device(cap_psid_ptrs[0]);
allow_named_pipe_device(cap_psid_ptrs[0]);
for psid in &cap_psid_ptrs {
allow_null_device(*psid);
allow_named_pipe_device(*psid);
}
}

View File

@@ -2,6 +2,8 @@ use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
use crate::setup::ProtectedMetadataTarget;
pub struct ElevatedSandboxCaptureRequest<'a> {
pub policy_json_or_preset: &'a str,
pub sandbox_policy_cwd: &'a Path,
@@ -16,10 +18,12 @@ pub struct ElevatedSandboxCaptureRequest<'a> {
pub read_roots_include_platform_defaults: bool,
pub write_roots_override: Option<&'a [PathBuf]>,
pub deny_write_paths_override: &'a [PathBuf],
pub protected_metadata_targets: &'a [ProtectedMetadataTarget],
}
mod windows_impl {
use super::ElevatedSandboxCaptureRequest;
use crate::acl::allow_named_pipe_device;
use crate::acl::allow_null_device;
use crate::cap::load_or_create_cap_sids;
use crate::env::ensure_non_interactive_pager;
@@ -36,6 +40,7 @@ mod windows_impl {
use crate::logging::log_success;
use crate::policy::SandboxPolicy;
use crate::policy::parse_policy;
use crate::protected_metadata::prepare_protected_metadata_targets;
use crate::runner_client::spawn_runner_transport;
use crate::token::convert_string_sid_to_sid;
use anyhow::Result;
@@ -125,6 +130,7 @@ mod windows_impl {
read_roots_include_platform_defaults,
write_roots_override,
deny_write_paths_override,
protected_metadata_targets,
} = request;
let policy = parse_policy(policy_json_or_preset)?;
normalize_null_device_env(&mut env_map);
@@ -137,6 +143,8 @@ mod windows_impl {
let logs_base_dir: Option<&Path> = Some(sandbox_base.as_path());
log_start(&command, logs_base_dir);
let protected_metadata_guard =
prepare_protected_metadata_targets(protected_metadata_targets);
let sandbox_creds = require_logon_sandbox_creds(
&policy,
sandbox_policy_cwd,
@@ -147,6 +155,7 @@ mod windows_impl {
read_roots_include_platform_defaults,
write_roots_override,
deny_write_paths_override,
protected_metadata_targets,
proxy_enforced,
)?;
// Build capability SID for ACL grants.
@@ -181,6 +190,7 @@ mod windows_impl {
unsafe {
allow_null_device(psid_to_use);
allow_named_pipe_device(psid_to_use);
}
(|| -> Result<CaptureResult> {
@@ -210,7 +220,7 @@ mod windows_impl {
let mut stdout = Vec::new();
let mut stderr = Vec::new();
let (exit_code, timed_out) = loop {
let (mut exit_code, timed_out) = loop {
let msg = read_frame(&mut pipe_read)?
.ok_or_else(|| anyhow::anyhow!("runner pipe closed before exit"))?;
match msg.message {
@@ -234,6 +244,12 @@ mod windows_impl {
}
};
let protected_metadata_violations =
protected_metadata_guard.cleanup_created_monitored_paths()?;
if !protected_metadata_violations.is_empty() && exit_code == 0 {
exit_code = 1;
}
if exit_code == 0 {
log_success(&command, logs_base_dir);
} else {

View File

@@ -140,6 +140,7 @@ pub fn require_logon_sandbox_creds(
read_roots_include_platform_defaults: bool,
write_roots_override: Option<&[PathBuf]>,
deny_write_paths_override: &[PathBuf],
protected_metadata_targets: &[crate::setup::ProtectedMetadataTarget],
proxy_enforced: bool,
) -> Result<SandboxCreds> {
let sandbox_dir = crate::setup::sandbox_dir(codex_home);
@@ -202,6 +203,7 @@ pub fn require_logon_sandbox_creds(
read_roots_include_platform_defaults,
write_roots: Some(needed_write.clone()),
deny_write_paths: Some(deny_write_paths_override.to_vec()),
protected_metadata_targets: Some(protected_metadata_targets.to_vec()),
},
)?;
identity = select_identity(network_identity, codex_home)?;
@@ -221,6 +223,7 @@ pub fn require_logon_sandbox_creds(
read_roots_include_platform_defaults,
write_roots: Some(needed_write),
deny_write_paths: Some(deny_write_paths_override.to_vec()),
protected_metadata_targets: Some(protected_metadata_targets.to_vec()),
},
)?;
let identity = identity.ok_or_else(|| {

View File

@@ -26,6 +26,7 @@ windows_modules!(
path_normalization,
policy,
process,
protected_metadata,
token,
wfp,
wfp_setup,
@@ -78,6 +79,8 @@ mod session;
#[cfg(target_os = "windows")]
pub use acl::add_deny_write_ace;
#[cfg(target_os = "windows")]
pub use acl::allow_named_pipe_device;
#[cfg(target_os = "windows")]
pub use acl::allow_null_device;
#[cfg(target_os = "windows")]
@@ -167,10 +170,16 @@ pub use process::read_handle_loop;
#[cfg(target_os = "windows")]
pub use process::spawn_process_with_pipes;
#[cfg(target_os = "windows")]
pub use protected_metadata::protected_metadata_existing_deny_paths;
#[cfg(target_os = "windows")]
pub use session::spawn_windows_sandbox_session_elevated;
#[cfg(target_os = "windows")]
pub use session::spawn_windows_sandbox_session_legacy;
#[cfg(target_os = "windows")]
pub use setup::ProtectedMetadataMode;
#[cfg(target_os = "windows")]
pub use setup::ProtectedMetadataTarget;
#[cfg(target_os = "windows")]
pub use setup::SETUP_VERSION;
#[cfg(target_os = "windows")]
pub use setup::SandboxSetupRequest;
@@ -251,9 +260,13 @@ pub use stub::run_windows_sandbox_legacy_preflight;
#[cfg(target_os = "windows")]
mod windows_impl {
use super::ProtectedMetadataTarget;
use super::acl::add_allow_ace;
use super::acl::add_deny_write_ace;
use super::acl::allow_named_pipe_device;
use super::acl::allow_null_device;
use super::acl::ensure_allow_mask_aces;
use super::acl::ensure_allow_mask_aces_with_inheritance;
use super::acl::revoke_ace;
use super::allow::AllowDenyPaths;
use super::allow::compute_allow_paths;
@@ -264,7 +277,10 @@ mod windows_impl {
use super::path_normalization::canonicalize_path;
use super::policy::SandboxPolicy;
use super::process::create_process_as_user;
use super::protected_metadata::prepare_protected_metadata_targets;
use super::sandbox_utils::ensure_codex_home_exists;
use super::spawn_prep::legacy_session_direct_read_paths;
use super::spawn_prep::legacy_session_executable_read_roots;
use super::spawn_prep::prepare_legacy_spawn_context;
use super::token::convert_string_sid_to_sid;
use super::token::create_workspace_write_token_with_caps_from;
@@ -281,6 +297,8 @@ mod windows_impl {
use windows_sys::Win32::Foundation::HANDLE;
use windows_sys::Win32::Foundation::HANDLE_FLAG_INHERIT;
use windows_sys::Win32::Foundation::SetHandleInformation;
use windows_sys::Win32::Storage::FileSystem::FILE_GENERIC_EXECUTE;
use windows_sys::Win32::Storage::FileSystem::FILE_GENERIC_READ;
use windows_sys::Win32::System::Pipes::CreatePipe;
use windows_sys::Win32::System::Threading::GetExitCodeProcess;
use windows_sys::Win32::System::Threading::INFINITE;
@@ -343,6 +361,7 @@ mod windows_impl {
env_map,
timeout_ms,
&[],
&[],
use_private_desktop,
)
}
@@ -357,6 +376,7 @@ mod windows_impl {
mut env_map: HashMap<String, String>,
timeout_ms: Option<u64>,
additional_deny_write_paths: &[PathBuf],
protected_metadata_targets: &[ProtectedMetadataTarget],
use_private_desktop: bool,
) -> Result<CaptureResult> {
let common = prepare_legacy_spawn_context(
@@ -366,7 +386,7 @@ mod windows_impl {
&mut env_map,
&command,
/*inherit_path*/ false,
/*add_git_safe_directory*/ false,
/*add_git_safe_directory*/ true,
)?;
let policy = common.policy;
let current_dir = common.current_dir;
@@ -418,6 +438,7 @@ mod windows_impl {
let mut tmp = bytes;
let psid2 = tmp.as_mut_ptr() as *mut c_void;
allow_null_device(psid2);
allow_named_pipe_device(psid2);
}
windows_sys::Win32::Foundation::CloseHandle(base);
}
@@ -426,6 +447,13 @@ mod windows_impl {
let persist_aces = is_workspace_write;
let AllowDenyPaths { allow, mut deny } =
compute_allow_paths(&policy, sandbox_policy_cwd, &current_dir, &env_map);
let read_roots = legacy_session_executable_read_roots(&env_map, &command);
let direct_read_paths = legacy_session_direct_read_paths(&env_map);
let protected_metadata_guard =
prepare_protected_metadata_targets(protected_metadata_targets);
for path in protected_metadata_guard.deny_paths() {
deny.insert(path.clone());
}
for path in additional_deny_write_paths {
if path.exists() {
deny.insert(path.clone());
@@ -433,7 +461,32 @@ mod windows_impl {
}
let canonical_cwd = canonicalize_path(&current_dir);
let mut guards: Vec<(PathBuf, *mut c_void)> = Vec::new();
let read_execute_mask = FILE_GENERIC_READ | FILE_GENERIC_EXECUTE;
unsafe {
let read_execute_sids: Vec<*mut c_void> = match psid_workspace {
Some(psid) => vec![psid_generic, psid],
None => vec![psid_generic],
};
for p in &read_roots {
if let Ok(added) = ensure_allow_mask_aces(p, &read_execute_sids, read_execute_mask)
&& added
&& !persist_aces
{
guards.push((p.clone(), psid_generic));
}
}
for p in &direct_read_paths {
if let Ok(added) = ensure_allow_mask_aces_with_inheritance(
p,
&read_execute_sids,
read_execute_mask,
/*inheritance*/ 0,
) && added
&& !persist_aces
{
guards.push((p.clone(), psid_generic));
}
}
for p in &allow {
let psid = if is_workspace_write && is_command_cwd_root(p, &canonical_cwd) {
psid_workspace.unwrap_or(psid_generic)
@@ -461,8 +514,10 @@ mod windows_impl {
}
}
allow_null_device(psid_generic);
allow_named_pipe_device(psid_generic);
if let Some(psid) = psid_workspace {
allow_null_device(psid);
allow_named_pipe_device(psid);
}
}
let (stdin_pair, stdout_pair, stderr_pair) = unsafe { setup_stdio_pipes()? };
@@ -576,11 +631,16 @@ mod windows_impl {
let _ = t_err.join();
let stdout = rx_out.recv().unwrap_or_default();
let stderr = rx_err.recv().unwrap_or_default();
let exit_code = if timed_out {
let mut exit_code = if timed_out {
128 + 64
} else {
exit_code_u32 as i32
};
let protected_metadata_violations =
protected_metadata_guard.cleanup_created_monitored_paths()?;
if !protected_metadata_violations.is_empty() && exit_code == 0 {
exit_code = 1;
}
if exit_code == 0 {
log_success(&command, logs_base_dir);
@@ -641,7 +701,9 @@ mod windows_impl {
let _ = add_deny_write_ace(p, psid_generic);
}
allow_null_device(psid_generic);
allow_named_pipe_device(psid_generic);
allow_null_device(psid_workspace);
allow_named_pipe_device(psid_workspace);
}
Ok(())

View File

@@ -0,0 +1,230 @@
use crate::setup::ProtectedMetadataMode;
use crate::setup::ProtectedMetadataTarget;
use anyhow::Context;
use anyhow::Result;
use std::collections::HashSet;
use std::fs::Metadata;
use std::io;
use std::os::windows::fs::FileTypeExt;
use std::os::windows::fs::MetadataExt;
use std::path::Path;
use std::path::PathBuf;
use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_REPARSE_POINT;
/// Layer: Windows enforcement layer. Existing metadata objects can be protected
/// with ACLs; missing names are monitored and removed if the sandbox creates
/// them.
#[derive(Debug)]
pub(crate) struct ProtectedMetadataGuard {
deny_paths: Vec<PathBuf>,
monitored_paths: Vec<PathBuf>,
}
impl ProtectedMetadataGuard {
pub(crate) fn deny_paths(&self) -> impl Iterator<Item = &PathBuf> {
self.deny_paths.iter()
}
pub(crate) fn cleanup_created_monitored_paths(&self) -> Result<Vec<PathBuf>> {
let mut removed = Vec::new();
for path in &self.monitored_paths {
let Some(existing_path) = existing_metadata_path(path)? else {
continue;
};
remove_metadata_path(&existing_path)
.with_context(|| format!("failed to remove protected metadata {}", path.display()))?;
removed.push(existing_path);
}
Ok(removed)
}
}
pub(crate) fn prepare_protected_metadata_targets(
targets: &[ProtectedMetadataTarget],
) -> ProtectedMetadataGuard {
let mut deny_paths = Vec::new();
let mut monitored_paths = Vec::new();
for target in targets {
match target.mode {
ProtectedMetadataMode::ExistingDeny => {
deny_paths.extend(protected_metadata_existing_deny_paths(&target.path));
}
ProtectedMetadataMode::MissingCreationMonitor => {
monitored_paths.push(target.path.clone());
}
}
}
ProtectedMetadataGuard {
deny_paths,
monitored_paths,
}
}
pub fn protected_metadata_existing_deny_paths(path: &Path) -> Vec<PathBuf> {
let Ok(metadata) = std::fs::symlink_metadata(path) else {
return Vec::new();
};
let mut seen = HashSet::new();
let mut paths = Vec::new();
push_deny_path(&mut paths, &mut seen, path.to_path_buf());
let file_type = metadata.file_type();
if (is_directory_reparse_point(&metadata)
|| file_type.is_symlink_dir()
|| file_type.is_symlink_file())
&& let Ok(target_path) = dunce::canonicalize(path)
{
push_deny_path(&mut paths, &mut seen, target_path);
}
paths
}
fn push_deny_path(paths: &mut Vec<PathBuf>, seen: &mut HashSet<String>, path: PathBuf) {
if seen.insert(path_text_key(&path)) {
paths.push(path);
}
}
fn path_text_key(path: &Path) -> String {
path.to_string_lossy()
.replace('\\', "/")
.to_ascii_lowercase()
}
fn existing_metadata_path(path: &Path) -> Result<Option<PathBuf>> {
match std::fs::symlink_metadata(path) {
Ok(_) => return Ok(Some(path.to_path_buf())),
Err(err) if err.kind() == io::ErrorKind::NotFound => {}
Err(err) => {
return Err(err)
.with_context(|| format!("failed to inspect protected metadata {}", path.display()));
}
}
let Some(parent) = path.parent() else {
return Ok(None);
};
let Some(expected_name) = path.file_name().and_then(|name| name.to_str()) else {
return Ok(None);
};
let entries = match std::fs::read_dir(parent) {
Ok(entries) => entries,
Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(None),
Err(err) => {
return Err(err)
.with_context(|| format!("failed to scan protected metadata parent {}", parent.display()));
}
};
for entry in entries {
let entry = entry.with_context(|| {
format!(
"failed to read protected metadata parent entry {}",
parent.display()
)
})?;
if entry
.file_name()
.to_str()
.is_some_and(|name| name.eq_ignore_ascii_case(expected_name))
{
return Ok(Some(entry.path()));
}
}
Ok(None)
}
fn remove_metadata_path(path: &Path) -> Result<()> {
let metadata = match std::fs::symlink_metadata(path) {
Ok(metadata) => metadata,
Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()),
Err(err) => {
return Err(err)
.with_context(|| format!("failed to inspect protected metadata {}", path.display()));
}
};
let file_type = metadata.file_type();
if is_directory_reparse_point(&metadata) || file_type.is_symlink_dir() {
std::fs::remove_dir(path)
.with_context(|| format!("failed to remove protected metadata {}", path.display()))?;
} else if file_type.is_symlink_file() {
std::fs::remove_file(path)
.with_context(|| format!("failed to remove protected metadata {}", path.display()))?;
} else if metadata.is_dir() {
std::fs::remove_dir_all(path)
.with_context(|| format!("failed to remove protected metadata {}", path.display()))?;
} else {
std::fs::remove_file(path)
.with_context(|| format!("failed to remove protected metadata {}", path.display()))?;
}
Ok(())
}
fn is_directory_reparse_point(metadata: &Metadata) -> bool {
metadata.is_dir() && (metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT) != 0
}
#[cfg(test)]
mod tests {
use super::*;
use crate::setup::ProtectedMetadataMode;
use crate::setup::ProtectedMetadataTarget;
#[test]
fn cleanup_created_monitored_paths_removes_case_variant() {
let temp_dir = tempfile::TempDir::new().expect("tempdir");
let target = temp_dir.path().join(".git");
let created = temp_dir.path().join(".GIT");
std::fs::create_dir_all(&created).expect("create metadata");
let guard = prepare_protected_metadata_targets(&[ProtectedMetadataTarget {
path: target.clone(),
mode: ProtectedMetadataMode::MissingCreationMonitor,
}]);
let removed = guard.cleanup_created_monitored_paths().expect("cleanup");
assert_eq!(removed.len(), 1);
assert!(
removed[0]
.file_name()
.is_some_and(|name| name.eq_ignore_ascii_case(".git")),
"removed path should be a .git case variant: {}",
removed[0].display()
);
assert!(!target.exists());
assert!(!created.exists());
}
#[test]
fn existing_deny_paths_include_symlink_target() {
let temp_dir = tempfile::TempDir::new().expect("tempdir");
let target_dir = temp_dir.path().join("target-codex");
let symlink_dir = temp_dir.path().join(".codex");
std::fs::create_dir_all(&target_dir).expect("create target");
if let Err(err) = std::os::windows::fs::symlink_dir(&target_dir, &symlink_dir) {
eprintln!("skipping symlink test because symlink creation failed: {err}");
return;
}
let guard = prepare_protected_metadata_targets(&[ProtectedMetadataTarget {
path: symlink_dir.clone(),
mode: ProtectedMetadataMode::ExistingDeny,
}]);
let deny_paths: Vec<PathBuf> = guard.deny_paths().cloned().collect();
let canonical_target = dunce::canonicalize(&target_dir).expect("canonical target");
assert!(
deny_paths
.iter()
.any(|path| path_text_key(path) == path_text_key(&symlink_dir)),
"deny paths should include metadata symlink: {deny_paths:?}"
);
assert!(
deny_paths
.iter()
.any(|path| path_text_key(path) == path_text_key(&canonical_target)),
"deny paths should include symlink target: {deny_paths:?}"
);
}
}

View File

@@ -8,6 +8,8 @@ use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64;
use codex_otel::StatsigMetricsSettings;
use codex_windows_sandbox::LOG_FILE_NAME;
use codex_windows_sandbox::ProtectedMetadataMode;
use codex_windows_sandbox::ProtectedMetadataTarget;
use codex_windows_sandbox::SETUP_VERSION;
use codex_windows_sandbox::SetupErrorCode;
use codex_windows_sandbox::SetupErrorReport;
@@ -24,6 +26,7 @@ use codex_windows_sandbox::is_command_cwd_root;
use codex_windows_sandbox::load_or_create_cap_sids;
use codex_windows_sandbox::log_note;
use codex_windows_sandbox::path_mask_allows;
use codex_windows_sandbox::protected_metadata_existing_deny_paths;
use codex_windows_sandbox::sandbox_bin_dir;
use codex_windows_sandbox::sandbox_dir;
use codex_windows_sandbox::sandbox_secrets_dir;
@@ -87,6 +90,8 @@ struct Payload {
write_roots: Vec<PathBuf>,
#[serde(default)]
deny_write_paths: Vec<PathBuf>,
#[serde(default)]
protected_metadata_targets: Vec<ProtectedMetadataTarget>,
proxy_ports: Vec<u16>,
#[serde(default)]
allow_local_binding: bool,
@@ -816,6 +821,66 @@ fn run_setup_full(payload: &Payload, log: &mut File, sbx_dir: &Path) -> Result<(
}
}
for target in &payload.protected_metadata_targets {
if !matches!(target.mode, ProtectedMetadataMode::ExistingDeny) {
continue;
}
let deny_paths = protected_metadata_existing_deny_paths(&target.path);
if deny_paths.is_empty() {
log_line(
log,
&format!(
"protected metadata {} missing during setup; skipping",
target.path.display()
),
)?;
continue;
}
for path in deny_paths {
if !seen_deny_paths.insert(path.clone()) {
continue;
}
if std::fs::symlink_metadata(&path).is_err() {
log_line(
log,
&format!(
"protected metadata {} missing during setup; skipping",
path.display()
),
)?;
continue;
}
let canonical_path = canonicalize_path(&path);
let deny_psid = if canonical_path.starts_with(&canonical_command_cwd) {
workspace_psid
} else {
cap_psid
};
match unsafe { add_deny_write_ace(&path, deny_psid) } {
Ok(true) => {
log_line(
log,
&format!("applied deny ACE to protect metadata {}", path.display()),
)?;
}
Ok(false) => {}
Err(err) => {
refresh_errors.push(format!(
"metadata deny ACE failed on {}: {err}",
path.display()
));
log_line(
log,
&format!("metadata deny ACE failed on {}: {err}", path.display()),
)?;
}
}
}
}
lock_sandbox_dir(
&sandbox_bin_dir(&payload.codex_home),
&payload.real_user,

View File

@@ -97,6 +97,24 @@ pub struct SetupRootOverrides {
pub read_roots_include_platform_defaults: bool,
pub write_roots: Option<Vec<PathBuf>>,
pub deny_write_paths: Option<Vec<PathBuf>>,
pub protected_metadata_targets: Option<Vec<ProtectedMetadataTarget>>,
}
/// Layer: Windows enforcement request boundary. These targets are projected by
/// the adapter layer before they reach the setup helper.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProtectedMetadataTarget {
pub path: PathBuf,
pub mode: ProtectedMetadataMode,
}
/// Layer: Windows enforcement request boundary. The helper must distinguish
/// existing metadata objects from missing names that need create monitoring.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ProtectedMetadataMode {
ExistingDeny,
MissingCreationMonitor,
}
pub fn run_setup_refresh(
@@ -152,6 +170,7 @@ pub fn run_setup_refresh_with_extra_read_roots(
read_roots_include_platform_defaults: false,
write_roots: Some(Vec::new()),
deny_write_paths: None,
protected_metadata_targets: None,
},
)
}
@@ -169,6 +188,8 @@ fn run_setup_refresh_inner(
}
let (read_roots, write_roots) = build_payload_roots(&request, &overrides);
let deny_write_paths = build_payload_deny_write_paths(&request, overrides.deny_write_paths);
let protected_metadata_targets =
build_payload_protected_metadata_targets(overrides.protected_metadata_targets);
let network_identity =
SandboxNetworkIdentity::from_policy(request.policy, request.proxy_enforced);
let offline_proxy_settings = offline_proxy_settings_from_env(request.env_map, network_identity);
@@ -181,6 +202,7 @@ fn run_setup_refresh_inner(
read_roots,
write_roots,
deny_write_paths,
protected_metadata_targets,
proxy_ports: offline_proxy_settings.proxy_ports,
allow_local_binding: offline_proxy_settings.allow_local_binding,
otel: None,
@@ -419,6 +441,8 @@ struct ElevationPayload {
write_roots: Vec<PathBuf>,
#[serde(default)]
deny_write_paths: Vec<PathBuf>,
#[serde(default)]
protected_metadata_targets: Vec<ProtectedMetadataTarget>,
proxy_ports: Vec<u16>,
#[serde(default)]
allow_local_binding: bool,
@@ -721,6 +745,8 @@ pub fn run_elevated_setup(
})?;
let (read_roots, write_roots) = build_payload_roots(&request, &overrides);
let deny_write_paths = build_payload_deny_write_paths(&request, overrides.deny_write_paths);
let protected_metadata_targets =
build_payload_protected_metadata_targets(overrides.protected_metadata_targets);
let network_identity =
SandboxNetworkIdentity::from_policy(request.policy, request.proxy_enforced);
let offline_proxy_settings = offline_proxy_settings_from_env(request.env_map, network_identity);
@@ -733,6 +759,7 @@ pub fn run_elevated_setup(
read_roots,
write_roots,
deny_write_paths,
protected_metadata_targets,
proxy_ports: offline_proxy_settings.proxy_ports,
allow_local_binding: offline_proxy_settings.allow_local_binding,
real_user: std::env::var("USERNAME").unwrap_or_else(|_| "Administrators".to_string()),
@@ -817,6 +844,12 @@ fn build_payload_deny_write_paths(
deny_write_paths
}
fn build_payload_protected_metadata_targets(
explicit_targets: Option<Vec<ProtectedMetadataTarget>>,
) -> Vec<ProtectedMetadataTarget> {
explicit_targets.unwrap_or_default()
}
fn expand_user_profile_root(roots: Vec<PathBuf>) -> Vec<PathBuf> {
let Ok(user_profile) = std::env::var("USERPROFILE") else {
return roots;
@@ -1328,6 +1361,7 @@ mod tests {
read_roots_include_platform_defaults: true,
write_roots: None,
deny_write_paths: None,
protected_metadata_targets: None,
},
);
let expected_helper =
@@ -1375,6 +1409,7 @@ mod tests {
read_roots_include_platform_defaults: false,
write_roots: None,
deny_write_paths: None,
protected_metadata_targets: None,
},
);
let expected_helper =

View File

@@ -1,6 +1,9 @@
use crate::acl::add_allow_ace;
use crate::acl::add_deny_write_ace;
use crate::acl::allow_named_pipe_device;
use crate::acl::allow_null_device;
use crate::acl::ensure_allow_mask_aces;
use crate::acl::ensure_allow_mask_aces_with_inheritance;
use crate::allow::AllowDenyPaths;
use crate::allow::compute_allow_paths;
use crate::cap::load_or_create_cap_sids;
@@ -17,6 +20,7 @@ use crate::policy::SandboxPolicy;
use crate::policy::parse_policy;
use crate::sandbox_utils::ensure_codex_home_exists;
use crate::sandbox_utils::inject_git_safe_directory;
use crate::setup::ProtectedMetadataTarget;
use crate::token::convert_string_sid_to_sid;
use crate::token::create_readonly_token_with_cap;
use crate::token::create_workspace_write_token_with_caps_from;
@@ -34,6 +38,8 @@ use windows_sys::Win32::Foundation::CloseHandle;
use windows_sys::Win32::Foundation::HANDLE;
use windows_sys::Win32::Foundation::HLOCAL;
use windows_sys::Win32::Foundation::LocalFree;
use windows_sys::Win32::Storage::FileSystem::FILE_GENERIC_EXECUTE;
use windows_sys::Win32::Storage::FileSystem::FILE_GENERIC_READ;
pub(crate) struct SpawnContext {
pub(crate) policy: SandboxPolicy,
@@ -205,6 +211,7 @@ pub(crate) fn allow_null_device_for_workspace_write(is_workspace_write: bool) {
let mut tmp = bytes;
let psid = tmp.as_mut_ptr() as *mut c_void;
allow_null_device(psid);
allow_named_pipe_device(psid);
}
CloseHandle(base);
}
@@ -216,15 +223,45 @@ pub(crate) fn apply_legacy_session_acl_rules(
sandbox_policy_cwd: &Path,
current_dir: &Path,
env_map: &HashMap<String, String>,
command: &[String],
psid_generic: &LocalSid,
psid_workspace: Option<&LocalSid>,
persist_aces: bool,
additional_deny_paths: &[PathBuf],
) -> Vec<PathBuf> {
let AllowDenyPaths { allow, deny } =
let AllowDenyPaths { allow, mut deny } =
compute_allow_paths(policy, sandbox_policy_cwd, current_dir, env_map);
deny.extend(additional_deny_paths.iter().cloned());
let mut guards: Vec<PathBuf> = Vec::new();
let read_roots = legacy_session_executable_read_roots(env_map, command);
let direct_read_paths = legacy_session_direct_read_paths(env_map);
let read_execute_mask = FILE_GENERIC_READ | FILE_GENERIC_EXECUTE;
let canonical_cwd = canonicalize_path(current_dir);
unsafe {
let read_execute_sids: Vec<*mut std::ffi::c_void> = match psid_workspace {
Some(psid_workspace) => vec![psid_generic.as_ptr(), psid_workspace.as_ptr()],
None => vec![psid_generic.as_ptr()],
};
for p in &read_roots {
if let Ok(added) = ensure_allow_mask_aces(p, &read_execute_sids, read_execute_mask)
&& added
&& !persist_aces
{
guards.push(p.clone());
}
}
for p in &direct_read_paths {
if let Ok(added) = ensure_allow_mask_aces_with_inheritance(
p,
&read_execute_sids,
read_execute_mask,
/*inheritance*/ 0,
) && added
&& !persist_aces
{
guards.push(p.clone());
}
}
for p in &allow {
let psid = if matches!(policy, SandboxPolicy::WorkspaceWrite { .. })
&& is_command_cwd_root(p, &canonical_cwd)
@@ -246,8 +283,10 @@ pub(crate) fn apply_legacy_session_acl_rules(
}
}
allow_null_device(psid_generic.as_ptr());
allow_named_pipe_device(psid_generic.as_ptr());
if let Some(psid_workspace) = psid_workspace {
allow_null_device(psid_workspace.as_ptr());
allow_named_pipe_device(psid_workspace.as_ptr());
if persist_aces && matches!(policy, SandboxPolicy::WorkspaceWrite { .. }) {
let _ = protect_workspace_codex_dir(current_dir, psid_workspace.as_ptr());
let _ = protect_workspace_agents_dir(current_dir, psid_workspace.as_ptr());
@@ -257,6 +296,121 @@ pub(crate) fn apply_legacy_session_acl_rules(
guards
}
pub(crate) fn legacy_session_executable_read_roots(
env_map: &HashMap<String, String>,
command: &[String],
) -> Vec<PathBuf> {
let mut roots = Vec::new();
if let Some(program) = command.first() {
let program_path = PathBuf::from(program);
if program_path.is_absolute()
&& let Some(parent) = program_path.parent()
{
roots.push(parent.to_path_buf());
}
}
for (name, value) in env_map {
if !name.eq_ignore_ascii_case("PATH") {
continue;
}
for path in std::env::split_paths(value) {
roots.push(path.clone());
if let Some(tool_root) = windows_tool_root_for_path_dir(&path) {
add_git_for_windows_support_roots(env_map, &tool_root, &mut roots);
roots.push(tool_root);
}
}
}
canonical_existing_deduped(roots)
}
pub(crate) fn legacy_session_direct_read_paths(env_map: &HashMap<String, String>) -> Vec<PathBuf> {
let mut paths = Vec::new();
for home in legacy_session_home_dirs(env_map) {
paths.push(home.clone());
paths.push(home.join(".gitconfig"));
}
canonical_existing_deduped(paths)
}
fn add_git_for_windows_support_roots(
env_map: &HashMap<String, String>,
tool_root: &Path,
roots: &mut Vec<PathBuf>,
) {
let Some(name) = tool_root.file_name() else {
return;
};
if !name.to_string_lossy().eq_ignore_ascii_case("Git") {
return;
}
if let Some(program_data) = env_path(env_map, "PROGRAMDATA") {
roots.push(program_data.join("Git"));
}
}
fn legacy_session_home_dirs(env_map: &HashMap<String, String>) -> Vec<PathBuf> {
let mut homes = Vec::new();
if let Some(user_profile) = env_path(env_map, "USERPROFILE") {
homes.push(user_profile);
}
if let Some(home) = env_path(env_map, "HOME") {
homes.push(home);
}
if let (Some(drive), Some(path)) = (
env_value(env_map, "HOMEDRIVE"),
env_value(env_map, "HOMEPATH"),
) {
homes.push(PathBuf::from(format!("{drive}{path}")));
}
canonical_existing_deduped(homes)
}
fn env_path(env_map: &HashMap<String, String>, name: &str) -> Option<PathBuf> {
env_value(env_map, name).map(PathBuf::from)
}
fn env_value(env_map: &HashMap<String, String>, name: &str) -> Option<String> {
env_map
.iter()
.find(|(key, _)| key.eq_ignore_ascii_case(name))
.map(|(_, value)| value.clone())
}
fn canonical_existing_deduped(paths: Vec<PathBuf>) -> Vec<PathBuf> {
let mut deduped = Vec::new();
for path in paths {
if !path.exists() {
continue;
}
let path = dunce::canonicalize(&path).unwrap_or(path);
if !deduped.iter().any(|existing| existing == &path) {
deduped.push(path);
}
}
deduped
}
fn windows_tool_root_for_path_dir(path: &Path) -> Option<PathBuf> {
let name = path.file_name()?.to_string_lossy();
if !name.eq_ignore_ascii_case("cmd") && !name.eq_ignore_ascii_case("bin") {
return None;
}
let parent = path.parent()?;
let parent_name = parent.file_name()?.to_string_lossy();
if parent_name.eq_ignore_ascii_case("Git") {
return Some(parent.to_path_buf());
}
None
}
pub(crate) fn prepare_elevated_spawn_context(
policy_json_or_preset: &str,
sandbox_policy_cwd: &Path,
@@ -264,6 +418,7 @@ pub(crate) fn prepare_elevated_spawn_context(
cwd: &Path,
env_map: &mut HashMap<String, String>,
command: &[String],
protected_metadata_targets: &[ProtectedMetadataTarget],
) -> Result<ElevatedSpawnContext> {
let common = prepare_spawn_context_common(
policy_json_or_preset,
@@ -298,6 +453,7 @@ pub(crate) fn prepare_elevated_spawn_context(
/*read_roots_include_platform_defaults*/ false,
write_roots_override,
&deny_write_paths,
protected_metadata_targets,
/*proxy_enforced*/ false,
)?;
let caps = load_or_create_cap_sids(codex_home)?;
@@ -320,6 +476,7 @@ pub(crate) fn prepare_elevated_spawn_context(
unsafe {
allow_null_device(psid_to_use.as_ptr());
allow_named_pipe_device(psid_to_use.as_ptr());
}
Ok(ElevatedSpawnContext {
@@ -332,6 +489,8 @@ pub(crate) fn prepare_elevated_spawn_context(
#[cfg(test)]
mod tests {
use super::SandboxPolicy;
use super::legacy_session_direct_read_paths;
use super::legacy_session_executable_read_roots;
use super::prepare_legacy_spawn_context;
use super::prepare_spawn_context_common;
use super::should_apply_network_block;
@@ -409,4 +568,46 @@ mod tests {
Some(&"http://user.proxy:8080".to_string())
);
}
#[test]
fn legacy_session_read_roots_include_git_support_roots() {
let tmp = TempDir::new().expect("tempdir");
let git_root = tmp.path().join("Git");
let git_cmd = git_root.join("cmd");
let program_data_git = tmp.path().join("ProgramData").join("Git");
std::fs::create_dir_all(&git_cmd).expect("create git cmd");
std::fs::create_dir_all(&program_data_git).expect("create programdata git");
let env_map = HashMap::from([
("PATH".to_string(), git_cmd.to_string_lossy().to_string()),
(
"PROGRAMDATA".to_string(),
tmp.path().join("ProgramData").to_string_lossy().to_string(),
),
]);
let roots = legacy_session_executable_read_roots(&env_map, &["cmd.exe".to_string()]);
assert!(roots.contains(&dunce::canonicalize(git_root).expect("canonical git root")));
assert!(
roots.contains(&dunce::canonicalize(program_data_git).expect("canonical programdata"))
);
}
#[test]
fn legacy_session_direct_read_paths_include_home_git_config() {
let tmp = TempDir::new().expect("tempdir");
let home = tmp.path().join("profile");
std::fs::create_dir_all(&home).expect("create profile");
let gitconfig = home.join(".gitconfig");
std::fs::write(&gitconfig, "[safe]\n").expect("write git config");
let env_map = HashMap::from([(
"USERPROFILE".to_string(),
home.to_string_lossy().to_string(),
)]);
let paths = legacy_session_direct_read_paths(&env_map);
assert!(paths.contains(&dunce::canonicalize(home).expect("canonical home")));
assert!(paths.contains(&dunce::canonicalize(gitconfig).expect("canonical gitconfig")));
}
}

View File

@@ -7,7 +7,9 @@ use crate::ipc_framed::EmptyPayload;
use crate::ipc_framed::FramedMessage;
use crate::ipc_framed::Message;
use crate::ipc_framed::SpawnRequest;
use crate::protected_metadata::prepare_protected_metadata_targets;
use crate::runner_client::spawn_runner_transport;
use crate::setup::ProtectedMetadataTarget;
use crate::spawn_prep::prepare_elevated_spawn_context;
use anyhow::Result;
use codex_utils_pty::ProcessDriver;
@@ -29,6 +31,7 @@ pub(crate) async fn spawn_windows_sandbox_session_elevated(
timeout_ms: Option<u64>,
tty: bool,
stdin_open: bool,
protected_metadata_targets: &[ProtectedMetadataTarget],
use_private_desktop: bool,
) -> Result<SpawnedProcess> {
let elevated = prepare_elevated_spawn_context(
@@ -38,8 +41,10 @@ pub(crate) async fn spawn_windows_sandbox_session_elevated(
cwd,
&mut env_map,
&command,
protected_metadata_targets,
)?;
let protected_metadata_guard = prepare_protected_metadata_targets(protected_metadata_targets);
let spawn_request = SpawnRequest {
command: command.clone(),
cwd: cwd.to_path_buf(),
@@ -99,6 +104,7 @@ pub(crate) async fn spawn_windows_sandbox_session_elevated(
stdout_tx,
stderr_rx.as_ref().map(|(tx, _rx)| tx.clone()),
exit_tx,
Some(protected_metadata_guard),
);
Ok(finish_driver_spawn(

View File

@@ -9,6 +9,9 @@ use crate::process::StderrMode;
use crate::process::StdinMode;
use crate::process::read_handle_loop;
use crate::process::spawn_process_with_pipes;
use crate::protected_metadata::ProtectedMetadataGuard;
use crate::protected_metadata::prepare_protected_metadata_targets;
use crate::setup::ProtectedMetadataTarget;
use crate::spawn_prep::LocalSid;
use crate::spawn_prep::allow_null_device_for_workspace_write;
use crate::spawn_prep::apply_legacy_session_acl_rules;
@@ -202,10 +205,11 @@ fn finalize_exit(
output_join: std::thread::JoinHandle<()>,
guards: Vec<PathBuf>,
cap_sid: Option<String>,
protected_metadata_guard: ProtectedMetadataGuard,
logs_base_dir: Option<&Path>,
command: Vec<String>,
) {
let exit_code = {
let mut exit_code = {
let mut raw_exit = 1u32;
if let Ok(guard) = process_handle.lock()
&& let Some(handle) = guard.as_ref()
@@ -219,6 +223,21 @@ fn finalize_exit(
};
let _ = output_join.join();
let protected_metadata_failure =
match protected_metadata_guard.cleanup_created_monitored_paths() {
Ok(paths) => {
if !paths.is_empty() && exit_code == 0 {
exit_code = 1;
}
None
}
Err(err) => {
if exit_code == 0 {
exit_code = 1;
}
Some(format!("protected metadata cleanup failed: {err:#}"))
}
};
let _ = exit_tx.send(exit_code);
unsafe {
@@ -232,7 +251,9 @@ fn finalize_exit(
}
}
if exit_code == 0 {
if let Some(message) = protected_metadata_failure {
log_failure(&command, &message, logs_base_dir);
} else if exit_code == 0 {
log_success(&command, logs_base_dir);
} else {
log_failure(&command, &format!("exit code {exit_code}"), logs_base_dir);
@@ -286,6 +307,7 @@ pub(crate) async fn spawn_windows_sandbox_session_legacy(
timeout_ms: Option<u64>,
tty: bool,
stdin_open: bool,
protected_metadata_targets: &[ProtectedMetadataTarget],
use_private_desktop: bool,
) -> Result<SpawnedProcess> {
let common = prepare_legacy_spawn_context(
@@ -295,7 +317,7 @@ pub(crate) async fn spawn_windows_sandbox_session_legacy(
&mut env_map,
&command,
/*inherit_path*/ false,
/*add_git_safe_directory*/ false,
/*add_git_safe_directory*/ true,
)?;
if !common.policy.has_full_disk_read_access() {
anyhow::bail!("Restricted read-only access requires the elevated Windows sandbox backend");
@@ -304,14 +326,19 @@ pub(crate) async fn spawn_windows_sandbox_session_legacy(
allow_null_device_for_workspace_write(common.is_workspace_write);
let persist_aces = common.is_workspace_write;
let protected_metadata_guard = prepare_protected_metadata_targets(protected_metadata_targets);
let additional_deny_write_paths: Vec<PathBuf> =
protected_metadata_guard.deny_paths().cloned().collect();
let guards = apply_legacy_session_acl_rules(
&common.policy,
sandbox_policy_cwd,
&common.current_dir,
&env_map,
&command,
&security.psid_generic,
security.psid_workspace.as_ref(),
persist_aces,
&additional_deny_write_paths,
);
let (writer_tx, writer_rx) = mpsc::channel::<Vec<u8>>(128);
@@ -404,6 +431,7 @@ pub(crate) async fn spawn_windows_sandbox_session_legacy(
output_join,
guards_for_wait,
cap_sid_for_wait,
protected_metadata_guard,
common.logs_base_dir.as_deref(),
command_for_wait,
);

View File

@@ -6,6 +6,7 @@ use crate::ipc_framed::ResizePayload;
use crate::ipc_framed::StdinPayload;
use crate::ipc_framed::decode_bytes;
use crate::ipc_framed::encode_bytes;
use crate::protected_metadata::ProtectedMetadataGuard;
use anyhow::Result;
use codex_utils_pty::ProcessDriver;
use codex_utils_pty::SpawnedProcess;
@@ -97,6 +98,7 @@ pub(crate) fn start_runner_stdout_reader(
stdout_tx: broadcast::Sender<Vec<u8>>,
stderr_tx: Option<broadcast::Sender<Vec<u8>>>,
exit_tx: oneshot::Sender<i32>,
protected_metadata_guard: Option<ProtectedMetadataGuard>,
) {
std::thread::spawn(move || {
loop {
@@ -140,7 +142,27 @@ pub(crate) fn start_runner_stdout_reader(
}
}
Message::Exit { payload } => {
let _ = exit_tx.send(payload.exit_code);
let mut exit_code = payload.exit_code;
if let Some(protected_metadata_guard) = protected_metadata_guard {
match protected_metadata_guard.cleanup_created_monitored_paths() {
Ok(paths) => {
if !paths.is_empty() && exit_code == 0 {
exit_code = 1;
}
}
Err(err) => {
send_runner_error(
&format!("protected metadata cleanup failed: {err:#}"),
&stdout_tx,
stderr_tx.as_ref(),
);
if exit_code == 0 {
exit_code = 1;
}
}
}
}
let _ = exit_tx.send(exit_code);
break;
}
Message::Error { payload } => {

View File

@@ -9,6 +9,7 @@
mod backends;
use crate::setup::ProtectedMetadataTarget;
use anyhow::Result;
use codex_utils_pty::SpawnedProcess;
use std::collections::HashMap;
@@ -25,6 +26,7 @@ pub async fn spawn_windows_sandbox_session_legacy(
timeout_ms: Option<u64>,
tty: bool,
stdin_open: bool,
protected_metadata_targets: &[ProtectedMetadataTarget],
use_private_desktop: bool,
) -> Result<SpawnedProcess> {
backends::legacy::spawn_windows_sandbox_session_legacy(
@@ -37,6 +39,7 @@ pub async fn spawn_windows_sandbox_session_legacy(
timeout_ms,
tty,
stdin_open,
protected_metadata_targets,
use_private_desktop,
)
.await
@@ -53,6 +56,7 @@ pub async fn spawn_windows_sandbox_session_elevated(
timeout_ms: Option<u64>,
tty: bool,
stdin_open: bool,
protected_metadata_targets: &[ProtectedMetadataTarget],
use_private_desktop: bool,
) -> Result<SpawnedProcess> {
backends::elevated::spawn_windows_sandbox_session_elevated(
@@ -65,6 +69,7 @@ pub async fn spawn_windows_sandbox_session_elevated(
timeout_ms,
tty,
stdin_open,
protected_metadata_targets,
use_private_desktop,
)
.await

View File

@@ -162,6 +162,7 @@ fn legacy_non_tty_cmd_emits_output() {
Some(5_000),
/*tty*/ false,
/*stdin_open*/ false,
&[],
/*use_private_desktop*/ true,
)
.await
@@ -202,6 +203,7 @@ fn legacy_non_tty_powershell_emits_output() {
Some(5_000),
/*tty*/ false,
/*stdin_open*/ false,
&[],
/*use_private_desktop*/ true,
)
.await
@@ -426,6 +428,7 @@ fn legacy_tty_powershell_emits_output_and_accepts_input() {
Some(10_000),
/*tty*/ true,
/*stdin_open*/ true,
&[],
/*use_private_desktop*/ true,
)
.await
@@ -474,6 +477,7 @@ fn legacy_tty_cmd_emits_output_and_accepts_input() {
Some(10_000),
/*tty*/ true,
/*stdin_open*/ true,
&[],
/*use_private_desktop*/ true,
)
.await
@@ -525,6 +529,7 @@ fn legacy_tty_cmd_default_desktop_emits_output_and_accepts_input() {
Some(10_000),
/*tty*/ true,
/*stdin_open*/ true,
&[],
/*use_private_desktop*/ false,
)
.await