Compare commits

...

2 Commits

Author SHA1 Message Date
David Wiesen
f49d5924a9 windows-sandbox: isolate temp ACL grants 2026-03-25 11:09:21 -07:00
David Wiesen
7c3209fe5f Avoid sandbox ACL grants under AppData 2026-03-25 11:03:32 -07:00
4 changed files with 74 additions and 2 deletions

View File

@@ -3,6 +3,7 @@ mod windows_impl {
use crate::allow::compute_allow_paths;
use crate::allow::AllowDenyPaths;
use crate::cap::load_or_create_cap_sids;
use crate::env::configure_private_temp_env;
use crate::env::ensure_non_interactive_pager;
use crate::env::inherit_path_env;
use crate::env::normalize_null_device_env;
@@ -215,6 +216,7 @@ mod windows_impl {
let policy = parse_policy(policy_json_or_preset)?;
normalize_null_device_env(&mut env_map);
ensure_non_interactive_pager(&mut env_map);
configure_private_temp_env(&mut env_map, codex_home, cwd)?;
inherit_path_env(&mut env_map);
inject_git_safe_directory(&mut env_map, cwd, None);
let current_dir = cwd.to_path_buf();

View File

@@ -1,8 +1,11 @@
use anyhow::{anyhow, Result};
use dirs_next::home_dir;
use std::collections::hash_map::DefaultHasher;
use std::collections::HashMap;
use std::env;
use std::fs::{self, File};
use std::hash::Hash;
use std::hash::Hasher;
use std::io::Write;
use std::path::{Path, PathBuf};
@@ -28,6 +31,29 @@ pub fn ensure_non_interactive_pager(env_map: &mut HashMap<String, String>) {
env_map.entry("LESS".into()).or_insert_with(|| "".into());
}
fn workspace_temp_dir(codex_home: &Path, cwd: &Path) -> PathBuf {
let canonical_cwd = dunce::canonicalize(cwd).unwrap_or_else(|_| cwd.to_path_buf());
let mut hasher = DefaultHasher::new();
canonical_cwd.hash(&mut hasher);
codex_home
.join(".sandbox-temp")
.join(format!("ws-{:016x}", hasher.finish()))
}
pub fn configure_private_temp_env(
env_map: &mut HashMap<String, String>,
codex_home: &Path,
cwd: &Path,
) -> Result<PathBuf> {
let sandbox_temp = workspace_temp_dir(codex_home, cwd);
fs::create_dir_all(&sandbox_temp)?;
let sandbox_temp = sandbox_temp.to_string_lossy().to_string();
env_map.insert("TEMP".into(), sandbox_temp.clone());
env_map.insert("TMP".into(), sandbox_temp.clone());
env_map.insert("TMPDIR".into(), sandbox_temp.clone());
Ok(PathBuf::from(sandbox_temp))
}
// Keep PATH and PATHEXT stable for callers that rely on inheriting the parent process env.
pub fn inherit_path_env(env_map: &mut HashMap<String, String>) {
if !env_map.contains_key("PATH") {
@@ -172,3 +198,36 @@ pub fn apply_no_network_to_env(env_map: &mut HashMap<String, String>) -> Result<
reorder_pathext_for_stubs(env_map);
Ok(())
}
#[cfg(test)]
mod tests {
use super::configure_private_temp_env;
use std::collections::HashMap;
#[test]
fn configure_private_temp_env_redirects_all_temp_vars_to_private_workspace_dir() {
let tmp = tempfile::tempdir().expect("temp dir");
let codex_home = tmp.path().join("codex-home");
let workspace = tmp.path().join("workspace");
std::fs::create_dir_all(&workspace).expect("workspace dir");
let mut env_map = HashMap::new();
let sandbox_temp =
configure_private_temp_env(&mut env_map, &codex_home, &workspace).expect("temp env");
assert!(sandbox_temp.starts_with(codex_home.join(".sandbox-temp")));
assert!(sandbox_temp.is_dir());
assert_eq!(
env_map.get("TEMP"),
Some(&sandbox_temp.to_string_lossy().to_string())
);
assert_eq!(
env_map.get("TMP"),
Some(&sandbox_temp.to_string_lossy().to_string())
);
assert_eq!(
env_map.get("TMPDIR"),
Some(&sandbox_temp.to_string_lossy().to_string())
);
}
}

View File

@@ -182,6 +182,7 @@ mod windows_impl {
use super::cap::load_or_create_cap_sids;
use super::cap::workspace_cap_sid_for_cwd;
use super::env::apply_no_network_to_env;
use super::env::configure_private_temp_env;
use super::env::ensure_non_interactive_pager;
use super::env::normalize_null_device_env;
use super::logging::log_failure;
@@ -299,6 +300,7 @@ mod windows_impl {
let apply_network_block = should_apply_network_block(&policy);
normalize_null_device_env(&mut env_map);
ensure_non_interactive_pager(&mut env_map);
configure_private_temp_env(&mut env_map, codex_home, cwd)?;
if apply_network_block {
apply_no_network_to_env(&mut env_map)?;
}

View File

@@ -44,6 +44,7 @@ const USERPROFILE_READ_ROOT_EXCLUSIONS: &[&str] = &[
".gnupg",
".aws",
".azure",
"AppData",
".kube",
".docker",
".config",
@@ -133,11 +134,14 @@ fn run_setup_refresh_inner(
) {
return Ok(());
}
let mut env_map = env_map.clone();
crate::env::configure_private_temp_env(&mut env_map, codex_home, command_cwd)
.context("configure private sandbox temp env")?;
let (read_roots, write_roots) = build_payload_roots(
policy,
policy_cwd,
command_cwd,
env_map,
&env_map,
codex_home,
read_roots_override,
write_roots_override,
@@ -590,11 +594,14 @@ pub fn run_elevated_setup(
format!("failed to create sandbox dir {}: {err}", sbx_dir.display()),
)
})?;
let mut env_map = env_map.clone();
crate::env::configure_private_temp_env(&mut env_map, codex_home, command_cwd)
.context("configure private sandbox temp env")?;
let (read_roots, write_roots) = build_payload_roots(
policy,
policy_cwd,
command_cwd,
env_map,
&env_map,
codex_home,
read_roots_override,
write_roots_override,
@@ -698,11 +705,13 @@ mod tests {
let user_profile = tmp.path();
let allowed_dir = user_profile.join("Documents");
let allowed_file = user_profile.join(".gitconfig");
let excluded_appdata = user_profile.join("AppData");
let excluded_dir = user_profile.join(".ssh");
let excluded_case_variant = user_profile.join(".AWS");
fs::create_dir_all(&allowed_dir).expect("create allowed dir");
fs::write(&allowed_file, "safe").expect("create allowed file");
fs::create_dir_all(excluded_appdata.join("Local")).expect("create excluded appdata");
fs::create_dir_all(&excluded_dir).expect("create excluded dir");
fs::create_dir_all(&excluded_case_variant).expect("create excluded case variant");