Python asyncio subprocess wait can timeout inside Linux bwrap sandbox after child exits with returncode=0
What version of Codex CLI is running?
codex-cli 0.125.0
What subscription do you have?
N/A for this local CLI sandbox repro.
Which model were you using?
N/A. This reproduces through shell/tool execution and does not depend on the model.
What platform is your computer?
Linux 5.15.0-139-generic x86_64 x86_64
Additional local details:
bubblewrap 0.4.0/usr/bin/python3:Python 3.8.10
What terminal emulator and version are you using (if applicable)?
Codex CLI running from a zsh/tmux remote shell. The failure is observed inside Codex's Linux sandboxed shell execution path.
What issue are you seeing?
Inside the Codex Linux sandbox, a minimal Python asyncio.subprocess program can time out waiting for a child process even though the child has already exited successfully.
The minimal repro starts cat, sends x\n to stdin, and waits for communicate() with a 1 second timeout. Inside the Codex sandbox it intermittently raises TimeoutError, but the child process already has returncode=0.
This appears to be specific to Codex's sandboxed execution environment:
- Running through Codex sandbox with the default asyncio child watcher: fails intermittently.
- Running the same script outside the Codex sandbox: passes.
- Running the same script in a normal user terminal: passes.
- Running inside the Codex sandbox after forcing
asyncio.SafeChildWatcher(): passes.
In the failing Codex sandbox process, the Python process appears as PID 2 with PPID 1, consistent with the bubblewrap PID namespace. /proc/1/cmdline shows Codex launching bwrap with PID namespace isolation (--unshare-pid) and seccomp enabled (--apply-seccomp-then-exec).
What steps can reproduce the bug?
Create this repro script:
import asyncio
import os
import sys
async def main() -> None:
print("pid", os.getpid(), "ppid", os.getppid())
print(sys.executable)
print(sys.version)
fails = 0
for i in range(20):
proc = await asyncio.create_subprocess_exec(
"cat",
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
try:
out, _err = await asyncio.wait_for(proc.communicate(b"x\n"), timeout=1)
if proc.returncode != 0 or out != b"x\n":
fails += 1
print("fail", i, f"returncode={proc.returncode}", f"stdout={out!r}")
except Exception as exc:
fails += 1
print("fail", i, type(exc).__name__, f"returncode={proc.returncode}")
try:
proc.kill()
await asyncio.wait_for(proc.wait(), timeout=1)
except Exception:
pass
print(f"fails={fails}/20")
asyncio.run(main())
Run it from a Codex shell/tool command in the sandboxed path:
env python3 /tmp/asyncio_subprocess_probe_sandbox.py
Observed output from Codex sandbox:
pid 2 ppid 1
/usr/bin/python3
3.8.10 (default, Mar 18 2025, 20:04:55)
[GCC 9.4.0]
fail 0 TimeoutError returncode=0
fail 1 TimeoutError returncode=0
fail 5 TimeoutError returncode=0
fail 6 TimeoutError returncode=0
fail 7 TimeoutError returncode=0
fail 9 TimeoutError returncode=0
fail 10 TimeoutError returncode=0
fail 11 TimeoutError returncode=0
fail 12 TimeoutError returncode=0
fail 13 TimeoutError returncode=0
fail 14 TimeoutError returncode=0
fail 16 TimeoutError returncode=0
fail 17 TimeoutError returncode=0
fail 18 TimeoutError returncode=0
fail 19 TimeoutError returncode=0
fails=15/20
Run the same script outside the Codex sandbox:
python3 /tmp/asyncio_subprocess_probe_sandbox.py
Observed output outside sandbox:
pid 2225708 ppid 1848639
/usr/bin/python3
3.8.10 (default, Mar 18 2025, 20:04:55)
[GCC 9.4.0]
fails=0/20
Run inside the Codex sandbox but force SafeChildWatcher:
env python3 -c 'import asyncio, runpy; asyncio.set_child_watcher(asyncio.SafeChildWatcher()); runpy.run_path("/tmp/asyncio_subprocess_probe_sandbox.py", run_name="__main__")'
Observed output:
pid 2 ppid 1
/usr/bin/python3
3.8.10 (default, Mar 18 2025, 20:04:55)
[GCC 9.4.0]
fails=0/20
What is the expected behavior?
A subprocess that has already exited with returncode=0 should not cause asyncio.subprocess.Process.communicate() / wait() to time out in Codex's sandboxed execution environment.
The repro should consistently print fails=0/20, as it does outside the sandbox and inside the sandbox when SafeChildWatcher is forced.
Additional information
This may be related to sandbox-only timeout reports such as #3557, but this issue has a narrower repro: a simple cat subprocess exits successfully, yet asyncio still times out in the Codex sandbox.
The SafeChildWatcher A/B result suggests a Linux PID namespace / asyncio child watcher interaction in the sandboxed subprocess path rather than a workload-specific timeout.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗