mirror of
https://github.com/openai/codex.git
synced 2026-05-18 02:02:30 +00:00
## Why `PermissionProfile` is the canonical runtime permission model in the Rust workspace, but the Linux sandbox helper still accepted a legacy `SandboxPolicy` plus separate filesystem and network policy flags. That translation layer made the helper interface harder to reason about and left `linux-sandbox`-specific callers and tests coupled to the legacy policy representation. This change moves the helper onto `PermissionProfile` directly so the Linux sandbox plumbing matches the rest of the permission stack. ## What changed - changed `codex-linux-sandbox` to accept `--permission-profile` and derive the runtime filesystem and network policies internally - updated the in-process seccomp and legacy Landlock path in `codex-rs/linux-sandbox` to operate on `PermissionProfile` - updated Linux sandbox argv construction in `codex-rs/sandboxing`, `codex-rs/core`, and the CLI debug sandbox path to pass the canonical profile instead of serializing compatibility policy projections - simplified the Linux sandbox tests to build the exact permission profile under test, including the managed-proxy path and direct-runtime-enforcement carveout coverage - removed helper-local `SandboxPolicy` usage from `bwrap` tests where `FileSystemSandboxPolicy` is already the value being exercised ## Testing - `cargo test -p codex-sandboxing` - `cargo test -p codex-linux-sandbox` (on this macOS host, the crate compiled cleanly and its Linux-only tests were cfg-gated) - `cargo test -p codex-core --no-run` - `cargo test -p codex-cli --no-run`
346 lines
12 KiB
Rust
346 lines
12 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_permission_profile;
|
|
use crate::policy_transforms::effective_permission_profile;
|
|
use crate::policy_transforms::should_require_platform_sandbox;
|
|
use codex_network_proxy::NetworkProxy;
|
|
use codex_protocol::config_types::WindowsSandboxLevel;
|
|
use codex_protocol::models::AdditionalPermissionProfile;
|
|
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<AdditionalPermissionProfile>,
|
|
}
|
|
|
|
#[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 permission_profile: PermissionProfile,
|
|
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 permissions: &'a PermissionProfile,
|
|
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,
|
|
permissions,
|
|
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 effective_permission_profile =
|
|
effective_permission_profile(permissions, additional_permissions.as_ref());
|
|
let (effective_file_system_policy, effective_network_policy) =
|
|
effective_permission_profile.to_runtime_permissions();
|
|
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_permission_profile(
|
|
os_argv_to_strings(argv),
|
|
command.cwd.as_path(),
|
|
&effective_permission_profile,
|
|
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,
|
|
permission_profile: effective_permission_profile,
|
|
file_system_sandbox_policy: effective_file_system_policy,
|
|
network_sandbox_policy: effective_network_policy,
|
|
arg0: arg0_override,
|
|
})
|
|
}
|
|
}
|
|
|
|
pub fn compatibility_sandbox_policy_for_permission_profile(
|
|
permissions: &PermissionProfile,
|
|
file_system_policy: &FileSystemSandboxPolicy,
|
|
network_policy: NetworkSandboxPolicy,
|
|
cwd: &Path,
|
|
) -> SandboxPolicy {
|
|
permissions
|
|
.to_legacy_sandbox_policy(cwd)
|
|
.unwrap_or_else(|_| {
|
|
compatibility_workspace_write_policy(file_system_policy, network_policy, cwd)
|
|
})
|
|
}
|
|
|
|
fn compatibility_workspace_write_policy(
|
|
file_system_policy: &FileSystemSandboxPolicy,
|
|
network_policy: NetworkSandboxPolicy,
|
|
cwd: &Path,
|
|
) -> SandboxPolicy {
|
|
let cwd_abs = AbsolutePathBuf::from_absolute_path(cwd).ok();
|
|
let writable_roots = file_system_policy
|
|
.get_writable_roots_with_cwd(cwd)
|
|
.into_iter()
|
|
.map(|root| root.root)
|
|
.filter(|root| cwd_abs.as_ref() != Some(root))
|
|
.collect();
|
|
let tmpdir_writable = std::env::var_os("TMPDIR")
|
|
.filter(|tmpdir| !tmpdir.is_empty())
|
|
.and_then(|tmpdir| {
|
|
AbsolutePathBuf::from_absolute_path(std::path::PathBuf::from(tmpdir)).ok()
|
|
})
|
|
.is_some_and(|tmpdir| file_system_policy.can_write_path_with_cwd(tmpdir.as_path(), cwd));
|
|
let slash_tmp = Path::new("/tmp");
|
|
let slash_tmp_writable = slash_tmp.is_absolute()
|
|
&& slash_tmp.is_dir()
|
|
&& file_system_policy.can_write_path_with_cwd(slash_tmp, cwd);
|
|
|
|
SandboxPolicy::WorkspaceWrite {
|
|
writable_roots,
|
|
network_access: network_policy.is_enabled(),
|
|
exclude_tmpdir_env_var: !tmpdir_writable,
|
|
exclude_slash_tmp: !slash_tmp_writable,
|
|
}
|
|
}
|
|
|
|
#[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;
|