Skip to content

Close the ConPTY output pipe before the HPCON on cancellation - #35

Merged
tonyfettes merged 3 commits into
mainfrom
fix/win32-conpty-close-order
Oct 11, 2026
Merged

tonyfettes merged 3 commits into
mainfrom
fix/win32-conpty-close-order

Conversation

@tonyfettes

@tonyfettes tonyfettes commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

Raised by Codex on #34, and it predates that PR: the win32 cancellation cleanup calls ClosePseudoConsole while our end of the output pipe is still open and nobody is reading it (the pipe is only closed later, by the task group's defer).

Microsoft's documentation says the caller must either close the output pipe before ClosePseudoConsole or keep reading it until the call returns, and that before Windows 11 24H2 (build 26100) the call otherwise "may wait indefinitely". The call runs on the event-loop thread, so when it blocks nothing else runs: not the grace-period kill, not a timeout.

with_pty (#34) makes this path the ordinary one — it is taken whenever the callback returns while the child is still running.

Change

Three commits, pushed separately so each has its own CI run:

  1. CI + test, no fix. Adds a windows-2022 job (older than 24H2, so it still has the waiting ClosePseudoConsole), timeout-minutes: 10 (jobs take about a minute; a deadlock hangs the test binary instead of failing it), and a test whose child floods a pty that nobody reads and is then cancelled.
  2. The fix. The cancellation branch closes the output pipe first, then the HPCON. The group defer closing the pipe again is harmless: RawFdStream::close is idempotent. docs/invariants.md records the order and why.
  3. A second, pre-existing bug the new job exposed. When CreateProcessW fails, the cleanup closed both pipes and the HPCON and only then read GetLastError(). On windows-2022 the closes overwrote it, and the existing bad-exe test saw ERROR_ACCESS_DENIED. The error is now read first, as the unix side already does with errno.

CI evidence

commit windows-2022 other three jobs
1 — test, no fix (c43c489) Test step hung for 9 minutes, cancelled by timeout-minutes pass
2 — close-order fix (60119c1) flooding test passes; 12 of 13 pass, the bad-exe test fails with "Access is denied" pass
3 — read the error first (bad1f84) pass pass

windows-latest passes at every commit, including the one without the fix, so it cannot catch a regression here; that is what the windows-2022 job is for.

What is and is not verified

  • Nothing win32 here was run locally (macOS); the runs above are the evidence.
  • The hang at commit 1 is attributed to the flooding test because the fix is the only difference between commits 1 and 2; the cancelled job's log does not name the test.
  • Unchanged, and still exposed on pre-24H2 Windows: the normal exit path closes the HPCON after the child exits with the output pipe open, because the caller is still entitled to read up to EOF and EOF only arrives after ClosePseudoConsole. A caller that does not drain the pty there can hit the same wait. Fixing that needs ClosePseudoConsole off the event-loop thread and is out of scope here.

🤖 Generated with Claude Code

Before Windows 11 24H2, ClosePseudoConsole waits for the pseudoconsole to
exit, and the pseudoconsole blocks on a full output pipe. The win32
cancellation cleanup closes the HPCON while the output pipe is still open
and unread, so a child that keeps producing output can hang the teardown.

This commit only adds the means to see that:

- a windows-2022 job, which still has the waiting ClosePseudoConsole
- timeout-minutes, because the hang blocks the event-loop thread and
  shows up as a job that never ends rather than a failed assertion
- a test whose child floods a pty nobody reads, then cancels it

The fix follows in the next commit, so this one is expected to time out
on windows-2022.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-11T07:03:07.933844Z bad1f84 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

The win32 cancellation cleanup called ClosePseudoConsole while our end of
the output pipe was still open and unread. Before Windows 11 24H2 that
call waits for the pseudoconsole to exit, and the pseudoconsole blocks on
a full output pipe, so a child that keeps producing output could hang the
teardown on the event-loop thread, before the grace-period kill.

Close the output pipe first, as Microsoft's documentation requires. The
group defer closing it a second time is harmless: RawFdStream::close is
idempotent.

The normal exit path is unchanged: the caller may still read up to EOF
there, and EOF only arrives after ClosePseudoConsole.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@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: c43c48954a

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread pty_win32_test.mbt
Comment on lines +195 to +198
// writing to it. Before Windows 11 24H2 a `ClosePseudoConsole` issued in
// that state never returns, and it runs on the event-loop thread, so no
// timeout can fire either: the failure mode of this test is the CI job's
// `timeout-minutes`, not an assertion. The bound covers the 5000ms grace

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid landing a CI test that intentionally deadlocks

On the newly added windows-2022 CI leg, once this child fills the unread output pipe, cancellation still calls close_pseudo_console() in pty_win32.mbt before the group defer closes output_reader. As this comment notes, that blocks the event-loop thread on this Windows version, so the 15-second timeout cannot fire and the required test job instead fails at its 10-minute job timeout. Include the output-pipe-first cleanup change in this commit, or do not enable this known-failing test/runner yet.

Useful? React with 👍 / 👎.

When CreateProcessW fails, the cleanup closed both pipes and the HPCON and
only then read GetLastError(). On the windows-2022 runner the bad-exe test
then saw ERROR_ACCESS_DENIED instead of the CreateProcessW error; the
closes are free to overwrite the thread's last-error value.

Read it first, as the unix side already does with errno.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tonyfettes
tonyfettes merged commit d577689 into main Oct 11, 2026
4 checks passed
@tonyfettes
tonyfettes deleted the fix/win32-conpty-close-order branch October 11, 2026 07:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant