ProcessHandle::terminate aborts its only child waiter

Open 💬 1 comment Opened Jul 10, 2026 by morluto

What version of Codex CLI is running?

\codex-cli 0.144.1\

What subscription do you have?

Pro

Which model were you using?

Not applicable; this is an app-server process-lifecycle bug.

What platform is your computer?

\Darwin 25.3.0 arm64 arm\

What terminal emulator and version are you using?

RepoPrompt CE invoking \codex app-server\; no terminal emulator is involved.

Codex doctor report

\codex doctor --json\ produced no output with \codex-cli 0.144.1\.

What issue are you seeing?

Terminating or dropping a PTY/pipe \ProcessHandle\ can kill its child and then abort the only task responsible for waiting on that child.

The exited process remains a zombie while the Codex app-server parent stays alive. During a local app-server run, repeated exec/tool calls accumulated dozens of defunct children beneath the active app-server processes.

The source establishes a direct ownership path:

  1. \ProcessHandle::terminate()\ calls \request_terminate()\, which kills the child.
  2. It aborts the reader and writer helpers.
  3. It also aborts \wait_handle\.
  4. The wait task owns the child and is the only code calling \child.wait()\.

\Drop for ProcessHandle\ calls the same \terminate()\ method.

The behavior is present in the \rust-v0.144.1\ source and the same ownership pattern is present on current \main\.

What steps can reproduce the bug?

  1. Start \codex app-server\.
  2. Run repeated exec/tool calls that spawn short-lived child processes.
  3. Allow the associated \ProcessHandle\ values to be terminated or dropped while keeping the app-server alive.
  4. Inspect children of the app-server:

\\\sh
ps -axo pid,ppid,state,command
\
\\

  1. Observe that completed children remain in state \Z\ and that their count increases with additional exec calls.

A repository-level regression can reproduce the ownership bug more directly:

  1. Spawn a pipe or PTY child through \codex-rs/utils/pty\.
  2. Call \ProcessHandle::terminate()\ or drop the handle before natural exit.
  3. Wait for the child to exit.
  4. Verify that it is reaped rather than remaining a zombie.

The relevant implementation is \codex-rs/utils/pty/src/process.rs\.

What is the expected behavior?

Terminating or dropping a process handle must not abandon child reaping.

Reader and writer helpers may be cancelled, but the child waiter must be allowed to finish or be replaced by another owner that performs \wait\.

Additional information

Suggested regression coverage:

  • explicit termination of a pipe child
  • dropping a pipe handle before child exit
  • explicit termination of a PTY child
  • dropping a PTY handle before child exit
  • repeated short exec calls leaving no zombie children

This may be related to #12491, but the concrete path here is \utils/pty::ProcessHandle\.

PR #19753 addresses stdio MCP-server shutdown rather than this PTY/pipe waiter-abort path.

View original on GitHub ↗

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