Codex Handbook
linux-sandbox/src/landlock.rs 347 lines
//! In-process Linux sandbox primitives: `no_new_privs` and seccomp.//!//! Filesystem restrictions are enforced by bubblewrap in `linux_run_main`.//! Landlock helpers remain available here as legacy/backup utilities.use std::collections::BTreeMap;use std::path::Path;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 the network seccomp filter when network access is disabled.////// Filesystem restrictions are intentionally handled by bubblewrap.pub(crate) fn apply_permission_profile_to_current_thread(    permission_profile: &PermissionProfile,    cwd: &Path,    apply_landlock_fs: bool,    allow_network_for_proxy: bool,    proxy_routed_network: 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,        allow_network_for_proxy,        proxy_routed_network,    );    // `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)?;    }    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,}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.////// Note: this is currently unused because filesystem sandboxing is performed/// via bubblewrap. It is kept for reference and potential fallback use.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,) -> 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();    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);    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) but deny socket()            // for all other families, including AF_UNIX. Only AF_UNIX            // socketpair() remains available for process-local IPC because it            // cannot connect to a socket outside the sandbox or bypass the            // bridge.            let deny_non_ip_socket = SeccompRule::new(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,                )?,            ])?;            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]);        }    }    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        );    }}