//! 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 { 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, ) -> 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>, nr: i64) { rules.insert(nr, vec![]); // empty rule vec = unconditional match } // Build rule map. let mut rules: BTreeMap> = 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)] mod tests { use super::NetworkSeccompMode; use super::network_seccomp_mode; use super::should_install_network_seccomp; use codex_protocol::protocol::NetworkSandboxPolicy; use pretty_assertions::assert_eq; #[test] fn managed_network_enforces_seccomp_even_for_full_network_policy() { assert_eq!( should_install_network_seccomp( NetworkSandboxPolicy::Enabled, /*allow_network_for_proxy*/ true, ), true ); } #[test] fn full_network_policy_without_managed_network_skips_seccomp() { assert_eq!( should_install_network_seccomp( NetworkSandboxPolicy::Enabled, /*allow_network_for_proxy*/ false, ), false ); } #[test] fn restricted_network_policy_always_installs_seccomp() { assert!(should_install_network_seccomp( NetworkSandboxPolicy::Restricted, /*allow_network_for_proxy*/ false, )); assert!(should_install_network_seccomp( NetworkSandboxPolicy::Restricted, /*allow_network_for_proxy*/ true, )); } #[test] fn managed_proxy_routes_use_proxy_routed_seccomp_mode() { assert_eq!( network_seccomp_mode( NetworkSandboxPolicy::Enabled, /*allow_network_for_proxy*/ true, /*proxy_routed_network*/ true, ), Some(NetworkSeccompMode::ProxyRouted) ); } #[test] fn restricted_network_without_proxy_routing_uses_restricted_mode() { assert_eq!( network_seccomp_mode( NetworkSandboxPolicy::Restricted, /*allow_network_for_proxy*/ false, /*proxy_routed_network*/ false, ), Some(NetworkSeccompMode::Restricted) ); } #[test] fn full_network_without_managed_proxy_skips_network_seccomp_mode() { assert_eq!( network_seccomp_mode( NetworkSandboxPolicy::Enabled, /*allow_network_for_proxy*/ false, /*proxy_routed_network*/ false, ), None ); } }