move cap_sid file into ~/.codex so the sandbox cannot overwrite it (#6798)

The `cap_sid` file contains the IDs of the two custom SIDs that the
Windows sandbox creates/manages to implement read-only and
workspace-write sandbox policies.

It previously lived in `<cwd>/.codex` which means that the sandbox could
write to it, which could degrade the efficacy of the sandbox. This
change moves it to `~/.codex/` (or wherever `CODEX_HOME` points to) so
that it is outside the workspace.
This commit is contained in:
iceweasel-oai 2025-11-17 15:49:41 -08:00 committed by GitHub
parent 8bebe86a47
commit e032d338f2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 34 additions and 22 deletions

View file

@ -155,11 +155,11 @@ async fn run_command_under_sandbox(
run_windows_sandbox_capture(
policy_str,
&sandbox_cwd,
base_dir.as_path(),
command_vec,
&cwd_clone,
env_map,
None,
Some(base_dir.as_path()),
)
})
.await;

View file

@ -189,16 +189,20 @@ async fn exec_windows_sandbox(
};
let sandbox_cwd = cwd.clone();
let logs_base_dir = find_codex_home().ok();
let codex_home = find_codex_home().map_err(|err| {
CodexErr::Io(io::Error::other(format!(
"windows sandbox: failed to resolve codex_home: {err}"
)))
})?;
let spawn_res = tokio::task::spawn_blocking(move || {
run_windows_sandbox_capture(
policy_str,
&sandbox_cwd,
codex_home.as_ref(),
command,
&cwd,
env,
timeout_ms,
logs_base_dir.as_deref(),
)
})
.await;

View file

@ -340,7 +340,13 @@ def main() -> int:
add("WS: protected path case-variation denied", rc != 0 and assert_not_exists(git_variation), f"rc={rc}")
# 34. WS: policy tamper (.codex artifacts) denied
rc, out, err = run_sbx("workspace-write", ["cmd", "/c", "echo tamper > .codex\\cap_sid"], WS_ROOT)
codex_home = Path(os.environ["USERPROFILE"]) / ".codex"
cap_sid_target = codex_home / "cap_sid"
rc, out, err = run_sbx(
"workspace-write",
["cmd", "/c", f"echo tamper > \"{cap_sid_target}\""],
WS_ROOT,
)
rc2, out2, err2 = run_sbx("workspace-write", ["cmd", "/c", "echo tamper > .codex\\policy.json"], WS_ROOT)
add("WS: .codex cap_sid tamper denied", rc != 0, f"rc={rc}, err={err}")
add("WS: .codex policy tamper denied", rc2 != 0, f"rc={rc2}, err={err2}")

View file

@ -13,8 +13,8 @@ pub struct CapSids {
pub readonly: String,
}
pub fn cap_sid_file(policy_cwd: &Path) -> PathBuf {
policy_cwd.join(".codex").join("cap_sid")
pub fn cap_sid_file(codex_home: &Path) -> PathBuf {
codex_home.join("cap_sid")
}
fn make_random_cap_sid_string() -> String {
@ -26,8 +26,8 @@ fn make_random_cap_sid_string() -> String {
format!("S-1-5-21-{}-{}-{}-{}", a, b, c, d)
}
pub fn load_or_create_cap_sids(policy_cwd: &Path) -> CapSids {
let path = cap_sid_file(policy_cwd);
pub fn load_or_create_cap_sids(codex_home: &Path) -> CapSids {
let path = cap_sid_file(codex_home);
if path.exists() {
if let Ok(txt) = fs::read_to_string(&path) {
let t = txt.trim();

View file

@ -73,6 +73,11 @@ mod windows_impl {
Ok(())
}
fn ensure_codex_home_exists(p: &Path) -> Result<()> {
std::fs::create_dir_all(p)?;
Ok(())
}
fn make_env_block(env: &HashMap<String, String>) -> Vec<u16> {
let mut items: Vec<(String, String)> =
env.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
@ -179,40 +184,37 @@ mod windows_impl {
pub fn run_windows_sandbox_capture(
policy_json_or_preset: &str,
sandbox_policy_cwd: &Path,
codex_home: &Path,
command: Vec<String>,
cwd: &Path,
mut env_map: HashMap<String, String>,
timeout_ms: Option<u64>,
logs_base_dir: Option<&Path>,
) -> Result<CaptureResult> {
let policy = SandboxPolicy::parse(policy_json_or_preset)?;
normalize_null_device_env(&mut env_map);
ensure_non_interactive_pager(&mut env_map);
apply_no_network_to_env(&mut env_map)?;
ensure_codex_home_exists(codex_home)?;
let current_dir = cwd.to_path_buf();
// for now, don't fail if we detect world-writable directories
// audit::audit_everyone_writable(&current_dir, &env_map)?;
let logs_base_dir = Some(codex_home);
log_start(&command, logs_base_dir);
let cap_sid_path = cap_sid_file(codex_home);
let (h_token, psid_to_use): (HANDLE, *mut c_void) = unsafe {
match &policy.0 {
SandboxMode::ReadOnly => {
let caps = load_or_create_cap_sids(sandbox_policy_cwd);
ensure_dir(&cap_sid_file(sandbox_policy_cwd))?;
fs::write(
cap_sid_file(sandbox_policy_cwd),
serde_json::to_string(&caps)?,
)?;
let caps = load_or_create_cap_sids(codex_home);
ensure_dir(&cap_sid_path)?;
fs::write(&cap_sid_path, serde_json::to_string(&caps)?)?;
let psid = convert_string_sid_to_sid(&caps.readonly).unwrap();
super::token::create_readonly_token_with_cap(psid)?
}
SandboxMode::WorkspaceWrite => {
let caps = load_or_create_cap_sids(sandbox_policy_cwd);
ensure_dir(&cap_sid_file(sandbox_policy_cwd))?;
fs::write(
cap_sid_file(sandbox_policy_cwd),
serde_json::to_string(&caps)?,
)?;
let caps = load_or_create_cap_sids(codex_home);
ensure_dir(&cap_sid_path)?;
fs::write(&cap_sid_path, serde_json::to_string(&caps)?)?;
let psid = convert_string_sid_to_sid(&caps.workspace).unwrap();
super::token::create_workspace_write_token_with_cap(psid)?
}
@ -445,11 +447,11 @@ mod stub {
pub fn run_windows_sandbox_capture(
_policy_json_or_preset: &str,
_sandbox_policy_cwd: &Path,
_codex_home: &Path,
_command: Vec<String>,
_cwd: &Path,
_env_map: HashMap<String, String>,
_timeout_ms: Option<u64>,
_logs_base_dir: Option<&Path>,
) -> Result<CaptureResult> {
bail!("Windows sandbox is only available on Windows")
}