Compare commits

...

1 Commits

Author SHA1 Message Date
Eric Traut
544af30ef6 linux-sandbox: allow sendto(NULL, 0) for asyncio self-pipe wakeups
Fixes a hang in async SQLite / asyncio under no-network sandboxing by
allowing sendto only when dest_addr == NULL and addrlen == 0. This
permits send()/self-pipe wakeups (e.g., call_soon_threadsafe) while
keeping connect/bind/listen/etc. blocked.

Risk assessment:
- Main risk: a sandboxed process could write to a pre-connected network
  socket inherited via FD leakage (since send() maps to sendto(NULL, 0)).
- Mitigation: the sandbox still blocks creating/connecting sockets; risk
  depends on connected socket FD inheritance.

Addresses #9906
2026-01-28 12:48:11 -08:00
2 changed files with 50 additions and 1 deletions

View File

@@ -115,7 +115,6 @@ fn install_network_seccomp_filter_on_current_thread() -> std::result::Result<(),
deny_syscall(libc::SYS_getpeername);
deny_syscall(libc::SYS_getsockname);
deny_syscall(libc::SYS_shutdown);
deny_syscall(libc::SYS_sendto);
deny_syscall(libc::SYS_sendmmsg);
// NOTE: allowing recvfrom allows some tools like: `cargo clippy` to run
// with their socketpair + child processes for sub-proc management
@@ -133,6 +132,25 @@ fn install_network_seccomp_filter_on_current_thread() -> std::result::Result<(),
libc::AF_UNIX as u64,
)?])?;
// Allow sendto only for send()/self-pipe wakeups where dest_addr == NULL
// and addrlen == 0. Deny any other use.
let sendto_dest_addr_rule = SeccompRule::new(vec![SeccompCondition::new(
4, // fifth argument (dest_addr)
SeccompCmpArgLen::Qword,
SeccompCmpOp::Ne,
0,
)?])?;
let sendto_addrlen_rule = SeccompRule::new(vec![SeccompCondition::new(
5, // sixth argument (addrlen)
SeccompCmpArgLen::Dword,
SeccompCmpOp::Ne,
0,
)?])?;
rules.insert(
libc::SYS_sendto,
vec![sendto_dest_addr_rule, sendto_addrlen_rule],
);
rules.insert(libc::SYS_socket, vec![unix_only_rule.clone()]);
rules.insert(libc::SYS_socketpair, vec![unix_only_rule]); // always deny (Unix can use socketpair but fine, keep open?)

View File

@@ -13,6 +13,7 @@ use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use std::collections::HashMap;
use std::path::PathBuf;
use std::process::Command;
use tempfile::NamedTempFile;
// At least on GitHub CI, the arm64 tests appear to need longer timeouts.
@@ -163,6 +164,36 @@ async fn test_timeout() {
run_cmd(&["sleep", "2"], &[], 50).await;
}
#[tokio::test]
async fn sandbox_allows_sendto_self_pipe_wakeup() {
let python_available = Command::new("python3")
.arg("--version")
.status()
.map(|status| status.success())
.unwrap_or(false);
if !python_available {
return;
}
let script = r#"
import socket
a, b = socket.socketpair()
try:
a.send(b"x")
finally:
a.close()
b.close()
print("ok")
"#;
run_cmd(
&["bash", "-lc", &format!("python3 - <<'PY'\n{script}\nPY")],
&[],
SHORT_TIMEOUT_MS,
)
.await;
}
/// Helper that runs `cmd` under the Linux sandbox and asserts that the command
/// does NOT succeed (i.e. returns a nonzero exit code) **unless** the binary
/// is missing in which case we silently treat it as an accepted skip so the