Skip to content

fix(codex): clean up subprocess on cancellation#3915

Open
shiwenbin1617 wants to merge 5 commits into
openai:mainfrom
shiwenbin1617:fix/codex-exec-cancellation-cleanup
Open

fix(codex): clean up subprocess on cancellation#3915
shiwenbin1617 wants to merge 5 commits into
openai:mainfrom
shiwenbin1617:fix/codex-exec-cancellation-cleanup

Conversation

@shiwenbin1617

Copy link
Copy Markdown

Summary

This pull request fixes a subprocess cleanup deadlock in the experimental Codex executor.

When the async output stream was cancelled or closed early, cleanup waited for the stderr drain task before terminating the subprocess. Since stderr remained open while the process was running, cancellation could hang indefinitely and leave the child process unreaped.

The cleanup path now:

  • covers stdin writes, stdout reads, cancellation, and exceptional exits with a common try/finally;
  • terminates and waits for a live subprocess before stopping the stderr drain task;
  • cancels and awaits the optional signal watcher; and
  • preserves the original asyncio.CancelledError semantics.

A deterministic regression test verifies that cancellation completes promptly, the subprocess is killed and reaped, and the stderr task is cancelled.

Test plan

  • bash .agents/skills/code-change-verification/scripts/run.sh
  • pytest -q tests/extensions/experiemental/codex/test_codex_exec_thread.py -k codex_exec_run
  • A local subprocess reproduction confirmed cancellation completes and the child exits with -9.

Issue number

N/A

Checks

  • I've added new tests, if relevant
  • I've run .agents/skills/code-change-verification/scripts/run.sh
  • I've confirmed all verification steps pass
  • If using Codex, I've run /review before submitting this PR

@seratch seratch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the fix. The original cancellation deadlock is real, but the current cleanup can still hang when stdout has unread buffered data: after process.kill(), await process.wait() blocks if the stdout transport has paused due to backpressure, even though the child has already exited.

Please drain the remaining stdout in bounded chunks before awaiting process.wait(), and add a regression test using a real subprocess that fills stdout beyond the stream-reader limit before the stream is closed. The existing fake-process test cannot reproduce this pipe-level deadlock. I confirmed that draining stdout first resolves the issue while preserving the existing cancellation and stderr-cleanup behavior.

@shiwenbin1617
shiwenbin1617 force-pushed the fix/codex-exec-cancellation-cleanup branch from 53c8fb6 to 8e7af59 Compare July 23, 2026 05:46
@shiwenbin1617

Copy link
Copy Markdown
Author

Thanks for catching this — you were right that waiting for the process before draining a paused stdout transport can still deadlock.

I've updated the cleanup path to cancel any in-flight timeout reader, kill the subprocess, drain the remaining stdout in 64 KiB chunks, and only then await process.wait(). I also added a real-subprocess regression that writes beyond the StreamReader limit, confirms the transport is paused, closes the async generator, and verifies that cleanup completes and the child is reaped.

The existing cancellation and stderr-cleanup coverage remains in place, and the full verification stack passes.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8e7af5931a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

process.kill()
with contextlib.suppress(ProcessLookupError):
process.kill()
await _drain_remaining_stdout()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bound stdout draining during cleanup

If the wrapped CLI spawns a helper process that inherits stdout and outlives the direct child, cancellation or early aclose() kills only the direct subprocess, but this unbounded drain waits for EOF from every remaining holder of the pipe. In that scenario cleanup hangs until the descendant exits, which preserves the cancellation deadlock this change is trying to fix; please bound the drain or ensure the whole process tree/stdout fd is closed before awaiting it.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, good catch. Fixed in da45e04. Cleanup now bounds the combined stdout drain and process wait; if an outliving descendant keeps a pipe open past the deadline, it closes the subprocess transport and performs a second bounded wait. I also added a real subprocess regression where the direct child fills stdout while a helper inherits stdout and outlives it.

@shiwenbin1617

Copy link
Copy Markdown
Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Security review completed. No security issues were found in this pull request.

Reviewed commit: da45e04d40

View security finding report

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Swish!

Reviewed commit: da45e04d40

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@shiwenbin1617

Copy link
Copy Markdown
Author

@seratch I addressed the requested stdout backpressure case and the follow-up bot finding in da45e04. Cleanup is now bounded even when an outliving descendant keeps stdout open, with a real-subprocess regression covering that case. Codex review and security review on the latest head are both clean. When you have a chance, could you please take another look? Thanks!

@seratch

seratch commented Jul 23, 2026

Copy link
Copy Markdown
Member

Thanks for addressing the stdout backpressure deadlock. A deeper lifecycle review found two remaining merge blockers.
First, when a descendant holds an inherited pipe, cleanup returns after closing the transport while the descendant remains alive and can continue executing after cancellation. Please ensure cancellation owns and terminates the entire Codex subprocess tree, and add a real-subprocess regression test asserting that the descendant is no longer running.
Second, failures from stdout draining or process.wait() currently replace the original CancelledError or execution exception, and a second cancellation can skip stderr-task cleanup. Please preserve the original exception and make resource cleanup resilient to repeated cancellation, with focused regression coverage for both cases.

@shiwenbin1617

Copy link
Copy Markdown
Author

Addressed both remaining lifecycle blockers in c004807:

  • Codex subprocesses now own a process tree and terminate descendants during cancellation/stream closure on POSIX and Windows. The real-subprocess regression now asserts the inherited-pipe descendant has exited.
  • Cleanup now runs as a shielded task, preserves the original cancellation or execution exception when drain/wait fails, and completes stderr cleanup across repeated cancellation.
  • The Windows coverage avoids the SelectorEventLoop asyncio-subprocess limitation while still exercising real process-tree termination.

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Security review completed. No security issues were found in this pull request.

Reviewed commit: da45e04d40

View security finding report

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: da45e04d40

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

if process.returncode is None:
process.kill()
with contextlib.suppress(ProcessLookupError):
process.kill()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Terminate the full subprocess tree on cleanup

When cancellation or aclose() happens after Codex has spawned a helper that inherited stdout/stderr, this only kills the immediate Codex process. Because the subprocess is not launched in an owned POSIX process group or Windows job/process group, _cleanup_process can return after closing the transport while the descendant continues running; the new descendant regression even has to kill helper_pid in its finally. Please make the executor own and terminate the process tree before returning from cleanup.

Useful? React with 👍 / 👎.

process.kill()
with contextlib.suppress(ProcessLookupError):
process.kill()
await _cleanup_process()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Shield cleanup from repeated cancellation

If a caller cancels the stream and then applies its own timeout, or shutdown sends a second cancellation while this await is in progress, that second CancelledError interrupts _cleanup_process and skips the later stderr-task cleanup. In that repeated-cancellation path the child can remain unreaped or the drain task can survive, so cleanup should run in a shielded task and then re-raise the original cancellation or execution error.

Useful? React with 👍 / 👎.

@shiwenbin1617

Copy link
Copy Markdown
Author

@codex review

Please review commit c004807 specifically: shiwenbin1617@c004807

The fork branch already points to this commit, while the PR head synchronization is delayed by the ongoing GitHub Pull Requests/Webhooks incident.

@chatgpt-codex-connector

Copy link
Copy Markdown

Security review completed. No security issues were found in this pull request.

Reviewed commit: da45e04d40

View security finding report

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

Reviewed commit: da45e04d40

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@shiwenbin1617

Copy link
Copy Markdown
Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Security review completed. No security issues were found in this pull request.

Reviewed commit: c0048076d5

View security finding report

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c0048076d5

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

assert process.returncode is not None
for _ in range(500):
try:
os.kill(helper_pid, 0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat defunct descendants as terminated

In Linux containers where PID 1 does not reap orphaned grandchildren, this os.kill(pid, 0) probe continues to succeed after the SIGKILL because the helper remains as a defunct zombie. The new test then spins for the full 5 seconds and fails even though no runnable descendant survived; either make the assertion tolerate zombie state or structure the child process to reap the helper before exiting.

Useful? React with 👍 / 👎.

@shiwenbin1617

Copy link
Copy Markdown
Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Security review completed. No security issues were found in this pull request.

Reviewed commit: 378c14f764

View security finding report

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 378c14f764

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +246 to +248
if original_error is not None or process.returncode is None:
try:
await _terminate_process_tree(process)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Kill descendants after a clean parent exit

When the direct Codex CLI exits successfully after spawning a background helper that inherited stderr, process.returncode is already set here, so this branch skips _terminate_process_tree; the cleanup then cancels the stderr drain and run() returns while the helper keeps running. Since this change creates an owned POSIX process group for descendants, the cleanup path should also terminate that group before returning when descendants can still be holding subprocess pipes open.

Useful? React with 👍 / 👎.

@shiwenbin1617

Copy link
Copy Markdown
Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Security review completed. No security issues were found in this pull request.

Reviewed commit: b6bd302799

View security finding report

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Delightful!

Reviewed commit: b6bd302799

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@shiwenbin1617

Copy link
Copy Markdown
Author

@seratch Thank you for the careful review and for pointing out the deeper lifecycle cases. Thank you as well to Codex for the follow-up checks. I’m sorry for the extra review notifications earlier—the GitHub Pull Requests/Webhooks incident caused two reviews to run against a stale head.

I believe the remaining blockers are now addressed on b6bd302: cleanup owns and terminates descendants across cancellation, errors, repeated cancellation, and clean parent exit, with focused real-subprocess coverage. Codex code and security reviews on the current head are clean.

When you have time, could you please take another look? I appreciate your patience and detailed guidance.

@shiwenbin1617
shiwenbin1617 requested a review from seratch July 23, 2026 12:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants