-
Notifications
You must be signed in to change notification settings - Fork 5k
usockets (Windows): keep a closed socket allocated until the dispatch that closed it returns #39910
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from 4 commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
4ce07d3
usockets (Windows): keep a closed socket allocated until the dispatch…
dylan-conway acac7a3
usockets (Windows): do not uv_close a poll handle from inside its own…
dylan-conway 6137126
Merge remote-tracking branch 'origin/main' into claude/deinitializati…
dylan-conway aaf698c
test: await the churn connects in the reentrant-close fixture, drain …
dylan-conway dd7070d
Merge remote-tracking branch 'origin/main' into claude/deinitializati…
dylan-conway 137e969
Merge remote-tracking branch 'origin/main' into claude/deinitializati…
robobun File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔴 Deferring
uv_closefromus_poll_stoptous_poll_freereorders it to run after the caller'sbsd_close_socket(socket.c:201-202/304-313, context.c:484-485, udp.c:118-119), souv__poll_close's AFD cancel ioctl now targets a closed socket handle. UnderProcessStrictHandleCheckPolicy(always on inside AppContainer) that terminates the process with0xC0000008 STATUS_INVALID_HANDLE— robobun's dual-build comparison reproduced this 3/3 on this build with the extendedappcontainer.test.tsand a plain strict-handle-checking script, both passing on main and on #39643 — and without strict checking the cancel can land on a reused handle. The fix is to keep the fd open untiluv_closeis issued (as #39643 does viaus_internal_poll_close_fd+ aclose_fdbit).Extended reasoning...
What the bug is
Before this PR,
us_poll_stop()calleduv_close()on theuv_poll_tsynchronously; only after that did the caller close the OS socket. This PR moves theuv_close()call tous_poll_free()(libuv.c:164-177), but every close path still callsbsd_close_socket()immediately afterus_poll_stop()returns:us_internal_socket_close_raw— socket.c:304 → 313us_connecting_socket_close— socket.c:201 → 202us_listen_socket_close— context.c:484 → 485us_udp_socket_close— udp.c:118 → 119us_poll_free()runs later, fromus_internal_free_closed_socketsincheck_cb→us_internal_loop_post(loop.c:422-423, gated ontick_depth <= 1). Souv_close— and theuv__poll_closeit invokes — now runs after the socket handle has been closed.The specific code path that triggers it
libuv's
uv__poll_close(win/poll.c) checkssubmitted_events_1 | submitted_events_2; if nonzero, it issues anIOCTL_AFD_POLLcancel viauv__msafd_poll(handle->socket, ...)→NtDeviceIoControlFile((HANDLE)handle->socket, ...), using the socket handle as the ioctl's file-handle argument.uv_poll_stop()only setshandle->events = 0; it does not clearsubmitted_events_*.For a socket closed from inside its own
data()callback — the exact scenario this PR targets, and what its own fixture does — the sequence is:uv__fast_poll_process_poll_reqruns.patches/libuv/win-poll-rearm-before-callback.patchre-submits an AFD poll before invokingpoll_cb, sosubmitted_events_*is nonzero throughout the callback.poll_cb→data()→us_socket_close→us_poll_stop(now justuv_poll_stop) →bsd_close_socket(fd). The OS handle is now invalid; AFD queues aLOCAL_CLOSEcompletion into the IOCP.poll_cbreturns. In the sameuv_runiteration,check_cbfires →us_internal_loop_post→us_internal_free_closed_sockets→us_poll_free→uv_close→uv__poll_close.LOCAL_CLOSEcompletion is still sitting in the IOCP (the poll phase has not run again between step 2 and step 3), sosubmitted_events_*is still set.uv__poll_closetakes its cancel branch and issues the AFD ioctl on the already-closed handle.Why existing code doesn't prevent it
The
tick_depthbracket added in this PR defersus_poll_freepast nested ticks, but for the ordinary (non-reentrant) close-from-data()case,tick_depth == 1andloop_postrunsus_poll_freein the same iteration'scheck_cb— afterbsd_close_socketand before the next poll phase would have dequeued theLOCAL_CLOSEcompletion.uv_poll_stopalone does not clearsubmitted_events_*, so nothing suppresses the cancel ioctl.Impact
ProcessStrictHandleCheckPolicy(always enabled inside an AppContainer, whichtest/js/bun/windows/appcontainer.test.tscovers):NtDeviceIoControlFileon an invalid handle raises0xC0000008 STATUS_INVALID_HANDLEand terminates the process. This is a regression vs. main in a supported, tested sandbox configuration.uv_closeignoresuv__poll_close's error, so the process survives — but if the handle value was reused for another socket betweenclosesocketandcheck_cb, the exclusive AFD cancel lands on that unrelated socket's request.robobun's dual-build comparison in this PR's timeline reproduced the AppContainer termination 3/3 on this build (both with the extended
appcontainer.test.tsfrom #39643, which adds a close-from-data()step, and with a standalone script that enables strict handle checking in a plain process); both survive on baseline main and on the #39643 build. It also notes that #39643's first push had this same ordering and failed the Windows 11 arm64 CI lane (build 101226) the same way. A close from a timer survives on this build (theLOCAL_CLOSEcompletion is dequeued in the next poll phase beforeloop_post), which further confirms the mechanism. CI is green here only because the currentappcontainer.test.tson main happens not to close a socket fromdata().Step-by-step proof
Given a Bun process inside an AppContainer with a TCP socket that receives data and calls
socket.terminate()insidedata():uv__fast_poll_process_poll_reqre-arms (patch)submitted_events_2 = X(nonzero)poll_cb→data()→terminate()→us_poll_stophandle->events = 0;submitted_events_*unchangedbsd_close_socket(fd)(socket.c:313)LOCAL_CLOSEqueued to IOCPpoll_cbreturns; same iteration →check_cbsubmitted_events_*still nonzerous_poll_free→uv_close→uv__poll_closesubmitted_events != 0→NtDeviceIoControlFile(closed_handle, ...)0xC0000008On main, step 5's ioctl ran at step 2 (inside
us_poll_stop), before step 3 closed the handle, so it targeted an open socket.How to fix it
Keep the fd open until
uv_closeis issued. #39643 does this withus_internal_poll_close_fdplus aclose_fdbit: inside a poll callback, the close paths mark the poll instead of callingbsd_close_socketimmediately, and the deferreduv_closesite closes the fd right after issuinguv_close(souv__poll_close's cancel ioctl still targets an open handle). Adopting that piece — or otherwise ensuringuv_closeprecedesclosesocketon every path — resolves both the AppContainer termination and the wrong-target-cancel hazard.