mirror of
https://github.com/openai/codex.git
synced 2026-04-26 15:45:02 +00:00
Protect first-time project .codex creation across Linux and macOS sandboxes (#15067)
## Problem Codex already treated an existing top-level project `./.codex` directory as protected, but there was a gap on first creation. If `./.codex` did not exist yet, a turn could create files under it, such as `./.codex/config.toml`, without going through the same approval path as later modifications. That meant the initial write could bypass the intended protection for project-local Codex state. ## What this changes This PR closes that first-creation gap in the Unix enforcement layers: - `codex-protocol` - treat the top-level project `./.codex` path as a protected carveout even when it does not exist yet - avoid injecting the default carveout when the user already has an explicit rule for that exact path - macOS Seatbelt - deny writes to both the exact protected path and anything beneath it, so creating `./.codex` itself is blocked in addition to writes inside it - Linux bubblewrap - preserve the same protected-path behavior for first-time creation under `./.codex` - tests - add protocol regressions for missing `./.codex` and explicit-rule collisions - add Unix sandbox coverage for blocking first-time `./.codex` creation - tighten Seatbelt policy assertions around excluded subpaths ## Scope This change is intentionally scoped to protecting the top-level project `.codex` subtree from agent writes. It does not make `.codex` unreadable, and it does not change the product behavior around loading project skills from `.codex` when project config is untrusted. ## Why this shape The fix is pointed rather than broad: - it preserves the current model of “project `.codex` is protected from writes” - it closes the security-relevant first-write hole - it avoids folding a larger permissions-model redesign into this PR ## Validation - `cargo test -p codex-protocol` - `cargo test -p codex-sandboxing seatbelt` - `cargo test -p codex-exec --test all sandbox_blocks_first_time_dot_codex_creation -- --nocapture` --------- Co-authored-by: Michael Bolin <mbolin@openai.com>
This commit is contained in:
@@ -118,6 +118,7 @@ async fn python_multiprocessing_lock_works_under_sandbox() {
|
||||
#[cfg(target_os = "linux")]
|
||||
let sandbox_env = match linux_sandbox_test_env().await {
|
||||
Some(env) => env,
|
||||
// Skip on Linux hosts where Landlock cannot actually be enforced.
|
||||
None => return,
|
||||
};
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
@@ -312,6 +313,78 @@ async fn sandbox_distinguishes_command_and_policy_cwds() {
|
||||
assert!(allowed_exists, "allowed path should exist");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sandbox_blocks_first_time_dot_codex_creation() {
|
||||
core_test_support::skip_if_sandbox!();
|
||||
#[cfg(target_os = "linux")]
|
||||
let sandbox_env = match linux_sandbox_test_env().await {
|
||||
Some(env) => env,
|
||||
None => return,
|
||||
};
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let sandbox_env = HashMap::new();
|
||||
|
||||
let temp = tempfile::tempdir().expect("should be able to create temp dir");
|
||||
let repo_root = temp.path().join("repo");
|
||||
create_dir_all(&repo_root).await.expect("mkdir repo");
|
||||
let dot_codex = repo_root.join(".codex");
|
||||
let config_toml = dot_codex.join("config.toml");
|
||||
let policy = SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![],
|
||||
read_only_access: Default::default(),
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
};
|
||||
|
||||
let mut child = spawn_command_under_sandbox(
|
||||
vec![
|
||||
"bash".to_string(),
|
||||
"-lc".to_string(),
|
||||
"mkdir -p .codex && echo 'sandbox_mode = \"danger-full-access\"' > .codex/config.toml"
|
||||
.to_string(),
|
||||
],
|
||||
repo_root.clone(),
|
||||
&policy,
|
||||
repo_root.as_path(),
|
||||
StdioPolicy::RedirectForShellTool,
|
||||
sandbox_env,
|
||||
)
|
||||
.await
|
||||
.expect("should spawn command creating .codex");
|
||||
|
||||
let status = child.wait().await.expect("should wait for .codex command");
|
||||
assert!(
|
||||
!status.success(),
|
||||
"sandbox unexpectedly allowed first-time .codex creation: {status:?}"
|
||||
);
|
||||
let dot_codex_metadata = tokio::fs::symlink_metadata(&dot_codex).await;
|
||||
if let Ok(metadata) = dot_codex_metadata {
|
||||
assert!(
|
||||
!metadata.is_dir(),
|
||||
"{} should not be creatable as a directory",
|
||||
dot_codex.display()
|
||||
);
|
||||
} else if let Err(err) = &dot_codex_metadata {
|
||||
assert_eq!(
|
||||
err.kind(),
|
||||
io::ErrorKind::NotFound,
|
||||
"unexpected metadata error for {}: {err}",
|
||||
dot_codex.display()
|
||||
);
|
||||
}
|
||||
let config_toml_exists = match tokio::fs::try_exists(&config_toml).await {
|
||||
Ok(exists) => exists,
|
||||
Err(err) if err.kind() == io::ErrorKind::NotADirectory => false,
|
||||
Err(err) => panic!("try_exists {} failed: {err}", config_toml.display()),
|
||||
};
|
||||
assert!(
|
||||
!config_toml_exists,
|
||||
"{} should not have been created",
|
||||
config_toml.display()
|
||||
);
|
||||
}
|
||||
|
||||
fn unix_sock_body() {
|
||||
unsafe {
|
||||
let mut fds = [0i32; 2];
|
||||
|
||||
Reference in New Issue
Block a user