Summary
The restricted Linux network seccomp filter unconditionally denies sendto(2). On Linux, send(2) is implemented as sendto(..., dest_addr = NULL), so this also breaks writes to already-connected Unix-domain sockets.
One visible consequence is that Python asyncio cross-thread wakeups stop working inside the default Codex workspace sandbox. loop.call_soon_threadsafe() writes to asyncio's AF_UNIX wakeup socket; the write receives EPERM, asyncio suppresses that OSError, and the event loop remains asleep.
Reproduction
On current main (315195492c), build Codex and run:
./target/debug/codex sandbox -P :workspace -C /tmp /usr/bin/python3 -c 'import asyncio, threading; loop=asyncio.new_event_loop(); threading.Thread(target=loop.run_forever, daemon=True).start(); print(asyncio.run_coroutine_threadsafe(asyncio.sleep(0, result=42), loop).result(timeout=2))'
Expected:
Actual:
This also causes Daft's native runner to hang on a simple collect() operation.
Isolation
- The same program succeeds outside the Codex sandbox.
- It succeeds with the same bubblewrap filesystem isolation and a full-network Codex profile.
- It succeeds with plain bubblewrap plus
--unshare-net.
- It fails only when Codex's restricted network seccomp filter is installed.
Root cause and possible fix
In codex-rs/linux-sandbox/src/landlock.rs, restricted mode unconditionally denies SYS_sendto.
A narrowly scoped fix is to deny sendto only when its fifth argument (dest_addr) is non-null. This permits send() on an already-connected socket, including AF_UNIX socketpairs used for local IPC, while continuing to deny sends to explicit destinations such as unconnected Unix datagram socket paths.
I have a tested patch with regression coverage for both sides of this boundary:
- connected AF_UNIX socket
send() succeeds;
sendto() with an explicit destination still returns EPERM.
The codex-linux-sandbox test suite passes (120/120), and both the minimal asyncio reproducer and the original Daft collect() scenario pass with the patched binary.
Summary
The restricted Linux network seccomp filter unconditionally denies
sendto(2). On Linux,send(2)is implemented assendto(..., dest_addr = NULL), so this also breaks writes to already-connected Unix-domain sockets.One visible consequence is that Python asyncio cross-thread wakeups stop working inside the default Codex workspace sandbox.
loop.call_soon_threadsafe()writes to asyncio's AF_UNIX wakeup socket; the write receivesEPERM, asyncio suppresses thatOSError, and the event loop remains asleep.Reproduction
On current
main(315195492c), build Codex and run:./target/debug/codex sandbox -P :workspace -C /tmp /usr/bin/python3 -c 'import asyncio, threading; loop=asyncio.new_event_loop(); threading.Thread(target=loop.run_forever, daemon=True).start(); print(asyncio.run_coroutine_threadsafe(asyncio.sleep(0, result=42), loop).result(timeout=2))'Expected:
Actual:
This also causes Daft's native runner to hang on a simple
collect()operation.Isolation
--unshare-net.Root cause and possible fix
In
codex-rs/linux-sandbox/src/landlock.rs, restricted mode unconditionally deniesSYS_sendto.A narrowly scoped fix is to deny
sendtoonly when its fifth argument (dest_addr) is non-null. This permitssend()on an already-connected socket, including AF_UNIX socketpairs used for local IPC, while continuing to deny sends to explicit destinations such as unconnected Unix datagram socket paths.I have a tested patch with regression coverage for both sides of this boundary:
send()succeeds;sendto()with an explicit destination still returnsEPERM.The
codex-linux-sandboxtest suite passes (120/120), and both the minimal asyncio reproducer and the original Daftcollect()scenario pass with the patched binary.