mirror of
https://github.com/openai/codex.git
synced 2026-05-06 04:17:03 +00:00
Compare commits
14 Commits
codex/wind
...
codex/wind
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6fbabc5258 | ||
|
|
8f93be5b9e | ||
|
|
6e60556d73 | ||
|
|
0e9394dbd8 | ||
|
|
6df1455723 | ||
|
|
c3bfbc0702 | ||
|
|
e7e0f112fc | ||
|
|
fa5d45227f | ||
|
|
86c1ad7b84 | ||
|
|
29723ac37c | ||
|
|
7404a3c8ec | ||
|
|
7e80ea4e25 | ||
|
|
f0c7ff2155 | ||
|
|
9719de72bf |
@@ -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,7 +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
|
||||
@@ -387,7 +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
|
||||
@@ -461,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::MissingDenySentinel
|
||||
};
|
||||
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>,
|
||||
|
||||
@@ -121,13 +121,18 @@ pub(crate) struct WindowsProtectedMetadataTarget {
|
||||
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.
|
||||
/// Layer: Windows adapter layer. The enforcement layer needs to know whether a
|
||||
/// protected metadata path already exists or must be denied before the command
|
||||
/// can create it.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub(crate) enum WindowsProtectedMetadataMode {
|
||||
/// The protected metadata object exists before launch, so the Windows
|
||||
/// sandbox should deny writes to the object and any canonical target.
|
||||
ExistingDeny,
|
||||
MissingCreationMonitor,
|
||||
/// The protected metadata object is absent before launch, so the Windows
|
||||
/// sandbox should create and deny-list a temporary sentinel before command
|
||||
/// execution can begin.
|
||||
MissingDenySentinel,
|
||||
}
|
||||
|
||||
fn windows_sandbox_uses_elevated_backend(
|
||||
@@ -666,8 +671,8 @@ async fn exec_windows_sandbox(
|
||||
WindowsProtectedMetadataMode::ExistingDeny => {
|
||||
codex_windows_sandbox::ProtectedMetadataMode::ExistingDeny
|
||||
}
|
||||
WindowsProtectedMetadataMode::MissingCreationMonitor => {
|
||||
codex_windows_sandbox::ProtectedMetadataMode::MissingCreationMonitor
|
||||
WindowsProtectedMetadataMode::MissingDenySentinel => {
|
||||
codex_windows_sandbox::ProtectedMetadataMode::MissingDenySentinel
|
||||
}
|
||||
};
|
||||
codex_windows_sandbox::ProtectedMetadataTarget {
|
||||
@@ -1361,7 +1366,7 @@ fn windows_protected_metadata_mode(path: &AbsolutePathBuf) -> WindowsProtectedMe
|
||||
return WindowsProtectedMetadataMode::ExistingDeny;
|
||||
}
|
||||
|
||||
WindowsProtectedMetadataMode::MissingCreationMonitor
|
||||
WindowsProtectedMetadataMode::MissingDenySentinel
|
||||
}
|
||||
|
||||
fn has_reopened_writable_descendant(
|
||||
|
||||
@@ -666,15 +666,15 @@ fn windows_restricted_token_supports_full_read_split_write_read_carveouts() {
|
||||
protected_metadata_targets: vec![
|
||||
WindowsProtectedMetadataTarget {
|
||||
path: cwd.join(".agents"),
|
||||
mode: WindowsProtectedMetadataMode::MissingCreationMonitor,
|
||||
mode: WindowsProtectedMetadataMode::MissingDenySentinel,
|
||||
},
|
||||
WindowsProtectedMetadataTarget {
|
||||
path: cwd.join(".codex"),
|
||||
mode: WindowsProtectedMetadataMode::MissingCreationMonitor,
|
||||
mode: WindowsProtectedMetadataMode::MissingDenySentinel,
|
||||
},
|
||||
WindowsProtectedMetadataTarget {
|
||||
path: cwd.join(".git"),
|
||||
mode: WindowsProtectedMetadataMode::MissingCreationMonitor,
|
||||
mode: WindowsProtectedMetadataMode::MissingDenySentinel,
|
||||
},
|
||||
],
|
||||
}))
|
||||
@@ -778,15 +778,15 @@ fn windows_elevated_supports_split_write_read_carveouts() {
|
||||
protected_metadata_targets: vec![
|
||||
WindowsProtectedMetadataTarget {
|
||||
path: expected_root.join(".agents"),
|
||||
mode: WindowsProtectedMetadataMode::MissingCreationMonitor,
|
||||
mode: WindowsProtectedMetadataMode::MissingDenySentinel,
|
||||
},
|
||||
WindowsProtectedMetadataTarget {
|
||||
path: expected_root.join(".codex"),
|
||||
mode: WindowsProtectedMetadataMode::MissingCreationMonitor,
|
||||
mode: WindowsProtectedMetadataMode::MissingDenySentinel,
|
||||
},
|
||||
WindowsProtectedMetadataTarget {
|
||||
path: expected_root.join(".git"),
|
||||
mode: WindowsProtectedMetadataMode::MissingCreationMonitor,
|
||||
mode: WindowsProtectedMetadataMode::MissingDenySentinel,
|
||||
},
|
||||
],
|
||||
}))
|
||||
@@ -840,11 +840,11 @@ fn windows_metadata_plan_marks_existing_metadata_for_deny() {
|
||||
protected_metadata_targets: vec![
|
||||
WindowsProtectedMetadataTarget {
|
||||
path: cwd.join(".agents"),
|
||||
mode: WindowsProtectedMetadataMode::MissingCreationMonitor,
|
||||
mode: WindowsProtectedMetadataMode::MissingDenySentinel,
|
||||
},
|
||||
WindowsProtectedMetadataTarget {
|
||||
path: cwd.join(".codex"),
|
||||
mode: WindowsProtectedMetadataMode::MissingCreationMonitor,
|
||||
mode: WindowsProtectedMetadataMode::MissingDenySentinel,
|
||||
},
|
||||
WindowsProtectedMetadataTarget {
|
||||
path: cwd.join(".git"),
|
||||
@@ -856,7 +856,7 @@ fn windows_metadata_plan_marks_existing_metadata_for_deny() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_metadata_plan_does_not_materialize_nested_missing_git() {
|
||||
fn windows_metadata_plan_uses_sentinel_for_nested_missing_git() {
|
||||
let temp_dir = tempfile::TempDir::new().expect("tempdir");
|
||||
let repo = dunce::canonicalize(temp_dir.path())
|
||||
.expect("canonical temp dir")
|
||||
@@ -904,21 +904,89 @@ fn windows_metadata_plan_does_not_materialize_nested_missing_git() {
|
||||
protected_metadata_targets: vec![
|
||||
WindowsProtectedMetadataTarget {
|
||||
path: cwd.join(".agents"),
|
||||
mode: WindowsProtectedMetadataMode::MissingCreationMonitor,
|
||||
mode: WindowsProtectedMetadataMode::MissingDenySentinel,
|
||||
},
|
||||
WindowsProtectedMetadataTarget {
|
||||
path: cwd.join(".codex"),
|
||||
mode: WindowsProtectedMetadataMode::MissingCreationMonitor,
|
||||
mode: WindowsProtectedMetadataMode::MissingDenySentinel,
|
||||
},
|
||||
WindowsProtectedMetadataTarget {
|
||||
path: cwd.join(".git"),
|
||||
mode: WindowsProtectedMetadataMode::MissingCreationMonitor,
|
||||
mode: WindowsProtectedMetadataMode::MissingDenySentinel,
|
||||
},
|
||||
],
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_shell_runtime_path_resolves_metadata_overrides() {
|
||||
let temp_dir = tempfile::TempDir::new().expect("tempdir");
|
||||
let cwd = dunce::canonicalize(temp_dir.path())
|
||||
.expect("canonical temp dir")
|
||||
.abs();
|
||||
let manager = codex_sandboxing::SandboxManager::new();
|
||||
let permissions = PermissionProfile::workspace_write_with(
|
||||
&[],
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
/*exclude_tmpdir_env_var*/ true,
|
||||
/*exclude_slash_tmp*/ true,
|
||||
);
|
||||
let request = manager
|
||||
.transform(codex_sandboxing::SandboxTransformRequest {
|
||||
command: codex_sandboxing::SandboxCommand {
|
||||
program: "cmd.exe".into(),
|
||||
args: vec!["/c".to_string(), "echo ok".to_string()],
|
||||
cwd: cwd.clone(),
|
||||
env: HashMap::new(),
|
||||
additional_permissions: None,
|
||||
},
|
||||
permissions: &permissions,
|
||||
sandbox: SandboxType::WindowsRestrictedToken,
|
||||
enforce_managed_network: false,
|
||||
network: None,
|
||||
sandbox_policy_cwd: &cwd,
|
||||
codex_linux_sandbox_exe: None,
|
||||
use_legacy_landlock: false,
|
||||
windows_sandbox_level: WindowsSandboxLevel::RestrictedToken,
|
||||
windows_sandbox_private_desktop: false,
|
||||
})
|
||||
.expect("transform");
|
||||
let mut exec_request = crate::sandboxing::ExecRequest::from_sandbox_exec_request(
|
||||
request,
|
||||
crate::sandboxing::ExecOptions {
|
||||
expiration: ExecExpiration::DefaultTimeout,
|
||||
capture_policy: ExecCapturePolicy::ShellTool,
|
||||
},
|
||||
cwd.clone(),
|
||||
);
|
||||
|
||||
assert_eq!(exec_request.windows_sandbox_filesystem_overrides, None);
|
||||
|
||||
ensure_windows_sandbox_filesystem_overrides(&mut exec_request).expect("resolve overrides");
|
||||
|
||||
let overrides = exec_request
|
||||
.windows_sandbox_filesystem_overrides
|
||||
.expect("metadata overrides");
|
||||
assert_eq!(
|
||||
overrides.protected_metadata_targets,
|
||||
vec![
|
||||
WindowsProtectedMetadataTarget {
|
||||
path: cwd.join(".agents"),
|
||||
mode: WindowsProtectedMetadataMode::MissingDenySentinel,
|
||||
},
|
||||
WindowsProtectedMetadataTarget {
|
||||
path: cwd.join(".codex"),
|
||||
mode: WindowsProtectedMetadataMode::MissingDenySentinel,
|
||||
},
|
||||
WindowsProtectedMetadataTarget {
|
||||
path: cwd.join(".git"),
|
||||
mode: WindowsProtectedMetadataMode::MissingDenySentinel,
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_elevated_rejects_unreadable_split_carveouts() {
|
||||
let temp_dir = tempfile::TempDir::new().expect("tempdir");
|
||||
|
||||
@@ -190,8 +190,8 @@ fn protected_metadata_targets_for_windows_session(
|
||||
WindowsProtectedMetadataMode::ExistingDeny => {
|
||||
codex_windows_sandbox::ProtectedMetadataMode::ExistingDeny
|
||||
}
|
||||
WindowsProtectedMetadataMode::MissingCreationMonitor => {
|
||||
codex_windows_sandbox::ProtectedMetadataMode::MissingCreationMonitor
|
||||
WindowsProtectedMetadataMode::MissingDenySentinel => {
|
||||
codex_windows_sandbox::ProtectedMetadataMode::MissingDenySentinel
|
||||
}
|
||||
};
|
||||
codex_windows_sandbox::ProtectedMetadataTarget {
|
||||
|
||||
@@ -183,15 +183,15 @@ fn open_session_prepares_windows_metadata_overrides_for_unified_exec() {
|
||||
vec![
|
||||
crate::exec::WindowsProtectedMetadataTarget {
|
||||
path: cwd.join(".agents"),
|
||||
mode: crate::exec::WindowsProtectedMetadataMode::MissingCreationMonitor,
|
||||
mode: crate::exec::WindowsProtectedMetadataMode::MissingDenySentinel,
|
||||
},
|
||||
crate::exec::WindowsProtectedMetadataTarget {
|
||||
path: cwd.join(".codex"),
|
||||
mode: crate::exec::WindowsProtectedMetadataMode::MissingCreationMonitor,
|
||||
mode: crate::exec::WindowsProtectedMetadataMode::MissingDenySentinel,
|
||||
},
|
||||
crate::exec::WindowsProtectedMetadataTarget {
|
||||
path: cwd.join(".git"),
|
||||
mode: crate::exec::WindowsProtectedMetadataMode::MissingCreationMonitor,
|
||||
mode: crate::exec::WindowsProtectedMetadataMode::MissingDenySentinel,
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
@@ -7,4 +7,5 @@ codex_rust_crate(
|
||||
"Cargo.toml",
|
||||
"codex-windows-sandbox-setup.manifest",
|
||||
],
|
||||
unit_test_timeout = "long",
|
||||
)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -26,6 +26,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;
|
||||
@@ -254,8 +255,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ pub struct ElevatedSandboxCaptureRequest<'a> {
|
||||
|
||||
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;
|
||||
@@ -39,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;
|
||||
@@ -141,6 +143,9 @@ mod windows_impl {
|
||||
|
||||
let logs_base_dir: Option<&Path> = Some(sandbox_base.as_path());
|
||||
log_start(&command, logs_base_dir);
|
||||
let mut protected_metadata_guard =
|
||||
prepare_protected_metadata_targets(protected_metadata_targets)?;
|
||||
protected_metadata_guard.arm_sentinel_cleanup()?;
|
||||
let sandbox_creds = require_logon_sandbox_creds(
|
||||
&policy,
|
||||
sandbox_policy_cwd,
|
||||
@@ -186,7 +191,9 @@ mod windows_impl {
|
||||
|
||||
unsafe {
|
||||
allow_null_device(psid_to_use);
|
||||
allow_named_pipe_device(psid_to_use);
|
||||
}
|
||||
let protected_metadata_runtime = protected_metadata_guard.into_runtime()?;
|
||||
|
||||
(|| -> Result<CaptureResult> {
|
||||
let spawn_request = SpawnRequest {
|
||||
@@ -215,7 +222,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 {
|
||||
@@ -239,6 +246,11 @@ mod windows_impl {
|
||||
}
|
||||
};
|
||||
|
||||
let protected_metadata_violations = protected_metadata_runtime.finish()?;
|
||||
if !protected_metadata_violations.is_empty() && exit_code == 0 {
|
||||
exit_code = 1;
|
||||
}
|
||||
|
||||
if exit_code == 0 {
|
||||
log_success(&command, logs_base_dir);
|
||||
} else {
|
||||
|
||||
@@ -79,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")]
|
||||
@@ -170,6 +172,10 @@ 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::ensure_missing_deny_sentinel;
|
||||
#[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;
|
||||
@@ -261,7 +267,10 @@ 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;
|
||||
@@ -272,7 +281,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;
|
||||
@@ -289,6 +301,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;
|
||||
@@ -366,7 +380,7 @@ mod windows_impl {
|
||||
mut env_map: HashMap<String, String>,
|
||||
timeout_ms: Option<u64>,
|
||||
additional_deny_write_paths: &[PathBuf],
|
||||
_protected_metadata_targets: &[ProtectedMetadataTarget],
|
||||
protected_metadata_targets: &[ProtectedMetadataTarget],
|
||||
use_private_desktop: bool,
|
||||
) -> Result<CaptureResult> {
|
||||
let common = prepare_legacy_spawn_context(
|
||||
@@ -376,7 +390,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;
|
||||
@@ -428,6 +442,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);
|
||||
}
|
||||
@@ -436,14 +451,47 @@ mod windows_impl {
|
||||
let persist_aces = is_workspace_write;
|
||||
let AllowDenyPaths { allow, mut deny } =
|
||||
compute_allow_paths(&policy, sandbox_policy_cwd, ¤t_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 mut 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());
|
||||
}
|
||||
}
|
||||
protected_metadata_guard.arm_sentinel_cleanup()?;
|
||||
let canonical_cwd = canonicalize_path(¤t_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)
|
||||
@@ -471,10 +519,13 @@ 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 protected_metadata_runtime = protected_metadata_guard.into_runtime()?;
|
||||
let (stdin_pair, stdout_pair, stderr_pair) = unsafe { setup_stdio_pipes()? };
|
||||
let ((in_r, in_w), (out_r, out_w), (err_r, err_w)) = (stdin_pair, stdout_pair, stderr_pair);
|
||||
let spawn_res = unsafe {
|
||||
@@ -586,11 +637,15 @@ 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_runtime.finish()?;
|
||||
if !protected_metadata_violations.is_empty() && exit_code == 0 {
|
||||
exit_code = 1;
|
||||
}
|
||||
|
||||
if exit_code == 0 {
|
||||
log_success(&command, logs_base_dir);
|
||||
@@ -651,7 +706,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(())
|
||||
|
||||
@@ -1,24 +1,57 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use crate::setup::ProtectedMetadataMode;
|
||||
use crate::setup::ProtectedMetadataTarget;
|
||||
use crate::winutil::to_wide;
|
||||
use anyhow::anyhow;
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use std::collections::HashMap;
|
||||
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 std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use std::thread;
|
||||
use windows_sys::Win32::Foundation::CloseHandle;
|
||||
use windows_sys::Win32::Foundation::FALSE;
|
||||
use windows_sys::Win32::Foundation::HANDLE;
|
||||
use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
|
||||
use windows_sys::Win32::Foundation::TRUE;
|
||||
use windows_sys::Win32::Foundation::WAIT_FAILED;
|
||||
use windows_sys::Win32::Foundation::WAIT_OBJECT_0;
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_REPARSE_POINT;
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_FLAG_BACKUP_SEMANTICS;
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_FLAG_DELETE_ON_CLOSE;
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_NOTIFY_CHANGE_CREATION;
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_NOTIFY_CHANGE_DIR_NAME;
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_NOTIFY_CHANGE_FILE_NAME;
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_SHARE_DELETE;
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_SHARE_READ;
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_SHARE_WRITE;
|
||||
use windows_sys::Win32::Storage::FileSystem::CreateFileW;
|
||||
use windows_sys::Win32::Storage::FileSystem::DELETE;
|
||||
use windows_sys::Win32::Storage::FileSystem::FindCloseChangeNotification;
|
||||
use windows_sys::Win32::Storage::FileSystem::FindFirstChangeNotificationW;
|
||||
use windows_sys::Win32::Storage::FileSystem::FindNextChangeNotification;
|
||||
use windows_sys::Win32::Storage::FileSystem::OPEN_EXISTING;
|
||||
use windows_sys::Win32::System::Threading::CreateEventW;
|
||||
use windows_sys::Win32::System::Threading::INFINITE;
|
||||
use windows_sys::Win32::System::Threading::SetEvent;
|
||||
use windows_sys::Win32::System::Threading::WaitForMultipleObjects;
|
||||
|
||||
/// Layer: Windows enforcement layer. Existing metadata objects can be protected
|
||||
/// with ACLs; missing names are monitored and removed if the sandbox creates
|
||||
/// them.
|
||||
/// with ACLs. Missing names are materialized as empty deny sentinels when the
|
||||
/// caller needs pre-command creation denial, or monitored and removed after
|
||||
/// creation when the caller explicitly requests reactive cleanup.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ProtectedMetadataGuard {
|
||||
deny_paths: Vec<PathBuf>,
|
||||
monitored_paths: Vec<PathBuf>,
|
||||
sentinel_paths: Vec<PathBuf>,
|
||||
sentinel_handles: Vec<SentinelHandle>,
|
||||
}
|
||||
|
||||
impl ProtectedMetadataGuard {
|
||||
@@ -26,9 +59,26 @@ impl ProtectedMetadataGuard {
|
||||
self.deny_paths.iter()
|
||||
}
|
||||
|
||||
pub(crate) fn cleanup_created_monitored_paths(&self) -> Result<Vec<PathBuf>> {
|
||||
pub(crate) fn arm_sentinel_cleanup(&mut self) -> Result<()> {
|
||||
for path in &self.sentinel_paths {
|
||||
self.sentinel_handles
|
||||
.push(open_delete_on_close_directory(path)?);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn into_runtime(self) -> Result<ProtectedMetadataRuntime> {
|
||||
let monitor = MissingCreationMonitor::start(&self.monitored_paths)?;
|
||||
Ok(ProtectedMetadataRuntime {
|
||||
guard: self,
|
||||
monitor,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn cleanup_created_paths(&mut self) -> Result<Vec<PathBuf>> {
|
||||
self.sentinel_handles.clear();
|
||||
let mut removed = Vec::new();
|
||||
for path in &self.monitored_paths {
|
||||
for path in self.monitored_paths.iter().chain(self.sentinel_paths.iter()) {
|
||||
let Some(existing_path) = existing_metadata_path(path)? else {
|
||||
continue;
|
||||
};
|
||||
@@ -40,11 +90,317 @@ impl ProtectedMetadataGuard {
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ProtectedMetadataGuard {
|
||||
fn drop(&mut self) {
|
||||
self.sentinel_handles.clear();
|
||||
for path in &self.sentinel_paths {
|
||||
let _ = remove_metadata_path(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Layer: Windows enforcement runtime. Owns the prepared guard plus any active
|
||||
/// OS change listeners for the lifetime of one sandboxed command, then reports
|
||||
/// whether protected metadata was created during that command.
|
||||
pub(crate) struct ProtectedMetadataRuntime {
|
||||
guard: ProtectedMetadataGuard,
|
||||
monitor: MissingCreationMonitor,
|
||||
}
|
||||
|
||||
impl ProtectedMetadataRuntime {
|
||||
pub(crate) fn finish(mut self) -> Result<Vec<PathBuf>> {
|
||||
let monitor_result = self.monitor.finish();
|
||||
let cleanup_result = self.guard.cleanup_created_paths();
|
||||
match (monitor_result, cleanup_result) {
|
||||
(Ok(mut removed), Ok(cleaned)) => {
|
||||
removed.extend(cleaned);
|
||||
Ok(unique_paths(removed))
|
||||
}
|
||||
(Err(err), Ok(_)) | (Ok(_), Err(err)) => Err(err),
|
||||
(Err(monitor_err), Err(cleanup_err)) => Err(anyhow!(
|
||||
"protected metadata monitor failed: {monitor_err:#}; cleanup also failed: {cleanup_err:#}"
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Layer: Windows sentinel cleanup handle. Holds a delete-on-close directory
|
||||
/// handle for one Codex-created sentinel so forced parent-process termination
|
||||
/// does not leave a protected metadata artifact behind.
|
||||
#[derive(Debug)]
|
||||
struct SentinelHandle(HANDLE);
|
||||
|
||||
impl Drop for SentinelHandle {
|
||||
fn drop(&mut self) {
|
||||
if self.0 != 0 && self.0 != INVALID_HANDLE_VALUE {
|
||||
unsafe {
|
||||
CloseHandle(self.0);
|
||||
}
|
||||
self.0 = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Layer: Windows OS-event listener. Watches parents of missing protected
|
||||
/// metadata names and removes matching filesystem objects as soon as Windows
|
||||
/// reports creation, rename, or file-name changes under those parents.
|
||||
struct MissingCreationMonitor {
|
||||
stop_event: HANDLE,
|
||||
listeners: Vec<thread::JoinHandle<()>>,
|
||||
removed_paths: Arc<Mutex<Vec<PathBuf>>>,
|
||||
errors: Arc<Mutex<Vec<String>>>,
|
||||
}
|
||||
|
||||
impl MissingCreationMonitor {
|
||||
fn start(paths: &[PathBuf]) -> Result<Self> {
|
||||
if paths.is_empty() {
|
||||
return Ok(Self {
|
||||
stop_event: 0,
|
||||
listeners: Vec::new(),
|
||||
removed_paths: Arc::new(Mutex::new(Vec::new())),
|
||||
errors: Arc::new(Mutex::new(Vec::new())),
|
||||
});
|
||||
}
|
||||
|
||||
let stop_event = unsafe { CreateEventW(std::ptr::null(), TRUE, FALSE, std::ptr::null()) };
|
||||
if stop_event == 0 {
|
||||
return Err(anyhow!(
|
||||
"failed to create protected metadata monitor stop event: {}",
|
||||
io::Error::last_os_error()
|
||||
));
|
||||
}
|
||||
|
||||
let mut monitor = Self {
|
||||
stop_event,
|
||||
listeners: Vec::new(),
|
||||
removed_paths: Arc::new(Mutex::new(Vec::new())),
|
||||
errors: Arc::new(Mutex::new(Vec::new())),
|
||||
};
|
||||
|
||||
for (parent, watched_paths) in monitored_paths_by_parent(paths) {
|
||||
match monitor.spawn_listener(parent, watched_paths) {
|
||||
Ok(listener) => monitor.listeners.push(listener),
|
||||
Err(err) => {
|
||||
monitor.stop_listeners();
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(monitor)
|
||||
}
|
||||
|
||||
fn spawn_listener(
|
||||
&self,
|
||||
parent: PathBuf,
|
||||
watched_paths: Vec<PathBuf>,
|
||||
) -> Result<thread::JoinHandle<()>> {
|
||||
let parent_wide = to_wide(&parent);
|
||||
let change_handle = unsafe {
|
||||
FindFirstChangeNotificationW(
|
||||
parent_wide.as_ptr(),
|
||||
FALSE,
|
||||
FILE_NOTIFY_CHANGE_FILE_NAME
|
||||
| FILE_NOTIFY_CHANGE_DIR_NAME
|
||||
| FILE_NOTIFY_CHANGE_CREATION,
|
||||
)
|
||||
};
|
||||
if change_handle == INVALID_HANDLE_VALUE {
|
||||
return Err(anyhow!(
|
||||
"failed to monitor protected metadata parent {}: {}",
|
||||
parent.display(),
|
||||
io::Error::last_os_error()
|
||||
));
|
||||
}
|
||||
|
||||
let stop_event = self.stop_event;
|
||||
let removed_paths = Arc::clone(&self.removed_paths);
|
||||
let errors = Arc::clone(&self.errors);
|
||||
let parent_display = parent.display().to_string();
|
||||
let parent_display_for_listener = parent_display.clone();
|
||||
thread::Builder::new()
|
||||
.name("codex-protected-metadata-monitor".to_string())
|
||||
.spawn(move || {
|
||||
enforce_monitored_paths(&watched_paths, &removed_paths, &errors);
|
||||
loop {
|
||||
let handles = [change_handle, stop_event];
|
||||
let wait_result = unsafe {
|
||||
WaitForMultipleObjects(
|
||||
handles.len() as u32,
|
||||
handles.as_ptr(),
|
||||
FALSE,
|
||||
INFINITE,
|
||||
)
|
||||
};
|
||||
|
||||
if wait_result == WAIT_OBJECT_0 {
|
||||
enforce_monitored_paths(&watched_paths, &removed_paths, &errors);
|
||||
if unsafe { FindNextChangeNotification(change_handle) } == 0 {
|
||||
record_monitor_error(
|
||||
&errors,
|
||||
format!(
|
||||
"failed to resume protected metadata monitor for {}: {}",
|
||||
parent_display_for_listener,
|
||||
io::Error::last_os_error()
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
} else if wait_result == WAIT_OBJECT_0 + 1 {
|
||||
break;
|
||||
} else if wait_result == WAIT_FAILED {
|
||||
record_monitor_error(
|
||||
&errors,
|
||||
format!(
|
||||
"failed while waiting for protected metadata changes under {}: {}",
|
||||
parent_display_for_listener,
|
||||
io::Error::last_os_error()
|
||||
),
|
||||
);
|
||||
break;
|
||||
} else {
|
||||
record_monitor_error(
|
||||
&errors,
|
||||
format!(
|
||||
"unexpected protected metadata wait result {wait_result} for {parent_display_for_listener}"
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
unsafe {
|
||||
FindCloseChangeNotification(change_handle);
|
||||
}
|
||||
})
|
||||
.map_err(|err| {
|
||||
unsafe {
|
||||
FindCloseChangeNotification(change_handle);
|
||||
}
|
||||
anyhow!(
|
||||
"failed to start protected metadata monitor for {parent_display}: {err}"
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn finish(&mut self) -> Result<Vec<PathBuf>> {
|
||||
self.stop_listeners();
|
||||
let errors = self
|
||||
.errors
|
||||
.lock()
|
||||
.map_err(|_| anyhow!("protected metadata monitor error state is poisoned"))?
|
||||
.clone();
|
||||
if !errors.is_empty() {
|
||||
return Err(anyhow!(
|
||||
"protected metadata monitor failed: {}",
|
||||
errors.join("; ")
|
||||
));
|
||||
}
|
||||
|
||||
let removed = self
|
||||
.removed_paths
|
||||
.lock()
|
||||
.map_err(|_| anyhow!("protected metadata monitor removal state is poisoned"))?
|
||||
.clone();
|
||||
Ok(unique_paths(removed))
|
||||
}
|
||||
|
||||
fn stop_listeners(&mut self) {
|
||||
if self.stop_event != 0 {
|
||||
if unsafe { SetEvent(self.stop_event) } == 0 {
|
||||
record_monitor_error(
|
||||
&self.errors,
|
||||
format!(
|
||||
"failed to stop protected metadata monitor: {}",
|
||||
io::Error::last_os_error()
|
||||
),
|
||||
);
|
||||
}
|
||||
while let Some(listener) = self.listeners.pop() {
|
||||
if listener.join().is_err() {
|
||||
record_monitor_error(
|
||||
&self.errors,
|
||||
"protected metadata monitor listener panicked".to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
unsafe {
|
||||
CloseHandle(self.stop_event);
|
||||
}
|
||||
self.stop_event = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for MissingCreationMonitor {
|
||||
fn drop(&mut self) {
|
||||
self.stop_listeners();
|
||||
}
|
||||
}
|
||||
|
||||
fn monitored_paths_by_parent(paths: &[PathBuf]) -> Vec<(PathBuf, Vec<PathBuf>)> {
|
||||
let mut grouped: HashMap<String, (PathBuf, Vec<PathBuf>)> = HashMap::new();
|
||||
for path in paths {
|
||||
let Some(parent) = path.parent() else {
|
||||
continue;
|
||||
};
|
||||
let entry = grouped
|
||||
.entry(path_text_key(parent))
|
||||
.or_insert_with(|| (parent.to_path_buf(), Vec::new()));
|
||||
entry.1.push(path.clone());
|
||||
}
|
||||
grouped.into_values().collect()
|
||||
}
|
||||
|
||||
fn enforce_monitored_paths(
|
||||
paths: &[PathBuf],
|
||||
removed_paths: &Arc<Mutex<Vec<PathBuf>>>,
|
||||
errors: &Arc<Mutex<Vec<String>>>,
|
||||
) {
|
||||
for path in paths {
|
||||
match existing_metadata_path(path) {
|
||||
Ok(Some(existing_path)) => {
|
||||
if let Err(err) = remove_metadata_path(&existing_path) {
|
||||
record_monitor_error(
|
||||
errors,
|
||||
format!(
|
||||
"failed to remove protected metadata {}: {err:#}",
|
||||
existing_path.display()
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
match removed_paths.lock() {
|
||||
Ok(mut removed) => removed.push(existing_path),
|
||||
Err(_) => record_monitor_error(
|
||||
errors,
|
||||
"protected metadata monitor removal state is poisoned".to_string(),
|
||||
),
|
||||
}
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(err) => record_monitor_error(
|
||||
errors,
|
||||
format!(
|
||||
"failed to inspect protected metadata {}: {err:#}",
|
||||
path.display()
|
||||
),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn record_monitor_error(errors: &Arc<Mutex<Vec<String>>>, message: String) {
|
||||
if let Ok(mut errors) = errors.lock() {
|
||||
errors.push(message);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn prepare_protected_metadata_targets(
|
||||
targets: &[ProtectedMetadataTarget],
|
||||
) -> ProtectedMetadataGuard {
|
||||
) -> Result<ProtectedMetadataGuard> {
|
||||
let mut deny_paths = Vec::new();
|
||||
let mut monitored_paths = Vec::new();
|
||||
let mut sentinel_paths = Vec::new();
|
||||
for target in targets {
|
||||
match target.mode {
|
||||
ProtectedMetadataMode::ExistingDeny => {
|
||||
@@ -53,25 +409,75 @@ pub(crate) fn prepare_protected_metadata_targets(
|
||||
ProtectedMetadataMode::MissingCreationMonitor => {
|
||||
monitored_paths.push(target.path.clone());
|
||||
}
|
||||
ProtectedMetadataMode::MissingDenySentinel => {
|
||||
let created = ensure_missing_deny_sentinel(&target.path)?;
|
||||
let existing_deny_paths = protected_metadata_existing_deny_paths(&target.path);
|
||||
if existing_deny_paths.is_empty() {
|
||||
deny_paths.push(target.path.clone());
|
||||
} else {
|
||||
deny_paths.extend(existing_deny_paths);
|
||||
}
|
||||
if created {
|
||||
sentinel_paths.push(target.path.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ProtectedMetadataGuard {
|
||||
Ok(ProtectedMetadataGuard {
|
||||
deny_paths,
|
||||
monitored_paths,
|
||||
}
|
||||
sentinel_paths,
|
||||
sentinel_handles: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn protected_metadata_existing_deny_paths(path: &Path) -> Vec<PathBuf> {
|
||||
if std::fs::symlink_metadata(path).is_ok() {
|
||||
vec![path.to_path_buf()]
|
||||
} else {
|
||||
Vec::new()
|
||||
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 unique_paths(paths: Vec<PathBuf>) -> Vec<PathBuf> {
|
||||
let mut seen = HashSet::new();
|
||||
let mut unique = Vec::new();
|
||||
for path in paths {
|
||||
if seen.insert(path_text_key(&path)) {
|
||||
unique.push(path);
|
||||
}
|
||||
}
|
||||
unique
|
||||
}
|
||||
|
||||
fn existing_metadata_path(path: &Path) -> Result<Option<PathBuf>> {
|
||||
match std::fs::symlink_metadata(path) {
|
||||
Ok(_) => return Ok(Some(path.to_path_buf())),
|
||||
Ok(_) => return Ok(matching_metadata_child(path)?.or_else(|| Some(path.to_path_buf()))),
|
||||
Err(err) if err.kind() == io::ErrorKind::NotFound => {}
|
||||
Err(err) => {
|
||||
return Err(err)
|
||||
@@ -79,6 +485,10 @@ fn existing_metadata_path(path: &Path) -> Result<Option<PathBuf>> {
|
||||
}
|
||||
}
|
||||
|
||||
matching_metadata_child(path)
|
||||
}
|
||||
|
||||
fn matching_metadata_child(path: &Path) -> Result<Option<PathBuf>> {
|
||||
let Some(parent) = path.parent() else {
|
||||
return Ok(None);
|
||||
};
|
||||
@@ -138,6 +548,47 @@ fn remove_metadata_path(path: &Path) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Creates an empty sentinel directory for a missing protected metadata name.
|
||||
///
|
||||
/// Returns true when this call created the sentinel. If the target already
|
||||
/// exists by the time enforcement prepares, callers should still deny it, but
|
||||
/// must not claim it for cleanup as a Codex-created sentinel.
|
||||
pub fn ensure_missing_deny_sentinel(path: &Path) -> Result<bool> {
|
||||
if existing_metadata_path(path)?.is_some() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
match std::fs::create_dir(path) {
|
||||
Ok(()) => Ok(true),
|
||||
Err(err) if err.kind() == io::ErrorKind::AlreadyExists => Ok(false),
|
||||
Err(err) => Err(err)
|
||||
.with_context(|| format!("failed to create protected metadata sentinel {}", path.display())),
|
||||
}
|
||||
}
|
||||
|
||||
fn open_delete_on_close_directory(path: &Path) -> Result<SentinelHandle> {
|
||||
let path_wide = to_wide(path);
|
||||
let handle = unsafe {
|
||||
CreateFileW(
|
||||
path_wide.as_ptr(),
|
||||
DELETE,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
||||
std::ptr::null_mut(),
|
||||
OPEN_EXISTING,
|
||||
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_DELETE_ON_CLOSE,
|
||||
0,
|
||||
)
|
||||
};
|
||||
if handle == INVALID_HANDLE_VALUE {
|
||||
return Err(anyhow!(
|
||||
"failed to arm protected metadata sentinel cleanup for {}: {}",
|
||||
path.display(),
|
||||
io::Error::last_os_error()
|
||||
));
|
||||
}
|
||||
Ok(SentinelHandle(handle))
|
||||
}
|
||||
|
||||
fn is_directory_reparse_point(metadata: &Metadata) -> bool {
|
||||
metadata.is_dir() && (metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT) != 0
|
||||
}
|
||||
@@ -147,19 +598,22 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::setup::ProtectedMetadataMode;
|
||||
use crate::setup::ProtectedMetadataTarget;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
#[test]
|
||||
fn cleanup_created_monitored_paths_removes_case_variant() {
|
||||
fn cleanup_created_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 {
|
||||
let mut guard = prepare_protected_metadata_targets(&[ProtectedMetadataTarget {
|
||||
path: target.clone(),
|
||||
mode: ProtectedMetadataMode::MissingCreationMonitor,
|
||||
}]);
|
||||
}])
|
||||
.expect("guard");
|
||||
|
||||
let removed = guard.cleanup_created_monitored_paths().expect("cleanup");
|
||||
let removed = guard.cleanup_created_paths().expect("cleanup");
|
||||
assert_eq!(removed.len(), 1);
|
||||
assert!(
|
||||
removed[0]
|
||||
@@ -171,4 +625,108 @@ mod tests {
|
||||
assert!(!target.exists());
|
||||
assert!(!created.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_creation_monitor_removes_created_case_variant() {
|
||||
let temp_dir = tempfile::TempDir::new().expect("tempdir");
|
||||
let target = temp_dir.path().join(".git");
|
||||
let created = temp_dir.path().join(".GIT");
|
||||
let runtime = prepare_protected_metadata_targets(&[ProtectedMetadataTarget {
|
||||
path: target,
|
||||
mode: ProtectedMetadataMode::MissingCreationMonitor,
|
||||
}])
|
||||
.expect("guard")
|
||||
.into_runtime()
|
||||
.expect("runtime");
|
||||
|
||||
std::fs::create_dir_all(&created).expect("create metadata");
|
||||
let deadline = Instant::now() + Duration::from_secs(2);
|
||||
while created.exists() && Instant::now() < deadline {
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
}
|
||||
|
||||
assert!(
|
||||
!created.exists(),
|
||||
"monitor should remove protected metadata before final cleanup"
|
||||
);
|
||||
let removed = runtime.finish().expect("finish");
|
||||
assert!(
|
||||
removed
|
||||
.iter()
|
||||
.any(|path| path.file_name().and_then(std::ffi::OsStr::to_str) == Some(".GIT")),
|
||||
"removed paths should include the created case variant: {removed:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[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,
|
||||
}])
|
||||
.expect("guard");
|
||||
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:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_deny_sentinel_creates_and_cleans_path() {
|
||||
let temp_dir = tempfile::TempDir::new().expect("tempdir");
|
||||
let target = temp_dir.path().join(".git");
|
||||
|
||||
let mut guard = prepare_protected_metadata_targets(&[ProtectedMetadataTarget {
|
||||
path: target.clone(),
|
||||
mode: ProtectedMetadataMode::MissingDenySentinel,
|
||||
}])
|
||||
.expect("guard");
|
||||
|
||||
assert!(target.is_dir(), "sentinel directory should be created");
|
||||
assert!(
|
||||
guard.deny_paths().any(|path| path_text_key(path) == path_text_key(&target)),
|
||||
"sentinel should be deny-listed"
|
||||
);
|
||||
|
||||
let removed = guard.cleanup_created_paths().expect("cleanup");
|
||||
assert_eq!(removed, vec![target.clone()]);
|
||||
assert!(!target.exists(), "sentinel directory should be removed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_deny_sentinel_does_not_cleanup_preexisting_path() {
|
||||
let temp_dir = tempfile::TempDir::new().expect("tempdir");
|
||||
let target = temp_dir.path().join(".git");
|
||||
std::fs::create_dir_all(&target).expect("create metadata");
|
||||
|
||||
let mut guard = prepare_protected_metadata_targets(&[ProtectedMetadataTarget {
|
||||
path: target.clone(),
|
||||
mode: ProtectedMetadataMode::MissingDenySentinel,
|
||||
}])
|
||||
.expect("guard");
|
||||
|
||||
let removed = guard.cleanup_created_paths().expect("cleanup");
|
||||
assert!(removed.is_empty(), "pre-existing metadata is not Codex-owned cleanup");
|
||||
assert!(target.exists(), "pre-existing metadata should not be removed");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ 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;
|
||||
@@ -18,6 +19,7 @@ use codex_windows_sandbox::canonicalize_path;
|
||||
use codex_windows_sandbox::convert_string_sid_to_sid;
|
||||
use codex_windows_sandbox::ensure_allow_mask_aces_with_inheritance;
|
||||
use codex_windows_sandbox::ensure_allow_write_aces;
|
||||
use codex_windows_sandbox::ensure_missing_deny_sentinel;
|
||||
use codex_windows_sandbox::extract_setup_failure;
|
||||
use codex_windows_sandbox::hide_newly_created_users;
|
||||
use codex_windows_sandbox::install_wfp_filters;
|
||||
@@ -25,6 +27,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;
|
||||
@@ -77,6 +80,8 @@ use sandbox_users::resolve_sandbox_users_group_sid;
|
||||
use sandbox_users::resolve_sid;
|
||||
use sandbox_users::sid_bytes_to_psid;
|
||||
|
||||
/// Layer: Windows enforcement request boundary. Helper-process copy of the
|
||||
/// setup payload decoded from the orchestrator.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
struct Payload {
|
||||
version: u32,
|
||||
@@ -88,7 +93,6 @@ struct Payload {
|
||||
write_roots: Vec<PathBuf>,
|
||||
#[serde(default)]
|
||||
deny_write_paths: Vec<PathBuf>,
|
||||
#[allow(dead_code)]
|
||||
#[serde(default)]
|
||||
protected_metadata_targets: Vec<ProtectedMetadataTarget>,
|
||||
proxy_ports: Vec<u16>,
|
||||
@@ -820,6 +824,72 @@ fn run_setup_full(payload: &Payload, log: &mut File, sbx_dir: &Path) -> Result<(
|
||||
}
|
||||
}
|
||||
|
||||
for target in &payload.protected_metadata_targets {
|
||||
let deny_paths = match target.mode {
|
||||
ProtectedMetadataMode::ExistingDeny => {
|
||||
protected_metadata_existing_deny_paths(&target.path)
|
||||
}
|
||||
ProtectedMetadataMode::MissingCreationMonitor => continue,
|
||||
ProtectedMetadataMode::MissingDenySentinel => {
|
||||
ensure_missing_deny_sentinel(&target.path)?;
|
||||
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,
|
||||
|
||||
@@ -35,7 +35,7 @@ use windows_sys::Win32::Security::CheckTokenMembership;
|
||||
use windows_sys::Win32::Security::FreeSid;
|
||||
use windows_sys::Win32::Security::SECURITY_NT_AUTHORITY;
|
||||
|
||||
pub const SETUP_VERSION: u32 = 5;
|
||||
pub const SETUP_VERSION: u32 = 6;
|
||||
pub const OFFLINE_USERNAME: &str = "CodexSandboxOffline";
|
||||
pub const ONLINE_USERNAME: &str = "CodexSandboxOnline";
|
||||
const ERROR_CANCELLED: u32 = 1223;
|
||||
@@ -91,6 +91,9 @@ pub struct SandboxSetupRequest<'a> {
|
||||
pub proxy_enforced: bool,
|
||||
}
|
||||
|
||||
/// Layer: Windows enforcement request boundary. These overrides are already
|
||||
/// projected by the adapter layer; setup code only packages them for the helper
|
||||
/// process.
|
||||
#[derive(Default)]
|
||||
pub struct SetupRootOverrides {
|
||||
pub read_roots: Option<Vec<PathBuf>>,
|
||||
@@ -109,12 +112,20 @@ pub struct ProtectedMetadataTarget {
|
||||
}
|
||||
|
||||
/// Layer: Windows enforcement request boundary. The helper must distinguish
|
||||
/// existing metadata objects from missing names that need create monitoring.
|
||||
/// existing metadata objects from missing names that need pre-command denial or
|
||||
/// reactive cleanup.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum ProtectedMetadataMode {
|
||||
/// Protect an existing metadata object and any canonical reparse target by
|
||||
/// applying deny-write ACLs before the sandboxed command starts.
|
||||
ExistingDeny,
|
||||
/// Watch for a missing metadata object during the command and remove it if
|
||||
/// a caller intentionally requests reactive cleanup behavior.
|
||||
MissingCreationMonitor,
|
||||
/// Create a temporary deny-listed sentinel before the command starts so the
|
||||
/// sandbox cannot create the metadata object during execution.
|
||||
MissingDenySentinel,
|
||||
}
|
||||
|
||||
pub fn run_setup_refresh(
|
||||
@@ -430,6 +441,8 @@ pub(crate) fn gather_write_roots(
|
||||
out
|
||||
}
|
||||
|
||||
/// Layer: Windows enforcement request boundary. Serialized setup-helper process
|
||||
/// input; this carries prepared enforcement data, not policy decisions.
|
||||
#[derive(Serialize)]
|
||||
struct ElevationPayload {
|
||||
version: u32,
|
||||
|
||||
@@ -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;
|
||||
@@ -35,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,
|
||||
@@ -206,26 +211,58 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn apply_legacy_session_acl_rules(
|
||||
policy: &SandboxPolicy,
|
||||
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)
|
||||
@@ -247,8 +284,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());
|
||||
@@ -258,6 +297,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,
|
||||
@@ -323,6 +477,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 {
|
||||
@@ -335,6 +490,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;
|
||||
@@ -412,4 +569,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")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ 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;
|
||||
@@ -33,6 +34,10 @@ pub(crate) async fn spawn_windows_sandbox_session_elevated(
|
||||
protected_metadata_targets: &[ProtectedMetadataTarget],
|
||||
use_private_desktop: bool,
|
||||
) -> Result<SpawnedProcess> {
|
||||
let mut protected_metadata_guard =
|
||||
prepare_protected_metadata_targets(protected_metadata_targets)?;
|
||||
protected_metadata_guard.arm_sentinel_cleanup()?;
|
||||
|
||||
let elevated = prepare_elevated_spawn_context(
|
||||
policy_json_or_preset,
|
||||
sandbox_policy_cwd,
|
||||
@@ -43,6 +48,7 @@ pub(crate) async fn spawn_windows_sandbox_session_elevated(
|
||||
protected_metadata_targets,
|
||||
)?;
|
||||
|
||||
let protected_metadata_runtime = protected_metadata_guard.into_runtime()?;
|
||||
let spawn_request = SpawnRequest {
|
||||
command: command.clone(),
|
||||
cwd: cwd.to_path_buf(),
|
||||
@@ -102,6 +108,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_runtime),
|
||||
);
|
||||
|
||||
Ok(finish_driver_spawn(
|
||||
|
||||
@@ -10,6 +10,8 @@ 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::ProtectedMetadataRuntime;
|
||||
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;
|
||||
@@ -206,10 +208,11 @@ fn finalize_exit(
|
||||
output_join: std::thread::JoinHandle<()>,
|
||||
guards: Vec<PathBuf>,
|
||||
cap_sid: Option<String>,
|
||||
protected_metadata_runtime: ProtectedMetadataRuntime,
|
||||
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()
|
||||
@@ -222,9 +225,20 @@ fn finalize_exit(
|
||||
raw_exit as i32
|
||||
};
|
||||
|
||||
let _ = output_join.join();
|
||||
let _ = exit_tx.send(exit_code);
|
||||
|
||||
let protected_metadata_failure = match protected_metadata_runtime.finish() {
|
||||
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:#}"))
|
||||
}
|
||||
};
|
||||
unsafe {
|
||||
if thread_handle != 0 && thread_handle != INVALID_HANDLE_VALUE {
|
||||
CloseHandle(thread_handle);
|
||||
@@ -236,7 +250,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);
|
||||
@@ -251,6 +267,12 @@ fn finalize_exit(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Publish the process result after policy cleanup. Output readers may wait
|
||||
// for late pipe EOF from console helpers, but callers already have their
|
||||
// own bounded output drain after observing the exit.
|
||||
let _ = exit_tx.send(exit_code);
|
||||
let _ = output_join.join();
|
||||
}
|
||||
|
||||
fn resize_conpty_handle(hpc: &Arc<StdMutex<Option<HANDLE>>>, size: TerminalSize) -> Result<()> {
|
||||
@@ -290,7 +312,7 @@ pub(crate) async fn spawn_windows_sandbox_session_legacy(
|
||||
timeout_ms: Option<u64>,
|
||||
tty: bool,
|
||||
stdin_open: bool,
|
||||
_protected_metadata_targets: &[ProtectedMetadataTarget],
|
||||
protected_metadata_targets: &[ProtectedMetadataTarget],
|
||||
use_private_desktop: bool,
|
||||
) -> Result<SpawnedProcess> {
|
||||
let common = prepare_legacy_spawn_context(
|
||||
@@ -300,7 +322,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");
|
||||
@@ -309,15 +331,23 @@ 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 mut protected_metadata_guard =
|
||||
prepare_protected_metadata_targets(protected_metadata_targets)?;
|
||||
let additional_deny_write_paths: Vec<PathBuf> =
|
||||
protected_metadata_guard.deny_paths().cloned().collect();
|
||||
protected_metadata_guard.arm_sentinel_cleanup()?;
|
||||
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 protected_metadata_runtime = protected_metadata_guard.into_runtime()?;
|
||||
|
||||
let (writer_tx, writer_rx) = mpsc::channel::<Vec<u8>>(128);
|
||||
let (stdout_tx, stdout_rx) = broadcast::channel::<Vec<u8>>(256);
|
||||
@@ -408,6 +438,7 @@ pub(crate) async fn spawn_windows_sandbox_session_legacy(
|
||||
output_join,
|
||||
guards_for_wait,
|
||||
cap_sid_for_wait,
|
||||
protected_metadata_runtime,
|
||||
common.logs_base_dir.as_deref(),
|
||||
command_for_wait,
|
||||
);
|
||||
|
||||
@@ -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::ProtectedMetadataRuntime;
|
||||
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_runtime: Option<ProtectedMetadataRuntime>,
|
||||
) {
|
||||
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_runtime) = protected_metadata_runtime {
|
||||
match protected_metadata_runtime.finish() {
|
||||
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 } => {
|
||||
|
||||
Reference in New Issue
Block a user