mirror of
https://github.com/openai/codex.git
synced 2026-05-06 12:26:38 +00:00
Compare commits
6 Commits
xli-codex/
...
codex/wind
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16cc0e9c05 | ||
|
|
f29e31e27f | ||
|
|
df461fd9eb | ||
|
|
a6dfb7d038 | ||
|
|
801c0f5221 | ||
|
|
b508d9ad80 |
@@ -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(
|
||||
@@ -628,6 +647,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 +687,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 +700,7 @@ async fn exec_windows_sandbox(
|
||||
env,
|
||||
timeout_ms,
|
||||
&additional_deny_write_paths,
|
||||
&protected_metadata_targets,
|
||||
windows_sandbox_private_desktop,
|
||||
)
|
||||
}
|
||||
@@ -1131,7 +1174,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 +1188,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 +1309,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 +1325,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 {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,6 +18,7 @@ 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 {
|
||||
@@ -125,6 +128,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);
|
||||
@@ -147,6 +151,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.
|
||||
|
||||
@@ -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(|| {
|
||||
|
||||
@@ -26,6 +26,7 @@ windows_modules!(
|
||||
path_normalization,
|
||||
policy,
|
||||
process,
|
||||
protected_metadata,
|
||||
token,
|
||||
wfp,
|
||||
wfp_setup,
|
||||
@@ -173,6 +174,10 @@ 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;
|
||||
@@ -253,6 +258,7 @@ 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_null_device;
|
||||
@@ -345,6 +351,7 @@ mod windows_impl {
|
||||
env_map,
|
||||
timeout_ms,
|
||||
&[],
|
||||
&[],
|
||||
use_private_desktop,
|
||||
)
|
||||
}
|
||||
@@ -359,6 +366,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(
|
||||
|
||||
174
codex-rs/windows-sandbox-rs/src/protected_metadata.rs
Normal file
174
codex-rs/windows-sandbox-rs/src/protected_metadata.rs
Normal file
@@ -0,0 +1,174 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use crate::setup::ProtectedMetadataMode;
|
||||
use crate::setup::ProtectedMetadataTarget;
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
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> {
|
||||
if std::fs::symlink_metadata(path).is_ok() {
|
||||
vec![path.to_path_buf()]
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -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::ProtectedMetadataTarget;
|
||||
use codex_windows_sandbox::SETUP_VERSION;
|
||||
use codex_windows_sandbox::SetupErrorCode;
|
||||
use codex_windows_sandbox::SetupErrorReport;
|
||||
@@ -87,6 +88,9 @@ 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>,
|
||||
#[serde(default)]
|
||||
allow_local_binding: bool,
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -17,6 +17,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;
|
||||
@@ -264,6 +265,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 +300,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)?;
|
||||
|
||||
@@ -38,6 +38,7 @@ pub(crate) async fn spawn_windows_sandbox_session_elevated(
|
||||
cwd,
|
||||
&mut env_map,
|
||||
&command,
|
||||
&[],
|
||||
)?;
|
||||
|
||||
let spawn_request = SpawnRequest {
|
||||
|
||||
Reference in New Issue
Block a user