mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
Fixes #15283. ## Summary Older system bubblewrap builds reject `--argv0`, which makes our Linux sandbox fail before the helper can re-exec. This PR keeps using system `/usr/bin/bwrap` whenever it exists and only falls back to vendored bwrap when the system binary is missing. That matters on stricter AppArmor hosts, where the distro bwrap package also provides the policy setup needed for user namespaces. For old system bwrap, we avoid `--argv0` instead of switching binaries: - pass the sandbox helper a full-path `argv0`, - keep the existing `current_exe() + --argv0` path when the selected launcher supports it, - otherwise omit `--argv0` and re-exec through the helper's own `argv[0]` path, whose basename still dispatches as `codex-linux-sandbox`. Also updates the launcher/warning tests and docs so they match the new behavior: present-but-old system bwrap uses the compatibility path, and only absent system bwrap falls back to vendored. ### Validation 1. Install Ubuntu 20.04 in a VM 2. Compile codex and run without bubblewrap installed - see a warning about falling back to the vendored bwrap 3. Install bwrap and verify version is 0.4.0 without `argv0` support 4. run codex and use apply_patch tool without errors <img width="802" height="631" alt="Screenshot 2026-03-25 at 11 48 36 PM" src="https://github.com/user-attachments/assets/77248a29-aa38-4d7c-9833-496ec6a458b8" /> <img width="807" height="634" alt="Screenshot 2026-03-25 at 11 47 32 PM" src="https://github.com/user-attachments/assets/5af8b850-a466-489b-95a6-455b76b5050f" /> <img width="812" height="635" alt="Screenshot 2026-03-25 at 11 45 45 PM" src="https://github.com/user-attachments/assets/438074f0-8435-4274-a667-332efdd5cb57" /> <img width="801" height="623" alt="Screenshot 2026-03-25 at 11 43 56 PM" src="https://github.com/user-attachments/assets/0dc8d3f5-e8cf-4218-b4b4-a4f7d9bf02e3" /> --------- Co-authored-by: Michael Bolin <mbolin@openai.com>
289 lines
10 KiB
Rust
289 lines
10 KiB
Rust
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;
|
|
#[cfg(target_os = "macos")]
|
|
use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE;
|
|
#[cfg(target_os = "macos")]
|
|
use crate::seatbelt::create_seatbelt_command_args_for_policies_with_extensions;
|
|
use codex_network_proxy::NetworkProxy;
|
|
use codex_protocol::config_types::WindowsSandboxLevel;
|
|
#[cfg(target_os = "macos")]
|
|
use codex_protocol::models::MacOsSeatbeltProfileExtensions;
|
|
use codex_protocol::models::PermissionProfile;
|
|
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
|
use codex_protocol::permissions::NetworkSandboxPolicy;
|
|
use codex_protocol::protocol::SandboxPolicy;
|
|
use std::collections::HashMap;
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
|
|
#[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: String,
|
|
pub args: Vec<String>,
|
|
pub cwd: PathBuf,
|
|
pub env: HashMap<String, String>,
|
|
pub additional_permissions: Option<PermissionProfile>,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct SandboxExecRequest {
|
|
pub command: Vec<String>,
|
|
pub cwd: PathBuf,
|
|
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,
|
|
#[cfg(target_os = "macos")]
|
|
pub macos_seatbelt_profile_extensions: Option<&'a MacOsSeatbeltProfileExtensions>,
|
|
pub codex_linux_sandbox_exe: Option<&'a PathBuf>,
|
|
pub use_legacy_landlock: bool,
|
|
pub windows_sandbox_level: WindowsSandboxLevel,
|
|
pub windows_sandbox_private_desktop: bool,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum SandboxTransformError {
|
|
MissingLinuxSandboxExecutable,
|
|
#[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(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,
|
|
#[cfg(target_os = "macos")]
|
|
macos_seatbelt_profile_extensions,
|
|
codex_linux_sandbox_exe,
|
|
use_legacy_landlock,
|
|
windows_sandbox_level,
|
|
windows_sandbox_private_desktop,
|
|
} = request;
|
|
#[cfg(not(target_os = "macos"))]
|
|
let macos_seatbelt_profile_extensions = None;
|
|
let additional_permissions = command.additional_permissions.take();
|
|
let EffectiveSandboxPermissions {
|
|
sandbox_policy: effective_policy,
|
|
#[cfg(target_os = "macos")]
|
|
macos_seatbelt_profile_extensions: effective_macos_seatbelt_profile_extensions,
|
|
#[cfg(not(target_os = "macos"))]
|
|
macos_seatbelt_profile_extensions: _,
|
|
} = EffectiveSandboxPermissions::new(
|
|
policy,
|
|
macos_seatbelt_profile_extensions,
|
|
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.append(&mut command.args);
|
|
|
|
let (argv, arg0_override) = match sandbox {
|
|
SandboxType::None => (argv, None),
|
|
#[cfg(target_os = "macos")]
|
|
SandboxType::MacosSeatbelt => {
|
|
let mut args = create_seatbelt_command_args_for_policies_with_extensions(
|
|
argv.clone(),
|
|
&effective_file_system_policy,
|
|
effective_network_policy,
|
|
sandbox_policy_cwd,
|
|
enforce_managed_network,
|
|
network,
|
|
effective_macos_seatbelt_profile_extensions.as_ref(),
|
|
);
|
|
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);
|
|
let mut args = create_linux_sandbox_command_args_for_policies(
|
|
argv.clone(),
|
|
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(exe.to_string_lossy().to_string());
|
|
full_command.append(&mut args);
|
|
(
|
|
full_command,
|
|
Some(linux_sandbox_arg0_override(exe.as_path())),
|
|
)
|
|
}
|
|
#[cfg(target_os = "windows")]
|
|
SandboxType::WindowsRestrictedToken => (argv, None),
|
|
#[cfg(not(target_os = "windows"))]
|
|
SandboxType::WindowsRestrictedToken => (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,
|
|
})
|
|
}
|
|
}
|
|
|
|
fn linux_sandbox_arg0_override(exe: &Path) -> String {
|
|
if exe.file_name().and_then(|name| name.to_str()) == Some(CODEX_LINUX_SANDBOX_ARG0) {
|
|
exe.to_string_lossy().into_owned()
|
|
} else {
|
|
CODEX_LINUX_SANDBOX_ARG0.to_string()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "manager_tests.rs"]
|
|
mod tests;
|