Skip to content

windows: keep Close's reply channel when an error cannot be delivered - #9

Merged
tomassrnka merged 3 commits into
e2b-mainfrom
fix/windows-senderror-keeps-close-reply
Oct 28, 2026
Merged

tomassrnka merged 3 commits into
e2b-mainfrom
fix/windows-senderror-keeps-close-reply

Conversation

@tomassrnka

@tomassrnka tomassrnka commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

What

On Windows, Close can block forever once the error consumer has gone away and the backend then reports an error.

Close puts a reply channel on done, wakes the reader, and waits on that channel. sendError takes the channel from done when the watcher is closing and drops it, where sendEvent puts it back. Its callers ignore the false return, so the reader handles the wakeup without seeing a shutdown request. It then skips the cleanup of its watches and of the completion port, and Close never returns.

This happens whenever nobody reads Errors any more and an error arrives during shutdown: an overflow (ErrEventOverflow), a failed CancelIo or CloseHandle in remWatch, or a GetQueuedCompletionStatus failure. Remove reaches the same sendError calls.

Fix

sendError puts the reply channel back on done, exactly as sendEvent does, and still returns false. The reader takes the channel on its next wakeup and runs its normal shutdown.

Tests (Windows)

  • TestSendErrorKeepsCloseReply: with a reply queued and nobody reading Errors, sendError returns false and leaves the same channel on done.
  • TestCloseAfterUnreadError: a real watcher. Close's reply is queued, an error meets an unread Errors channel, and the reader is woken. The reply must arrive within 10 s.

The first commit adds only the tests, and its CI run on windows-latest is the failing baseline. The second commit is the fix.

On Linux, the existing TestAdd/permission_denied fails when the tests run as root, before and after this change. It is unrelated.

CI

  • windows-latest (Go 1.19 and 1.24): at the test-only head c6f96512c9 (run 39993141422) only the two new tests fail. At the fix head 73a141ee4f everything passes.
  • Linux, macOS, OpenBSD, staticcheck: pass.
  • dragonflybsd and illumos: fail on both heads, so they fail with the tests alone, before the fix. The change touches only backend_windows*.go.
  • netbsd: passed at c6f96512c9 and was cancelled at 73a141ee4f.

After merge, please tag v0.0.2. belt's envd pins this commit's pseudo-version until then.

Close queues a reply channel on done and waits for the reader to send on it after its cleanup. sendError takes that channel when the watcher is closing and drops it, so the reader never sees the shutdown request and Close blocks forever. These tests pin the channel handoff and the end-to-end Close; they fail before the fix.
sendError received the reply channel that Close queued on done and discarded it, unlike sendEvent, which puts it back. When nobody reads Errors any more and the backend then reports an error (an overflow, a failed CancelIo or CloseHandle), the reader handled the wakeup without the shutdown request, skipped the cleanup of its watches and the completion port, and Close waited on the reply forever. sendError now puts the channel back, as sendEvent does; TestSendErrorKeepsCloseReply and TestCloseAfterUnreadError fail without this change on windows-latest and pass with it.
@cla-bot cla-bot Bot added the cla-signed label Oct 23, 2026
@cursor

cursor Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes Windows watcher shutdown and error delivery on a one-slot coordination channel; wrong behavior could still hang Close or mishandle concurrent close.

Overview
On Windows, Close could block forever if shutdown was in progress and sendError could not write to Errors because nothing was reading it. sendError took Close's reply off done and discarded it unlike sendEvent, so the reader never saw the shutdown reply and never finished cleanup.

sendError now re-queues that reply on done when delivery fails. Close sets closed and returns early for duplicate calls under one lock so concurrent Close does not stack multiple replies on the single-slot done channel. Windows tests cover undelivered errors during shutdown and concurrent Close.

Reviewed by Cursor Bugbot for commit 17a6ca9. Bugbot is set up for automated code reviews on this repo. Configure here.

@tomassrnka
tomassrnka marked this pull request as ready for review October 23, 2026 17:27
Close read closed under the lock, released it, and set it under a second acquisition, so two concurrent calls could both proceed and queue two reply channels. done holds one, so the second Close blocked sending, and with an undeliverable error between them the reader, now keeping the first reply, could not drain the second: both calls and the reader hung. The check and the set now happen under one lock, so a second Close returns at once. TestConcurrentClose runs eight concurrent Close calls under a timeout and checks that no reply is left on done; the interleaving is a race, so it guards rather than reproduces.
@tomassrnka
tomassrnka merged commit b260f7c into e2b-main Oct 28, 2026
14 of 19 checks passed
@arkamar
arkamar deleted the fix/windows-senderror-keeps-close-reply branch October 29, 2026 08:26
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