Compare commits

...

2 Commits

Author SHA1 Message Date
David Wiesen
285cc3049c Fix Windows sandbox USERPROFILE realpath access 2026-05-19 09:45:46 -07:00
David Wiesen
d11e344df5 Fix Windows sandbox USERPROFILE read root 2026-05-19 09:44:50 -07:00
2 changed files with 110 additions and 2 deletions

View File

@@ -214,6 +214,18 @@ fn apply_read_acls(
Ok(())
}
fn user_profile_metadata_read_root(read_roots: &[PathBuf]) -> Option<PathBuf> {
let user_profile = PathBuf::from(std::env::var("USERPROFILE").ok()?);
let canonical_profile = canonicalize_path(&user_profile);
let needs_profile_root = read_roots.iter().any(|root| {
let canonical_root = canonicalize_path(root);
canonical_root != canonical_profile && canonical_root.starts_with(&canonical_profile)
});
needs_profile_root.then_some(user_profile)
}
fn read_mask_allows_or_log(
root: &Path,
psids: &[*mut c_void],
@@ -472,6 +484,20 @@ fn run_read_acl_only(payload: &Payload, log: &mut File) -> Result<()> {
sandbox_group_psid,
rx_psids: &rx_psids,
};
if let Some(user_profile_root) = user_profile_metadata_read_root(&payload.read_roots) {
// Some Windows tools call `realpath` on USERPROFILE itself before descending into a
// readable child. Grant a non-inheriting root ACE so those metadata probes succeed
// without re-exposing excluded profile children like `.ssh`.
apply_read_acls(
&[user_profile_root],
&subjects,
log,
&mut refresh_errors,
FILE_GENERIC_READ | FILE_GENERIC_EXECUTE,
"read",
0,
)?;
}
apply_read_acls(
&payload.read_roots,
&subjects,
@@ -914,9 +940,11 @@ fn run_setup_full(payload: &Payload, log: &mut File, sbx_dir: &Path) -> Result<(
mod tests {
use super::Payload;
use super::SETUP_VERSION;
use super::user_profile_metadata_read_root;
use codex_otel::StatsigMetricsSettings;
use pretty_assertions::assert_eq;
use serde_json::json;
use tempfile::TempDir;
fn payload_json() -> serde_json::Value {
json!({
@@ -954,4 +982,41 @@ mod tests {
})
);
}
#[test]
fn user_profile_metadata_root_is_added_for_profile_descendants() {
let tmp = TempDir::new().expect("tempdir");
let user_profile = tmp.path().join("user");
let docs = user_profile.join("Documents");
std::fs::create_dir_all(&docs).expect("create docs");
let previous = std::env::var_os("USERPROFILE");
std::env::set_var("USERPROFILE", &user_profile);
let root = user_profile_metadata_read_root(&[docs]);
match previous {
Some(value) => std::env::set_var("USERPROFILE", value),
None => std::env::remove_var("USERPROFILE"),
}
assert_eq!(root, Some(user_profile));
}
#[test]
fn user_profile_metadata_root_is_not_added_for_non_descendants() {
let tmp = TempDir::new().expect("tempdir");
let user_profile = tmp.path().join("user");
let outside = tmp.path().join("workspace");
std::fs::create_dir_all(&user_profile).expect("create profile");
std::fs::create_dir_all(&outside).expect("create workspace");
let previous = std::env::var_os("USERPROFILE");
std::env::set_var("USERPROFILE", &user_profile);
let root = user_profile_metadata_read_root(&[outside]);
match previous {
Some(value) => std::env::set_var("USERPROFILE", value),
None => std::env::remove_var("USERPROFILE"),
}
assert_eq!(root, None);
}
}

View File

@@ -783,8 +783,7 @@ fn build_payload_roots(
} else {
gather_read_roots(request.command_cwd, request.policy, request.codex_home)
};
read_roots = expand_user_profile_root(read_roots);
read_roots = filter_user_profile_root(read_roots);
read_roots = expand_user_profile_read_roots(read_roots);
read_roots = filter_user_profile_root_exclusions(read_roots);
read_roots = filter_ssh_config_dependency_roots(read_roots);
let write_root_set: HashSet<PathBuf> = write_roots.iter().cloned().collect();
@@ -824,6 +823,13 @@ fn expand_user_profile_root(roots: Vec<PathBuf>) -> Vec<PathBuf> {
expand_user_profile_root_for(roots, Path::new(&user_profile))
}
fn expand_user_profile_read_roots(roots: Vec<PathBuf>) -> Vec<PathBuf> {
let Ok(user_profile) = std::env::var("USERPROFILE") else {
return roots;
};
expand_user_profile_read_roots_for(roots, Path::new(&user_profile))
}
fn expand_user_profile_root_for(roots: Vec<PathBuf>, user_profile: &Path) -> Vec<PathBuf> {
let user_profile_key = canonical_path_key(user_profile);
let mut expanded = Vec::new();
@@ -840,6 +846,23 @@ fn expand_user_profile_root_for(roots: Vec<PathBuf>, user_profile: &Path) -> Vec
expanded
}
fn expand_user_profile_read_roots_for(roots: Vec<PathBuf>, user_profile: &Path) -> Vec<PathBuf> {
let user_profile_key = canonical_path_key(user_profile);
let mut expanded = Vec::new();
for root in roots {
if canonical_path_key(&root) == user_profile_key {
expanded.push(user_profile.to_path_buf());
expanded.extend(profile_read_roots(user_profile));
} else {
expanded.push(root);
}
}
expanded.sort_by_key(|root| canonical_path_key(root));
expanded.dedup_by(|a, b| canonical_path_key(a.as_path()) == canonical_path_key(b.as_path()));
expanded
}
fn filter_user_profile_root(mut roots: Vec<PathBuf>) -> Vec<PathBuf> {
let Ok(user_profile) = std::env::var("USERPROFILE") else {
return roots;
@@ -1241,6 +1264,26 @@ mod tests {
assert_eq!(expected, actual);
}
#[test]
fn expand_user_profile_read_roots_for_keeps_profile_root_for_metadata_access() {
let tmp = TempDir::new().expect("tempdir");
let user_profile = tmp.path().join("user-profile");
let documents = user_profile.join("Documents");
let other_root = tmp.path().join("other-root");
fs::create_dir_all(&documents).expect("create documents");
fs::create_dir_all(&other_root).expect("create other root");
let roots = super::expand_user_profile_read_roots_for(
vec![user_profile.clone(), other_root.clone()],
&user_profile,
);
let actual: HashSet<PathBuf> = roots.into_iter().collect();
let expected: HashSet<PathBuf> =
[user_profile, documents, other_root].into_iter().collect();
assert_eq!(expected, actual);
}
#[test]
fn expanded_write_roots_still_drop_protected_codex_home() {
let tmp = TempDir::new().expect("tempdir");