Python asyncio subprocess wait can timeout inside Linux bwrap sandbox after child exits with returncode=0

Open 💬 1 comment Opened Apr 28, 2026 by SUNGBEOMCHO1

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.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗