fix(network-proxy): harden linux proxy bridge helpers (#20001)

## Why
The Linux managed-proxy bridge helpers are long-lived child processes in
the sandbox networking path. Before this change they stayed dumpable and
the network seccomp profile did not block cross-process memory syscalls,
so another same-user process could potentially inspect or modify bridge
memory instead of interacting only through the intended proxy interface.

## What changed
- reuse the shared `codex-process-hardening` helper to mark bridge
helper children non-dumpable before they begin serving
- deny `process_vm_readv` and `process_vm_writev` in the existing
network seccomp filter

## Security impact
Bridge helpers are less exposed to same-user cross-process inspection or
memory writes, which reduces the chance that sandboxed code can
interfere with proxy support processes outside the intended IPC path.

## Verification
- `cargo test -p codex-process-hardening`
- `cargo test -p codex-linux-sandbox`
- attempted `cargo check -p codex-linux-sandbox --target
x86_64-unknown-linux-gnu`; blocked on missing `x86_64-linux-gnu-gcc` on
this macOS host

---------

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
viyatb-oai
2026-04-28 11:52:50 -07:00
committed by GitHub
Unverified
parent de2ccf9473
commit 3377afd84a
5 changed files with 22 additions and 2 deletions
+1
View File
@@ -2816,6 +2816,7 @@ dependencies = [
"cc",
"clap",
"codex-core",
"codex-process-hardening",
"codex-protocol",
"codex-sandboxing",
"codex-utils-absolute-path",
+1
View File
@@ -17,6 +17,7 @@ workspace = true
[target.'cfg(target_os = "linux")'.dependencies]
clap = { workspace = true, features = ["derive"] }
codex-process-hardening = { workspace = true }
codex-protocol = { workspace = true }
codex-sandboxing = { workspace = true }
codex-utils-absolute-path = { workspace = true }
+2
View File
@@ -176,6 +176,8 @@ fn install_network_seccomp_filter_on_current_thread(
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);
+7 -2
View File
@@ -450,7 +450,7 @@ fn spawn_host_bridge(endpoint: SocketAddr, uds_path: &Path) -> io::Result<libc::
}
fn run_host_bridge(endpoint: SocketAddr, uds_path: &Path, ready_fd: libc::c_int) -> io::Result<()> {
set_parent_death_signal()?;
harden_bridge_process()?;
if uds_path.exists() {
std::fs::remove_file(uds_path)?;
}
@@ -501,7 +501,7 @@ fn spawn_local_bridge(uds_path: &Path) -> io::Result<u16> {
}
fn run_local_bridge(uds_path: &Path, ready_fd: libc::c_int) -> io::Result<()> {
set_parent_death_signal()?;
harden_bridge_process()?;
let listener = bind_local_loopback_listener()?;
let port = listener.local_addr()?.port();
@@ -614,6 +614,11 @@ fn set_parent_death_signal() -> io::Result<()> {
}
}
fn harden_bridge_process() -> io::Result<()> {
set_parent_death_signal()?;
codex_process_hardening::disable_process_dumping()
}
fn proxy_bidirectional(mut tcp_stream: TcpStream, mut unix_stream: UnixStream) -> io::Result<()> {
let mut tcp_reader = tcp_stream.try_clone()?;
let mut unix_writer = unix_stream.try_clone()?;
+11
View File
@@ -61,6 +61,17 @@ pub(crate) fn pre_main_hardening_linux() {
remove_env_vars_with_prefix(b"LD_");
}
/// Mark the current Linux process non-dumpable so same-user processes cannot attach with ptrace.
#[cfg(target_os = "linux")]
pub fn disable_process_dumping() -> std::io::Result<()> {
let ret_code = unsafe { libc::prctl(libc::PR_SET_DUMPABLE, 0, 0, 0, 0) };
if ret_code == 0 {
Ok(())
} else {
Err(std::io::Error::last_os_error())
}
}
#[cfg(any(target_os = "freebsd", target_os = "openbsd"))]
pub(crate) fn pre_main_hardening_bsd() {
// FreeBSD/OpenBSD: set RLIMIT_CORE to 0 and clear LD_* env vars