Files
codex/codex-rs/linux-sandbox/src/linux_run_main_tests.rs
viyatb-oai 0d0abe839a feat(sandbox): add glob deny-read platform enforcement (#18096)
## Summary
- adds macOS Seatbelt deny rules for unreadable glob patterns
- expands unreadable glob matches on Linux and masks them in bwrap,
including canonical symlink targets
- keeps Linux glob expansion robust when `rg` is unavailable in minimal
or Bazel test environments
- adds sandbox integration coverage that runs `shell` and `exec_command`
with a `**/*.env = none` policy and verifies the secret contents do not
reach the model

## Linux glob expansion

```text
Prefer:   rg --files --hidden --no-ignore --glob <pattern> -- <search-root>
Fallback: internal globset walker when rg is not installed
Failure:  any other rg failure aborts sandbox construction
```

```
[permissions.workspace.filesystem]
glob_scan_max_depth = 2

[permissions.workspace.filesystem.":project_roots"]
"**/*.env" = "none"
```


This keeps the common path fast without making sandbox construction
depend on an ambient `rg` binary. If `rg` is present but fails for
another reason, the sandbox setup fails closed instead of silently
omitting deny-read masks.

## Platform support
- macOS: subprocess sandbox enforcement is handled by Seatbelt regex
deny rules
- Linux: subprocess sandbox enforcement is handled by expanding existing
glob matches and masking them in bwrap
- Windows: policy/config/direct-tool glob support is already on `main`
from #15979; Windows subprocess sandbox paths continue to fail closed
when unreadable split filesystem carveouts require runtime enforcement,
rather than silently running unsandboxed

## Stack
1. #15979 - merged: cross-platform glob deny-read
policy/config/direct-tool support for macOS, Linux, and Windows
2. This PR - macOS/Linux subprocess sandbox enforcement plus Windows
fail-closed clarification
3. #17740 - managed deny-read requirements

## Verification
- Added integration coverage for `shell` and `exec_command` glob
deny-read enforcement
- `cargo check -p codex-sandboxing -p codex-linux-sandbox --tests`
- `cargo check -p codex-core --test all`
- `cargo clippy -p codex-linux-sandbox -p codex-sandboxing --tests`
- `just bazel-lock-check`

---------

Co-authored-by: Codex <noreply@openai.com>
2026-04-16 17:35:16 -07:00

539 lines
18 KiB
Rust

#[cfg(test)]
use super::*;
#[cfg(test)]
use codex_protocol::protocol::FileSystemSandboxPolicy;
#[cfg(test)]
use codex_protocol::protocol::NetworkSandboxPolicy;
#[cfg(test)]
use codex_protocol::protocol::ReadOnlyAccess;
#[cfg(test)]
use codex_protocol::protocol::SandboxPolicy;
#[cfg(test)]
use codex_utils_absolute_path::AbsolutePathBuf;
#[cfg(test)]
use pretty_assertions::assert_eq;
#[test]
fn detects_proc_mount_invalid_argument_failure() {
let stderr = "bwrap: Can't mount proc on /newroot/proc: Invalid argument";
assert!(is_proc_mount_failure(stderr));
}
#[test]
fn detects_proc_mount_operation_not_permitted_failure() {
let stderr = "bwrap: Can't mount proc on /newroot/proc: Operation not permitted";
assert!(is_proc_mount_failure(stderr));
}
#[test]
fn detects_proc_mount_permission_denied_failure() {
let stderr = "bwrap: Can't mount proc on /newroot/proc: Permission denied";
assert!(is_proc_mount_failure(stderr));
}
#[test]
fn ignores_non_proc_mount_errors() {
let stderr = "bwrap: Can't bind mount /dev/null: Operation not permitted";
assert!(!is_proc_mount_failure(stderr));
}
#[test]
fn inserts_bwrap_argv0_before_command_separator() {
let sandbox_policy = SandboxPolicy::new_read_only_policy();
let mut argv = build_bwrap_argv(
vec!["/bin/true".to_string()],
&FileSystemSandboxPolicy::from(&sandbox_policy),
Path::new("/"),
Path::new("/"),
BwrapOptions {
mount_proc: true,
network_mode: BwrapNetworkMode::FullAccess,
..Default::default()
},
)
.args;
apply_inner_command_argv0_for_launcher(
&mut argv,
/*supports_argv0*/ true,
"/tmp/codex-arg0-session/codex-linux-sandbox".to_string(),
);
assert_eq!(
argv,
vec![
"bwrap".to_string(),
"--new-session".to_string(),
"--die-with-parent".to_string(),
"--ro-bind".to_string(),
"/".to_string(),
"/".to_string(),
"--dev".to_string(),
"/dev".to_string(),
"--unshare-user".to_string(),
"--unshare-pid".to_string(),
"--proc".to_string(),
"/proc".to_string(),
"--argv0".to_string(),
"codex-linux-sandbox".to_string(),
"--".to_string(),
"/bin/true".to_string(),
]
);
}
#[test]
fn rewrites_inner_command_path_when_bwrap_lacks_argv0() {
let sandbox_policy = SandboxPolicy::new_read_only_policy();
let mut argv = build_bwrap_argv(
vec!["/bin/true".to_string()],
&FileSystemSandboxPolicy::from(&sandbox_policy),
Path::new("/"),
Path::new("/"),
BwrapOptions {
mount_proc: true,
network_mode: BwrapNetworkMode::FullAccess,
..Default::default()
},
)
.args;
apply_inner_command_argv0_for_launcher(
&mut argv,
/*supports_argv0*/ false,
"/tmp/codex-arg0-session/codex-linux-sandbox".to_string(),
);
assert!(!argv.iter().any(|arg| arg == "--argv0"));
assert!(
argv.windows(2)
.any(|window| { window == ["--", "/tmp/codex-arg0-session/codex-linux-sandbox"] })
);
}
#[test]
fn rewrites_bwrap_helper_command_not_nested_user_command_when_current_exe_appears_later() {
let nested_current_exe = std::env::current_exe()
.expect("current exe")
.to_string_lossy()
.into_owned();
let mut argv = vec![
"bwrap".to_string(),
"--".to_string(),
"/tmp/helper-symlink".to_string(),
"--sandbox-policy-cwd".to_string(),
"/tmp/cwd".to_string(),
"--".to_string(),
nested_current_exe.clone(),
"--codex-run-as-apply-patch".to_string(),
"patch".to_string(),
];
apply_inner_command_argv0_for_launcher(
&mut argv,
/*supports_argv0*/ false,
"/tmp/argv0-fallback-helper".to_string(),
);
assert_eq!(
argv,
vec![
"bwrap".to_string(),
"--".to_string(),
"/tmp/argv0-fallback-helper".to_string(),
"--sandbox-policy-cwd".to_string(),
"/tmp/cwd".to_string(),
"--".to_string(),
nested_current_exe,
"--codex-run-as-apply-patch".to_string(),
"patch".to_string(),
]
);
}
#[test]
fn inserts_unshare_net_when_network_isolation_requested() {
let sandbox_policy = SandboxPolicy::new_read_only_policy();
let argv = build_bwrap_argv(
vec!["/bin/true".to_string()],
&FileSystemSandboxPolicy::from(&sandbox_policy),
Path::new("/"),
Path::new("/"),
BwrapOptions {
mount_proc: true,
network_mode: BwrapNetworkMode::Isolated,
..Default::default()
},
)
.args;
assert!(argv.contains(&"--unshare-net".to_string()));
}
#[test]
fn inserts_unshare_net_when_proxy_only_network_mode_requested() {
let sandbox_policy = SandboxPolicy::new_read_only_policy();
let argv = build_bwrap_argv(
vec!["/bin/true".to_string()],
&FileSystemSandboxPolicy::from(&sandbox_policy),
Path::new("/"),
Path::new("/"),
BwrapOptions {
mount_proc: true,
network_mode: BwrapNetworkMode::ProxyOnly,
..Default::default()
},
)
.args;
assert!(argv.contains(&"--unshare-net".to_string()));
}
#[test]
fn proxy_only_mode_takes_precedence_over_full_network_policy() {
let mode = bwrap_network_mode(
NetworkSandboxPolicy::Enabled,
/*allow_network_for_proxy*/ true,
);
assert_eq!(mode, BwrapNetworkMode::ProxyOnly);
}
#[test]
fn split_only_filesystem_policy_requires_direct_runtime_enforcement() {
let temp_dir = tempfile::TempDir::new().expect("tempdir");
let docs = temp_dir.path().join("docs");
std::fs::create_dir_all(&docs).expect("create docs");
let docs = AbsolutePathBuf::from_absolute_path(&docs).expect("absolute docs");
let policy = FileSystemSandboxPolicy::restricted(vec![
codex_protocol::permissions::FileSystemSandboxEntry {
path: codex_protocol::permissions::FileSystemPath::Special {
value: codex_protocol::permissions::FileSystemSpecialPath::CurrentWorkingDirectory,
},
access: codex_protocol::permissions::FileSystemAccessMode::Write,
},
codex_protocol::permissions::FileSystemSandboxEntry {
path: codex_protocol::permissions::FileSystemPath::Path { path: docs },
access: codex_protocol::permissions::FileSystemAccessMode::Read,
},
]);
assert!(
policy.needs_direct_runtime_enforcement(NetworkSandboxPolicy::Restricted, temp_dir.path(),)
);
}
#[test]
fn root_write_read_only_carveout_requires_direct_runtime_enforcement() {
let temp_dir = tempfile::TempDir::new().expect("tempdir");
let docs = temp_dir.path().join("docs");
std::fs::create_dir_all(&docs).expect("create docs");
let docs = AbsolutePathBuf::from_absolute_path(&docs).expect("absolute docs");
let 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::Write,
},
codex_protocol::permissions::FileSystemSandboxEntry {
path: codex_protocol::permissions::FileSystemPath::Path { path: docs },
access: codex_protocol::permissions::FileSystemAccessMode::Read,
},
]);
assert!(
policy.needs_direct_runtime_enforcement(NetworkSandboxPolicy::Restricted, temp_dir.path(),)
);
}
#[test]
fn managed_proxy_preflight_argv_is_wrapped_for_full_access_policy() {
let mode = bwrap_network_mode(
NetworkSandboxPolicy::Enabled,
/*allow_network_for_proxy*/ true,
);
let argv = build_preflight_bwrap_argv(
Path::new("/"),
Path::new("/"),
&FileSystemSandboxPolicy::from(&SandboxPolicy::DangerFullAccess),
mode,
)
.args;
assert!(argv.iter().any(|arg| arg == "--"));
}
#[test]
fn managed_proxy_inner_command_includes_route_spec() {
let sandbox_policy = SandboxPolicy::new_read_only_policy();
let args = build_inner_seccomp_command(InnerSeccompCommandArgs {
sandbox_policy_cwd: Path::new("/tmp"),
command_cwd: Some(Path::new("/tmp/link")),
sandbox_policy: &sandbox_policy,
file_system_sandbox_policy: &FileSystemSandboxPolicy::from(&sandbox_policy),
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
allow_network_for_proxy: true,
proxy_route_spec: Some("{\"routes\":[]}".to_string()),
command: vec!["/bin/true".to_string()],
});
assert!(args.iter().any(|arg| arg == "--proxy-route-spec"));
assert!(args.iter().any(|arg| arg == "{\"routes\":[]}"));
}
#[test]
fn inner_command_includes_split_policy_flags() {
let sandbox_policy = SandboxPolicy::new_read_only_policy();
let args = build_inner_seccomp_command(InnerSeccompCommandArgs {
sandbox_policy_cwd: Path::new("/tmp"),
command_cwd: Some(Path::new("/tmp/link")),
sandbox_policy: &sandbox_policy,
file_system_sandbox_policy: &FileSystemSandboxPolicy::from(&sandbox_policy),
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
allow_network_for_proxy: false,
proxy_route_spec: None,
command: vec!["/bin/true".to_string()],
});
assert!(args.iter().any(|arg| arg == "--file-system-sandbox-policy"));
assert!(args.iter().any(|arg| arg == "--network-sandbox-policy"));
assert!(
args.windows(2)
.any(|window| { window == ["--command-cwd", "/tmp/link"] })
);
}
#[test]
fn non_managed_inner_command_omits_route_spec() {
let sandbox_policy = SandboxPolicy::new_read_only_policy();
let args = build_inner_seccomp_command(InnerSeccompCommandArgs {
sandbox_policy_cwd: Path::new("/tmp"),
command_cwd: Some(Path::new("/tmp/link")),
sandbox_policy: &sandbox_policy,
file_system_sandbox_policy: &FileSystemSandboxPolicy::from(&sandbox_policy),
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
allow_network_for_proxy: false,
proxy_route_spec: None,
command: vec!["/bin/true".to_string()],
});
assert!(!args.iter().any(|arg| arg == "--proxy-route-spec"));
}
#[test]
fn managed_proxy_inner_command_requires_route_spec() {
let result = std::panic::catch_unwind(|| {
let sandbox_policy = SandboxPolicy::new_read_only_policy();
build_inner_seccomp_command(InnerSeccompCommandArgs {
sandbox_policy_cwd: Path::new("/tmp"),
command_cwd: Some(Path::new("/tmp/link")),
sandbox_policy: &sandbox_policy,
file_system_sandbox_policy: &FileSystemSandboxPolicy::from(&sandbox_policy),
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
allow_network_for_proxy: true,
proxy_route_spec: None,
command: vec!["/bin/true".to_string()],
})
});
assert!(result.is_err());
}
#[test]
fn resolve_sandbox_policies_derives_split_policies_from_legacy_policy() {
let sandbox_policy = SandboxPolicy::new_read_only_policy();
let resolved = resolve_sandbox_policies(
Path::new("/tmp"),
Some(sandbox_policy.clone()),
/*file_system_sandbox_policy*/ None,
/*network_sandbox_policy*/ None,
)
.expect("legacy policy should resolve");
assert_eq!(resolved.sandbox_policy, sandbox_policy);
assert_eq!(
resolved.file_system_sandbox_policy,
FileSystemSandboxPolicy::from(&sandbox_policy)
);
assert_eq!(
resolved.network_sandbox_policy,
NetworkSandboxPolicy::from(&sandbox_policy)
);
}
#[test]
fn resolve_sandbox_policies_derives_legacy_policy_from_split_policies() {
let sandbox_policy = SandboxPolicy::new_read_only_policy();
let file_system_sandbox_policy = FileSystemSandboxPolicy::from(&sandbox_policy);
let network_sandbox_policy = NetworkSandboxPolicy::from(&sandbox_policy);
let resolved = resolve_sandbox_policies(
Path::new("/tmp"),
/*sandbox_policy*/ None,
Some(file_system_sandbox_policy.clone()),
Some(network_sandbox_policy),
)
.expect("split policies should resolve");
assert_eq!(resolved.sandbox_policy, sandbox_policy);
assert_eq!(
resolved.file_system_sandbox_policy,
file_system_sandbox_policy
);
assert_eq!(resolved.network_sandbox_policy, network_sandbox_policy);
}
#[test]
fn resolve_sandbox_policies_rejects_partial_split_policies() {
let err = resolve_sandbox_policies(
Path::new("/tmp"),
Some(SandboxPolicy::new_read_only_policy()),
Some(FileSystemSandboxPolicy::default()),
/*network_sandbox_policy*/ None,
)
.expect_err("partial split policies should fail");
assert_eq!(err, ResolveSandboxPoliciesError::PartialSplitPolicies);
}
#[test]
fn resolve_sandbox_policies_rejects_mismatched_legacy_and_split_inputs() {
let err = resolve_sandbox_policies(
Path::new("/tmp"),
Some(SandboxPolicy::new_read_only_policy()),
Some(FileSystemSandboxPolicy::unrestricted()),
Some(NetworkSandboxPolicy::Enabled),
)
.expect_err("mismatched legacy and split policies should fail");
assert!(
matches!(
err,
ResolveSandboxPoliciesError::MismatchedLegacyPolicy { .. }
),
"{err}"
);
}
#[test]
fn resolve_sandbox_policies_accepts_split_policies_requiring_direct_runtime_enforcement() {
let temp_dir = tempfile::TempDir::new().expect("tempdir");
let docs = temp_dir.path().join("docs");
std::fs::create_dir_all(&docs).expect("create docs");
let docs = AbsolutePathBuf::from_absolute_path(&docs).expect("absolute docs");
let sandbox_policy = SandboxPolicy::new_read_only_policy();
let file_system_sandbox_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::Path { path: docs },
access: codex_protocol::permissions::FileSystemAccessMode::Write,
},
]);
let resolved = resolve_sandbox_policies(
temp_dir.path(),
Some(sandbox_policy.clone()),
Some(file_system_sandbox_policy.clone()),
Some(NetworkSandboxPolicy::Restricted),
)
.expect("split-only policy should preserve provided legacy fallback");
assert_eq!(resolved.sandbox_policy, sandbox_policy);
assert_eq!(
resolved.file_system_sandbox_policy,
file_system_sandbox_policy
);
assert_eq!(
resolved.network_sandbox_policy,
NetworkSandboxPolicy::Restricted
);
}
#[test]
fn resolve_sandbox_policies_accepts_semantically_equivalent_workspace_write_inputs() {
let temp_dir = tempfile::TempDir::new().expect("tempdir");
let workspace = temp_dir.path().join("workspace");
std::fs::create_dir_all(&workspace).expect("create workspace");
let workspace = AbsolutePathBuf::from_absolute_path(&workspace).expect("absolute workspace");
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![workspace],
read_only_access: ReadOnlyAccess::FullAccess,
network_access: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
};
let file_system_sandbox_policy =
FileSystemSandboxPolicy::from(&SandboxPolicy::new_workspace_write_policy());
let resolved = resolve_sandbox_policies(
temp_dir.path().join("workspace").as_path(),
Some(sandbox_policy.clone()),
Some(file_system_sandbox_policy.clone()),
Some(NetworkSandboxPolicy::Restricted),
)
.expect("semantically equivalent legacy workspace-write policy should resolve");
assert_eq!(resolved.sandbox_policy, sandbox_policy);
assert_eq!(
resolved.file_system_sandbox_policy,
file_system_sandbox_policy
);
assert_eq!(
resolved.network_sandbox_policy,
NetworkSandboxPolicy::Restricted
);
}
#[test]
fn apply_seccomp_then_exec_with_legacy_landlock_panics() {
let result = std::panic::catch_unwind(|| {
ensure_inner_stage_mode_is_valid(
/*apply_seccomp_then_exec*/ true, /*use_legacy_landlock*/ true,
)
});
assert!(result.is_err());
}
#[test]
fn legacy_landlock_rejects_split_only_filesystem_policies() {
let temp_dir = tempfile::TempDir::new().expect("tempdir");
let docs = temp_dir.path().join("docs");
std::fs::create_dir_all(&docs).expect("create docs");
let docs = AbsolutePathBuf::from_absolute_path(&docs).expect("absolute docs");
let 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::Path { path: docs },
access: codex_protocol::permissions::FileSystemAccessMode::Write,
},
]);
let result = std::panic::catch_unwind(|| {
ensure_legacy_landlock_mode_supports_policy(
/*use_legacy_landlock*/ true,
&policy,
NetworkSandboxPolicy::Restricted,
temp_dir.path(),
);
});
assert!(result.is_err());
}
#[test]
fn valid_inner_stage_modes_do_not_panic() {
ensure_inner_stage_mode_is_valid(
/*apply_seccomp_then_exec*/ false, /*use_legacy_landlock*/ false,
);
ensure_inner_stage_mode_is_valid(
/*apply_seccomp_then_exec*/ false, /*use_legacy_landlock*/ true,
);
ensure_inner_stage_mode_is_valid(
/*apply_seccomp_then_exec*/ true, /*use_legacy_landlock*/ false,
);
}