mirror of
https://github.com/openai/codex.git
synced 2026-10-22 17:58:46 +00:00
## What changed Extract the inline test modules for `bundled_bwrap`, `bwrap`, `exec_util`, `launcher`, `proxy_routing`, and `seccomp` into sibling `*_tests.rs` files. Keep them wired through `#[cfg(test)]` and explicit `#[path]` attributes, with existing test coverage and runtime behavior unchanged. GitOrigin-RevId: 79b70a092a19b10716939744542415bc1dcaf0b2
307 lines
12 KiB
Rust
307 lines
12 KiB
Rust
//! In-process Linux sandbox primitives: `no_new_privs` and seccomp.
|
||
//!
|
||
//! Filesystem restrictions are enforced by bubblewrap in `linux_run_main`.
|
||
//! The legacy Landlock filesystem implementation is retained here, but the
|
||
//! helper rejects legacy mode for filesystem-restricted policies.
|
||
use std::collections::BTreeMap;
|
||
use std::path::Path;
|
||
|
||
use codex_network_proxy::ManagedNetworkSandboxContext;
|
||
use codex_protocol::error::CodexErr;
|
||
use codex_protocol::error::Result;
|
||
use codex_protocol::error::SandboxErr;
|
||
use codex_protocol::models::PermissionProfile;
|
||
use codex_protocol::protocol::NetworkSandboxPolicy;
|
||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||
|
||
use landlock::ABI;
|
||
#[allow(unused_imports)]
|
||
use landlock::Access;
|
||
use landlock::AccessFs;
|
||
use landlock::CompatLevel;
|
||
use landlock::Compatible;
|
||
use landlock::Ruleset;
|
||
use landlock::RulesetAttr;
|
||
use landlock::RulesetCreatedAttr;
|
||
use seccompiler::BpfProgram;
|
||
use seccompiler::SeccompAction;
|
||
use seccompiler::SeccompCmpArgLen;
|
||
use seccompiler::SeccompCmpOp;
|
||
use seccompiler::SeccompCondition;
|
||
use seccompiler::SeccompFilter;
|
||
use seccompiler::SeccompRule;
|
||
use seccompiler::TargetArch;
|
||
use seccompiler::apply_filter;
|
||
|
||
/// Apply sandbox policies inside this thread so only the child inherits
|
||
/// them, not the entire CLI process.
|
||
///
|
||
/// This function is responsible for:
|
||
/// - enabling `PR_SET_NO_NEW_PRIVS` when restrictions apply, and
|
||
/// - installing seccomp restrictions for network isolation and VM sockets.
|
||
///
|
||
/// Filesystem restrictions are normally handled by bubblewrap. The legacy
|
||
/// Landlock implementation is only selected by `apply_landlock_fs`.
|
||
pub(crate) fn apply_permission_profile_to_current_thread(
|
||
permission_profile: &PermissionProfile,
|
||
cwd: &Path,
|
||
apply_landlock_fs: bool,
|
||
managed_network: Option<&ManagedNetworkSandboxContext>,
|
||
proxy_routing_active: bool,
|
||
) -> Result<()> {
|
||
let (file_system_sandbox_policy, network_sandbox_policy) =
|
||
permission_profile.to_runtime_permissions();
|
||
let network_seccomp_mode = network_seccomp_mode(
|
||
network_sandbox_policy,
|
||
managed_network.is_some(),
|
||
proxy_routing_active,
|
||
)
|
||
.or_else(|| {
|
||
// VM sockets can reach host services outside the filesystem sandbox.
|
||
// In WSL2 they also allow Windows process launch through an alias of
|
||
// the interop socket, even when /run/WSL is masked. Keep ordinary
|
||
// network access while denying that host bridge.
|
||
(!file_system_sandbox_policy.has_full_disk_write_access())
|
||
.then_some(NetworkSeccompMode::VmSocketRestricted)
|
||
});
|
||
|
||
// `PR_SET_NO_NEW_PRIVS` is required for seccomp, but it also prevents
|
||
// setuid privilege elevation. Many `bwrap` deployments rely on setuid, so
|
||
// we avoid this unless we need seccomp or we are explicitly using the
|
||
// legacy Landlock filesystem pipeline.
|
||
if network_seccomp_mode.is_some()
|
||
|| (apply_landlock_fs && !file_system_sandbox_policy.has_full_disk_write_access())
|
||
{
|
||
set_no_new_privs()?;
|
||
}
|
||
|
||
if let Some(mode) = network_seccomp_mode {
|
||
install_network_seccomp_filter_on_current_thread(mode, managed_network)?;
|
||
}
|
||
|
||
if apply_landlock_fs && !file_system_sandbox_policy.has_full_disk_write_access() {
|
||
if !file_system_sandbox_policy.has_full_disk_read_access() {
|
||
return Err(CodexErr::UnsupportedOperation(
|
||
"Restricted read-only access is not supported by the legacy Linux Landlock filesystem backend."
|
||
.to_string(),
|
||
));
|
||
}
|
||
|
||
let writable_roots = file_system_sandbox_policy
|
||
.get_writable_roots_with_cwd(cwd)
|
||
.into_iter()
|
||
.map(|writable_root| writable_root.root)
|
||
.collect();
|
||
install_filesystem_landlock_rules_on_current_thread(writable_roots)?;
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
enum NetworkSeccompMode {
|
||
Restricted,
|
||
ProxyRouted,
|
||
VmSocketRestricted,
|
||
}
|
||
|
||
fn should_install_network_seccomp(
|
||
network_sandbox_policy: NetworkSandboxPolicy,
|
||
allow_network_for_proxy: bool,
|
||
) -> bool {
|
||
// Managed-network sessions should remain fail-closed even for policies that
|
||
// would normally grant full network access (for example, DangerFullAccess).
|
||
!network_sandbox_policy.is_enabled() || allow_network_for_proxy
|
||
}
|
||
|
||
fn network_seccomp_mode(
|
||
network_sandbox_policy: NetworkSandboxPolicy,
|
||
allow_network_for_proxy: bool,
|
||
proxy_routed_network: bool,
|
||
) -> Option<NetworkSeccompMode> {
|
||
if !should_install_network_seccomp(network_sandbox_policy, allow_network_for_proxy) {
|
||
None
|
||
} else if proxy_routed_network {
|
||
Some(NetworkSeccompMode::ProxyRouted)
|
||
} else {
|
||
Some(NetworkSeccompMode::Restricted)
|
||
}
|
||
}
|
||
|
||
/// Enable `PR_SET_NO_NEW_PRIVS` so seccomp can be applied safely.
|
||
fn set_no_new_privs() -> Result<()> {
|
||
let result = unsafe { libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) };
|
||
if result != 0 {
|
||
return Err(std::io::Error::last_os_error().into());
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// Installs Landlock file-system rules on the current thread allowing read
|
||
/// access to the entire file-system while restricting write access to
|
||
/// `/dev/null` and the provided list of `writable_roots`.
|
||
///
|
||
/// # Errors
|
||
/// Returns [`CodexErr::Sandbox`] variants when the ruleset fails to apply.
|
||
///
|
||
/// Retained for the legacy pipeline. The helper currently rejects legacy mode
|
||
/// for filesystem-restricted policies, so filesystem sandboxing uses bubblewrap.
|
||
fn install_filesystem_landlock_rules_on_current_thread(
|
||
writable_roots: Vec<AbsolutePathBuf>,
|
||
) -> Result<()> {
|
||
let abi = ABI::V5;
|
||
let access_rw = AccessFs::from_all(abi);
|
||
let access_ro = AccessFs::from_read(abi);
|
||
|
||
let mut ruleset = Ruleset::default()
|
||
.set_compatibility(CompatLevel::BestEffort)
|
||
.handle_access(access_rw)?
|
||
.create()?
|
||
.add_rules(landlock::path_beneath_rules(&["/"], access_ro))?
|
||
.add_rules(landlock::path_beneath_rules(&["/dev/null"], access_rw))?
|
||
.set_no_new_privs(true);
|
||
|
||
if !writable_roots.is_empty() {
|
||
ruleset = ruleset.add_rules(landlock::path_beneath_rules(&writable_roots, access_rw))?;
|
||
}
|
||
|
||
let status = ruleset.restrict_self()?;
|
||
|
||
if status.ruleset == landlock::RulesetStatus::NotEnforced {
|
||
return Err(CodexErr::Sandbox(SandboxErr::LandlockRestrict));
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Installs a seccomp filter for Linux network sandboxing.
|
||
///
|
||
/// The filter is applied to the current thread so only the sandboxed child
|
||
/// inherits it.
|
||
fn install_network_seccomp_filter_on_current_thread(
|
||
mode: NetworkSeccompMode,
|
||
managed_network: Option<&ManagedNetworkSandboxContext>,
|
||
) -> std::result::Result<(), SandboxErr> {
|
||
fn deny_syscall(rules: &mut BTreeMap<i64, Vec<SeccompRule>>, nr: i64) {
|
||
rules.insert(nr, vec![]); // empty rule vec = unconditional match
|
||
}
|
||
|
||
// Build rule map.
|
||
let mut rules: BTreeMap<i64, Vec<SeccompRule>> = BTreeMap::new();
|
||
|
||
if mode != NetworkSeccompMode::VmSocketRestricted {
|
||
deny_syscall(&mut rules, libc::SYS_ptrace);
|
||
deny_syscall(&mut rules, libc::SYS_process_vm_readv);
|
||
deny_syscall(&mut rules, libc::SYS_process_vm_writev);
|
||
}
|
||
// io_uring can create AF_VSOCK sockets without a socket() syscall, so
|
||
// keep it unavailable in every mode with socket-family restrictions.
|
||
deny_syscall(&mut rules, libc::SYS_io_uring_setup);
|
||
deny_syscall(&mut rules, libc::SYS_io_uring_enter);
|
||
deny_syscall(&mut rules, libc::SYS_io_uring_register);
|
||
|
||
match mode {
|
||
NetworkSeccompMode::Restricted => {
|
||
deny_syscall(&mut rules, libc::SYS_connect);
|
||
deny_syscall(&mut rules, libc::SYS_accept);
|
||
deny_syscall(&mut rules, libc::SYS_accept4);
|
||
deny_syscall(&mut rules, libc::SYS_bind);
|
||
deny_syscall(&mut rules, libc::SYS_listen);
|
||
deny_syscall(&mut rules, libc::SYS_getpeername);
|
||
deny_syscall(&mut rules, libc::SYS_getsockname);
|
||
deny_syscall(&mut rules, libc::SYS_shutdown);
|
||
deny_syscall(&mut rules, libc::SYS_sendto);
|
||
deny_syscall(&mut rules, libc::SYS_sendmmsg);
|
||
// NOTE: allowing recvfrom allows some tools like: `cargo clippy`
|
||
// to run with their socketpair + child processes for sub-proc
|
||
// management.
|
||
// deny_syscall(&mut rules, libc::SYS_recvfrom);
|
||
deny_syscall(&mut rules, libc::SYS_recvmmsg);
|
||
deny_syscall(&mut rules, libc::SYS_getsockopt);
|
||
deny_syscall(&mut rules, libc::SYS_setsockopt);
|
||
|
||
// For `socket` we allow AF_UNIX (arg0 == AF_UNIX) and deny
|
||
// everything else.
|
||
let unix_only_rule = SeccompRule::new(vec![SeccompCondition::new(
|
||
0, // first argument (domain)
|
||
SeccompCmpArgLen::Dword,
|
||
SeccompCmpOp::Ne,
|
||
libc::AF_UNIX as u64,
|
||
)?])?;
|
||
|
||
rules.insert(libc::SYS_socket, vec![unix_only_rule.clone()]);
|
||
rules.insert(libc::SYS_socketpair, vec![unix_only_rule]);
|
||
}
|
||
NetworkSeccompMode::ProxyRouted => {
|
||
// In proxy-routed mode we allow IP sockets in the isolated
|
||
// namespace (used to reach the local TCP bridge). Standalone Unix
|
||
// sockets require an explicit managed-policy grant; all other
|
||
// socket families remain denied.
|
||
let mut denied_socket_conditions = vec![
|
||
SeccompCondition::new(
|
||
0,
|
||
SeccompCmpArgLen::Dword,
|
||
SeccompCmpOp::Ne,
|
||
libc::AF_INET as u64,
|
||
)?,
|
||
SeccompCondition::new(
|
||
0,
|
||
SeccompCmpArgLen::Dword,
|
||
SeccompCmpOp::Ne,
|
||
libc::AF_INET6 as u64,
|
||
)?,
|
||
];
|
||
if managed_network.is_some_and(|context| context.dangerously_allow_all_unix_sockets) {
|
||
denied_socket_conditions.push(SeccompCondition::new(
|
||
0,
|
||
SeccompCmpArgLen::Dword,
|
||
SeccompCmpOp::Ne,
|
||
libc::AF_UNIX as u64,
|
||
)?);
|
||
}
|
||
let deny_non_ip_socket = SeccompRule::new(denied_socket_conditions)?;
|
||
let deny_non_unix_socketpair = SeccompRule::new(vec![SeccompCondition::new(
|
||
0,
|
||
SeccompCmpArgLen::Dword,
|
||
SeccompCmpOp::Ne,
|
||
libc::AF_UNIX as u64,
|
||
)?])?;
|
||
rules.insert(libc::SYS_socket, vec![deny_non_ip_socket]);
|
||
rules.insert(libc::SYS_socketpair, vec![deny_non_unix_socketpair]);
|
||
}
|
||
NetworkSeccompMode::VmSocketRestricted => {
|
||
let deny_vsock = SeccompRule::new(vec![SeccompCondition::new(
|
||
0,
|
||
SeccompCmpArgLen::Dword,
|
||
SeccompCmpOp::Eq,
|
||
libc::AF_VSOCK as u64,
|
||
)?])?;
|
||
rules.insert(libc::SYS_socket, vec![deny_vsock.clone()]);
|
||
rules.insert(libc::SYS_socketpair, vec![deny_vsock]);
|
||
}
|
||
}
|
||
|
||
let filter = SeccompFilter::new(
|
||
rules,
|
||
SeccompAction::Allow, // default – allow
|
||
SeccompAction::Errno(libc::EPERM as u32), // when rule matches – return EPERM
|
||
if cfg!(target_arch = "x86_64") {
|
||
TargetArch::x86_64
|
||
} else if cfg!(target_arch = "aarch64") {
|
||
TargetArch::aarch64
|
||
} else {
|
||
unimplemented!("unsupported architecture for seccomp filter");
|
||
},
|
||
)?;
|
||
|
||
let prog: BpfProgram = filter.try_into()?;
|
||
|
||
apply_filter(&prog)?;
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[cfg(test)]
|
||
#[path = "seccomp_tests.rs"]
|
||
mod tests;
|