mirror of
https://github.com/openai/codex.git
synced 2026-11-13 21:30:47 +00:00
Enforce Linux seccomp restrictions for x32 syscalls (#61206)
## Why The x32 ABI shares the x86-64 audit architecture, so the architecture check alone does not enforce syscall restrictions for x32 callers. ## What changed Apply existing syscall denials and socket-family conditions to x32 syscall numbers on x86-64, including separate x32 numbers and legacy aliases supported before Linux 5.4. ## Testing Add probes across all three network seccomp modes to verify that restricted native and x32 calls return `EPERM`, allowed calls preserve their behavior, and socket-family restrictions apply to both ABIs. GitOrigin-RevId: fa38138d6b6c7b26dfafd6c931880ee1265a1b94
This commit is contained in:
committed by
copyberry
parent
caf0d30005
commit
1bda886113
@@ -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<i64, Vec<SeccompRule>>, 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<i64, Vec<SeccompRule>>,
|
||||
nr: i64,
|
||||
syscall_rules: Vec<SeccompRule>,
|
||||
) {
|
||||
#[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]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<i64, i32> {
|
||||
// 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::<Vec<_>>();
|
||||
// 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::<Vec<_>>();
|
||||
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::<Vec<_>>();
|
||||
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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user