mirror of
https://github.com/openai/codex.git
synced 2026-10-28 18:58:46 +00:00
Replace the exec-side legacy SandboxPolicy plumbing with PermissionProfile-based Windows sandbox override resolution. The runtime still needs a legacy SandboxPolicy projection for a few compatibility surfaces and for Windows override baseline comparisons, but that projection is now derived from a single PermissionProfile instead of accepting separately materialized FileSystemSandboxPolicy and NetworkSandboxPolicy inputs. This avoids mismatched permission arguments while keeping the migration scoped to the existing compatibility boundary. Keep the Windows exec tests aligned with that shape by constructing PermissionProfile values directly, without routing test setup through SandboxPolicy compatibility conversions. Validation: - just test -p codex-sandboxing - just test -p codex-core windows_restricted_token - CI for the follow-up exec_tests cleanup
373 lines
14 KiB
Rust
373 lines
14 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
|
|
}
|
|
}
|
|
|
|
pub fn with_managed_mitm_ca_readable_root(
|
|
permission_profile: PermissionProfile,
|
|
managed_mitm_ca_trust_bundle_path: Option<&AbsolutePathBuf>,
|
|
sandbox_policy_cwd: &Path,
|
|
) -> PermissionProfile {
|
|
let Some(managed_mitm_ca_trust_bundle_path) = managed_mitm_ca_trust_bundle_path else {
|
|
return permission_profile;
|
|
};
|
|
let (file_system_sandbox_policy, network_sandbox_policy) =
|
|
permission_profile.to_runtime_permissions();
|
|
let file_system_sandbox_policy = file_system_sandbox_policy.with_additional_readable_roots(
|
|
sandbox_policy_cwd,
|
|
std::slice::from_ref(managed_mitm_ca_trust_bundle_path),
|
|
);
|
|
PermissionProfile::from_runtime_permissions_with_enforcement(
|
|
permission_profile.enforcement(),
|
|
&file_system_sandbox_policy,
|
|
network_sandbox_policy,
|
|
)
|
|
}
|
|
|
|
#[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 managed_mitm_ca_trust_bundle_path =
|
|
network.and_then(NetworkProxy::managed_mitm_ca_trust_bundle_path);
|
|
let effective_permission_profile =
|
|
effective_permission_profile(permissions, additional_permissions.as_ref());
|
|
let effective_permission_profile = with_managed_mitm_ca_readable_root(
|
|
effective_permission_profile,
|
|
managed_mitm_ca_trust_bundle_path.as_ref(),
|
|
sandbox_policy_cwd,
|
|
);
|
|
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,
|
|
cwd: &Path,
|
|
) -> SandboxPolicy {
|
|
permissions
|
|
.to_legacy_sandbox_policy(cwd)
|
|
.unwrap_or_else(|_| {
|
|
let (file_system_policy, network_policy) = permissions.to_runtime_permissions();
|
|
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;
|