Files
codex/codex-rs/sandboxing/src/manager.rs
T
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

314 lines
11 KiB
Rust

#[cfg(target_os = "linux")]
use crate::bwrap::WSL1_BWRAP_WARNING;
#[cfg(target_os = "linux")]
use crate::bwrap::is_wsl1;
use crate::landlock::CODEX_LINUX_SANDBOX_ARG0;
use crate::landlock::allow_network_for_proxy;
use crate::landlock::create_linux_sandbox_command_args_for_policies;
use crate::policy_transforms::EffectiveSandboxPermissions;
use crate::policy_transforms::effective_file_system_sandbox_policy;
use crate::policy_transforms::effective_network_sandbox_policy;
use crate::policy_transforms::should_require_platform_sandbox;
use codex_network_proxy::NetworkProxy;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::SandboxPolicy;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::collections::HashMap;
use std::ffi::OsString;
use std::path::Path;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SandboxType {
None,
MacosSeatbelt,
LinuxSeccomp,
WindowsRestrictedToken,
}
impl SandboxType {
pub fn as_metric_tag(self) -> &'static str {
match self {
SandboxType::None => "none",
SandboxType::MacosSeatbelt => "seatbelt",
SandboxType::LinuxSeccomp => "seccomp",
SandboxType::WindowsRestrictedToken => "windows_sandbox",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SandboxablePreference {
Auto,
Require,
Forbid,
}
pub fn get_platform_sandbox(windows_sandbox_enabled: bool) -> Option<SandboxType> {
if cfg!(target_os = "macos") {
Some(SandboxType::MacosSeatbelt)
} else if cfg!(target_os = "linux") {
Some(SandboxType::LinuxSeccomp)
} else if cfg!(target_os = "windows") {
if windows_sandbox_enabled {
Some(SandboxType::WindowsRestrictedToken)
} else {
None
}
} else {
None
}
}
#[derive(Debug)]
pub struct SandboxCommand {
pub program: OsString,
pub args: Vec<String>,
pub cwd: AbsolutePathBuf,
pub env: HashMap<String, String>,
pub additional_permissions: Option<PermissionProfile>,
}
#[derive(Debug)]
pub struct SandboxExecRequest {
pub command: Vec<String>,
pub cwd: AbsolutePathBuf,
pub env: HashMap<String, String>,
pub network: Option<NetworkProxy>,
pub sandbox: SandboxType,
pub windows_sandbox_level: WindowsSandboxLevel,
pub windows_sandbox_private_desktop: bool,
pub sandbox_policy: SandboxPolicy,
pub file_system_sandbox_policy: FileSystemSandboxPolicy,
pub network_sandbox_policy: NetworkSandboxPolicy,
pub arg0: Option<String>,
}
/// Bundled arguments for sandbox transformation.
///
/// This keeps call sites self-documenting when several fields are optional.
pub struct SandboxTransformRequest<'a> {
pub command: SandboxCommand,
pub policy: &'a SandboxPolicy,
pub file_system_policy: &'a FileSystemSandboxPolicy,
pub network_policy: NetworkSandboxPolicy,
pub sandbox: SandboxType,
pub enforce_managed_network: bool,
// TODO(viyatb): Evaluate switching this to Option<Arc<NetworkProxy>>
// to make shared ownership explicit across runtime/sandbox plumbing.
pub network: Option<&'a NetworkProxy>,
pub sandbox_policy_cwd: &'a Path,
pub codex_linux_sandbox_exe: Option<&'a Path>,
pub use_legacy_landlock: bool,
pub windows_sandbox_level: WindowsSandboxLevel,
pub windows_sandbox_private_desktop: bool,
}
#[derive(Debug)]
pub enum SandboxTransformError {
MissingLinuxSandboxExecutable,
#[cfg(target_os = "linux")]
Wsl1UnsupportedForBubblewrap,
#[cfg(not(target_os = "macos"))]
SeatbeltUnavailable,
}
impl std::fmt::Display for SandboxTransformError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MissingLinuxSandboxExecutable => {
write!(f, "missing codex-linux-sandbox executable path")
}
#[cfg(target_os = "linux")]
Self::Wsl1UnsupportedForBubblewrap => write!(f, "{WSL1_BWRAP_WARNING}"),
#[cfg(not(target_os = "macos"))]
Self::SeatbeltUnavailable => write!(f, "seatbelt sandbox is only available on macOS"),
}
}
}
impl std::error::Error for SandboxTransformError {}
#[derive(Default)]
pub struct SandboxManager;
impl SandboxManager {
pub fn new() -> Self {
Self
}
pub fn select_initial(
&self,
file_system_policy: &FileSystemSandboxPolicy,
network_policy: NetworkSandboxPolicy,
pref: SandboxablePreference,
windows_sandbox_level: WindowsSandboxLevel,
has_managed_network_requirements: bool,
) -> SandboxType {
match pref {
SandboxablePreference::Forbid => SandboxType::None,
SandboxablePreference::Require => {
get_platform_sandbox(windows_sandbox_level != WindowsSandboxLevel::Disabled)
.unwrap_or(SandboxType::None)
}
SandboxablePreference::Auto => {
if should_require_platform_sandbox(
file_system_policy,
network_policy,
has_managed_network_requirements,
) {
get_platform_sandbox(windows_sandbox_level != WindowsSandboxLevel::Disabled)
.unwrap_or(SandboxType::None)
} else {
SandboxType::None
}
}
}
}
pub fn transform(
&self,
request: SandboxTransformRequest<'_>,
) -> Result<SandboxExecRequest, SandboxTransformError> {
let SandboxTransformRequest {
mut command,
policy,
file_system_policy,
network_policy,
sandbox,
enforce_managed_network,
network,
sandbox_policy_cwd,
codex_linux_sandbox_exe,
use_legacy_landlock,
windows_sandbox_level,
windows_sandbox_private_desktop,
} = request;
let additional_permissions = command.additional_permissions.take();
let EffectiveSandboxPermissions {
sandbox_policy: effective_policy,
} = EffectiveSandboxPermissions::new(policy, additional_permissions.as_ref());
let effective_file_system_policy = effective_file_system_sandbox_policy(
file_system_policy,
additional_permissions.as_ref(),
);
let effective_network_policy =
effective_network_sandbox_policy(network_policy, additional_permissions.as_ref());
let mut argv = Vec::with_capacity(1 + command.args.len());
argv.push(command.program);
argv.extend(command.args.into_iter().map(OsString::from));
let (argv, arg0_override) = match sandbox {
SandboxType::None => (os_argv_to_strings(argv), None),
#[cfg(target_os = "macos")]
SandboxType::MacosSeatbelt => {
use crate::seatbelt::CreateSeatbeltCommandArgsParams;
use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE;
use crate::seatbelt::create_seatbelt_command_args;
let mut args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams {
command: os_argv_to_strings(argv),
file_system_sandbox_policy: &effective_file_system_policy,
network_sandbox_policy: effective_network_policy,
sandbox_policy_cwd,
enforce_managed_network,
network,
extra_allow_unix_sockets: &[],
});
let mut full_command = Vec::with_capacity(1 + args.len());
full_command.push(MACOS_PATH_TO_SEATBELT_EXECUTABLE.to_string());
full_command.append(&mut args);
(full_command, None)
}
#[cfg(not(target_os = "macos"))]
SandboxType::MacosSeatbelt => return Err(SandboxTransformError::SeatbeltUnavailable),
SandboxType::LinuxSeccomp => {
let exe = codex_linux_sandbox_exe
.ok_or(SandboxTransformError::MissingLinuxSandboxExecutable)?;
let allow_proxy_network = allow_network_for_proxy(enforce_managed_network);
#[cfg(target_os = "linux")]
ensure_linux_bubblewrap_is_supported(
&effective_file_system_policy,
use_legacy_landlock,
allow_proxy_network,
is_wsl1(),
)?;
let mut args = create_linux_sandbox_command_args_for_policies(
os_argv_to_strings(argv),
command.cwd.as_path(),
&effective_policy,
&effective_file_system_policy,
effective_network_policy,
sandbox_policy_cwd,
use_legacy_landlock,
allow_proxy_network,
);
let mut full_command = Vec::with_capacity(1 + args.len());
full_command.push(os_string_to_command_component(exe.as_os_str().to_owned()));
full_command.append(&mut args);
(full_command, Some(linux_sandbox_arg0_override(exe)))
}
#[cfg(target_os = "windows")]
SandboxType::WindowsRestrictedToken => (os_argv_to_strings(argv), None),
#[cfg(not(target_os = "windows"))]
SandboxType::WindowsRestrictedToken => (os_argv_to_strings(argv), None),
};
Ok(SandboxExecRequest {
command: argv,
cwd: command.cwd,
env: command.env,
network: network.cloned(),
sandbox,
windows_sandbox_level,
windows_sandbox_private_desktop,
sandbox_policy: effective_policy,
file_system_sandbox_policy: effective_file_system_policy,
network_sandbox_policy: effective_network_policy,
arg0: arg0_override,
})
}
}
#[cfg(target_os = "linux")]
fn ensure_linux_bubblewrap_is_supported(
file_system_sandbox_policy: &FileSystemSandboxPolicy,
use_legacy_landlock: bool,
allow_network_for_proxy: bool,
is_wsl1: bool,
) -> Result<(), SandboxTransformError> {
let requires_bubblewrap = !use_legacy_landlock
&& (!file_system_sandbox_policy.has_full_disk_write_access() || allow_network_for_proxy);
if is_wsl1 && requires_bubblewrap {
return Err(SandboxTransformError::Wsl1UnsupportedForBubblewrap);
}
Ok(())
}
fn os_argv_to_strings(argv: Vec<OsString>) -> Vec<String> {
argv.into_iter()
.map(os_string_to_command_component)
.collect()
}
fn os_string_to_command_component(value: OsString) -> String {
value
.into_string()
.unwrap_or_else(|value| value.to_string_lossy().into_owned())
}
fn linux_sandbox_arg0_override(exe: &Path) -> String {
if exe.file_name().and_then(|name| name.to_str()) == Some(CODEX_LINUX_SANDBOX_ARG0) {
os_string_to_command_component(exe.as_os_str().to_owned())
} else {
CODEX_LINUX_SANDBOX_ARG0.to_string()
}
}
#[cfg(test)]
#[path = "manager_tests.rs"]
mod tests;