diff --git a/codex-rs/linux-sandbox/src/seccomp.rs b/codex-rs/linux-sandbox/src/seccomp.rs index 7c9f588b1b..5d524da38a 100644 --- a/codex-rs/linux-sandbox/src/seccomp.rs +++ b/codex-rs/linux-sandbox/src/seccomp.rs @@ -182,7 +182,40 @@ fn install_network_seccomp_filter_on_current_thread( 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 + add_syscall_rules(rules, nr, vec![]); // empty rule vec = unconditional match + } + + fn add_syscall_rules( + rules: &mut BTreeMap>, + nr: i64, + syscall_rules: Vec, + ) { + #[cfg(target_arch = "x86_64")] + { + const X32_SYSCALL_BIT: i64 = 0x4000_0000; + // x32 shares AUDIT_ARCH_X86_64, so the architecture check alone + // does not enforce these restrictions for x32 callers. + rules.insert(nr | X32_SYSCALL_BIT, syscall_rules.clone()); + + // These restricted calls have separate x32 syscall numbers. + let x32_nr = match nr { + libc::SYS_ptrace => Some(521), + libc::SYS_recvmmsg => Some(537), + libc::SYS_sendmmsg => Some(538), + libc::SYS_process_vm_readv => Some(539), + libc::SYS_process_vm_writev => Some(540), + libc::SYS_setsockopt => Some(541), + libc::SYS_getsockopt => Some(542), + _ => None, + }; + if let Some(x32_nr) = x32_nr { + rules.insert(x32_nr | X32_SYSCALL_BIT, syscall_rules.clone()); + // Before Linux 5.4, both the unflagged x32 number and the + // flagged native number above could also reach these calls. + rules.insert(x32_nr, syscall_rules.clone()); + } + } + rules.insert(nr, syscall_rules); } // Build rule map. @@ -228,8 +261,8 @@ fn install_network_seccomp_filter_on_current_thread( libc::AF_UNIX as u64, )?])?; - rules.insert(libc::SYS_socket, vec![unix_only_rule.clone()]); - rules.insert(libc::SYS_socketpair, vec![unix_only_rule]); + add_syscall_rules(&mut rules, libc::SYS_socket, vec![unix_only_rule.clone()]); + add_syscall_rules(&mut rules, libc::SYS_socketpair, vec![unix_only_rule]); } NetworkSeccompMode::ProxyRouted => { // In proxy-routed mode we allow IP sockets in the isolated @@ -265,8 +298,12 @@ fn install_network_seccomp_filter_on_current_thread( 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]); + add_syscall_rules(&mut rules, libc::SYS_socket, vec![deny_non_ip_socket]); + add_syscall_rules( + &mut rules, + libc::SYS_socketpair, + vec![deny_non_unix_socketpair], + ); } NetworkSeccompMode::VmSocketRestricted => { let deny_vsock = SeccompRule::new(vec![SeccompCondition::new( @@ -275,8 +312,8 @@ fn install_network_seccomp_filter_on_current_thread( SeccompCmpOp::Eq, libc::AF_VSOCK as u64, )?])?; - rules.insert(libc::SYS_socket, vec![deny_vsock.clone()]); - rules.insert(libc::SYS_socketpair, vec![deny_vsock]); + add_syscall_rules(&mut rules, libc::SYS_socket, vec![deny_vsock.clone()]); + add_syscall_rules(&mut rules, libc::SYS_socketpair, vec![deny_vsock]); } } diff --git a/codex-rs/linux-sandbox/src/seccomp_tests.rs b/codex-rs/linux-sandbox/src/seccomp_tests.rs index 8fde061f85..88f1039e17 100644 --- a/codex-rs/linux-sandbox/src/seccomp_tests.rs +++ b/codex-rs/linux-sandbox/src/seccomp_tests.rs @@ -1,4 +1,6 @@ -//! Verify network policies select the expected seccomp enforcement mode. +//! Verify network policies select and enforce the expected seccomp restrictions. + +#![cfg(test)] use super::NetworkSeccompMode; use super::network_seccomp_mode; @@ -75,3 +77,124 @@ fn full_network_without_managed_proxy_skips_network_seccomp_mode() { None ); } + +#[cfg(target_arch = "x86_64")] +fn syscall_result(nr: i64, arg0: i64) -> std::result::Result { + // Invalid remaining arguments keep the probes from accessing another process + // or opening a connection. Seccomp must reject denied calls before dispatch. + let result = unsafe { libc::syscall(nr, arg0, -1_i64, 0_i64, 0_i64, 0_i64, 0_i64) }; + if result == -1 { + Err(std::io::Error::last_os_error() + .raw_os_error() + .expect("syscall failure has an errno")) + } else { + Ok(result) + } +} + +#[cfg(target_arch = "x86_64")] +#[test_case::test_case(NetworkSeccompMode::Restricted; "restricted")] +#[test_case::test_case(NetworkSeccompMode::ProxyRouted; "proxy_routed")] +#[test_case::test_case(NetworkSeccompMode::VmSocketRestricted; "vm_socket_restricted")] +fn x32_restricted_syscalls_are_denied(mode: NetworkSeccompMode) { + std::thread::spawn(move || { + super::install_network_seccomp_filter_on_current_thread( + mode, /*managed_network*/ None, + ) + .expect("install seccomp filter on the disposable thread"); + + let mut denied = vec![ + (libc::SYS_io_uring_setup, 425), + (libc::SYS_io_uring_enter, 426), + (libc::SYS_io_uring_register, 427), + ]; + if mode != NetworkSeccompMode::VmSocketRestricted { + denied.extend([ + (libc::SYS_ptrace, 521), + (libc::SYS_process_vm_readv, 539), + (libc::SYS_process_vm_writev, 540), + ]); + } + if mode == NetworkSeccompMode::Restricted { + denied.extend([ + (libc::SYS_connect, 42), + (libc::SYS_accept, 43), + (libc::SYS_accept4, 288), + (libc::SYS_bind, 49), + (libc::SYS_listen, 50), + (libc::SYS_getpeername, 52), + (libc::SYS_getsockname, 51), + (libc::SYS_shutdown, 48), + (libc::SYS_sendto, 44), + (libc::SYS_recvmmsg, 537), + (libc::SYS_sendmmsg, 538), + (libc::SYS_setsockopt, 541), + (libc::SYS_getsockopt, 542), + ]); + } + for (native_nr, x32_nr) in denied { + for nr in [native_nr, native_nr | 0x4000_0000, x32_nr | 0x4000_0000] { + assert_eq!( + syscall_result(nr, /*arg0*/ -1), + Err(libc::EPERM), + "syscall {nr:#x}" + ); + } + if x32_nr != native_nr { + // These unflagged aliases were callable before Linux 5.4. + assert_eq!(syscall_result(x32_nr, /*arg0*/ -1), Err(libc::EPERM)); + } + } + }) + .join() + .expect("seccomp probe thread should succeed"); +} + +#[cfg(target_arch = "x86_64")] +#[test_case::test_case(NetworkSeccompMode::Restricted; "restricted")] +#[test_case::test_case(NetworkSeccompMode::ProxyRouted; "proxy_routed")] +#[test_case::test_case(NetworkSeccompMode::VmSocketRestricted; "vm_socket_restricted")] +fn x32_preserves_allowed_calls_and_socket_conditions(mode: NetworkSeccompMode) { + std::thread::spawn(move || { + let socket_domain = match mode { + NetworkSeccompMode::Restricted | NetworkSeccompMode::VmSocketRestricted => { + libc::AF_UNIX + } + NetworkSeccompMode::ProxyRouted => libc::AF_INET, + }; + let allowed = [ + (libc::SYS_getpid, 0), + (libc::SYS_socket, i64::from(socket_domain)), + (libc::SYS_socketpair, i64::from(libc::AF_UNIX)), + ]; + let probes = allowed + .into_iter() + .flat_map(|(nr, arg0)| [(nr, arg0), (nr | 0x4000_0000, arg0)]) + .collect::>(); + // On kernels without x32, permitted x32 calls must keep returning ENOSYS, + // not EPERM. On x32 kernels, getpid must still return the process ID. + let expected = probes + .iter() + .map(|&(nr, arg0)| syscall_result(nr, arg0)) + .collect::>(); + super::install_network_seccomp_filter_on_current_thread( + mode, /*managed_network*/ None, + ) + .expect("install seccomp filter on the disposable thread"); + let actual = probes + .iter() + .map(|&(nr, arg0)| syscall_result(nr, arg0)) + .collect::>(); + assert_eq!(actual, expected); + for nr in [libc::SYS_socket, libc::SYS_socketpair] { + for nr in [nr, nr | 0x4000_0000] { + assert_eq!( + syscall_result(nr, i64::from(libc::AF_VSOCK)), + Err(libc::EPERM) + ); + } + } + }) + .join() + .expect("seccomp probe thread should succeed"); +}