windows: close NamedPipe handle on drop - #2003
Open
MsfPablo wants to merge 1 commit into
Open
Conversation
`Inner` owns the pipe `HANDLE` and closes it when the last `Arc<Inner>` reference goes away. Every in-flight overlapped operation holds one of those references (leaked via `mem::forget` in `schedule_read` / `maybe_schedule_write`) and only returns it when its completion is witnessed. `SelectorInner::drop` tries to reap those completions with zero-timeout polls of the IOCP, but there is no way to guarantee that a still-pending read or write completes in that window. When one does not, the reference is never returned, `Inner` is never dropped, and the underlying `HANDLE` is leaked for the lifetime of the process. Wrap the handle in a `Mutex<Option<Handle>>` and have `NamedPipe::drop` close it synchronously, independently of the `Arc` refcount. Operations that race with the close now observe `None`: syscall wrappers return a `BrokenPipe` error, and `Inner::result` returns `None` so the completion handlers discard the completion instead of emitting an event for a pipe nobody holds anymore. Note the lock ordering: `io` is always acquired before `handle`, matching the existing call paths (`schedule_read` and the `*_done` handlers hold the `io` guard across the syscall wrappers), so `NamedPipe::drop` takes them in that same order. This means a write that is still pending when the `NamedPipe` is dropped is no longer guaranteed to be flushed, since closing the handle ends it. Previously such a write kept the handle alive forever rather than completing in any observable way. Closes tokio-rs#1944
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Closes #1944.
The leak
Innerowns the pipeHANDLE, so the handle is closed only when the lastArc<Inner>reference goes away. Every in-flight overlapped operation holds one of those references — leaked deliberately viamem::forget(me.clone())inschedule_read/maybe_schedule_write— and only gives it back once its completion is witnessed.SelectorInner::droptries to reap those completions with zero-timeout polls of the IOCP, but as @1c7718e7 points out in the issue, there is no way to guarantee a still-pending read or write completes in that window. When one doesn't, the reference is never returned,Innernever drops, and theHANDLEis leaked for the lifetime of the process.The change
Wrap the handle as
Mutex<Option<Handle>>and haveNamedPipe::dropclose it synchronously, independently of theArcrefcount. This is @1c7718e7's suggested fix from the issue (1c7718e7@8df3ebc); the regression test is theirs too (1c7718e7@94e6627). Credit to them — happy to close this if they'd rather open it themselves.Operations racing with the close now observe
None:connect_overlapped,disconnect,read_overlapped,write_overlapped) return aBrokenPipeerror;Inner::resultreturnsOption<io::Result<usize>>, andconnect_done/read_done/write_donediscard the completion rather than emitting an event for a pipe nobody holds anymore.One deliberate difference from the linked commit: lock ordering. Every existing path takes
iobeforehandle—schedule_readand the*_donehandlers hold theioguard across the syscall wrappers, andSource::registerholds it acrossadd_handle→AsRawHandle. The draft'sNamedPipe::droptookhandlefirst, which inverts that order and can deadlock. This version takesiofirst to match.Semantics change worth flagging
The old
Dropcomment says writes are deliberately not cancelled "to ensure that everything is flushed out". Closing the handle ends any in-flight write, so a write still pending at drop time is no longer guaranteed to flush. I've updated the comment accordingly.In practice this only affects the case that currently leaks: a write that completes synchronously (anything fitting in the pipe's buffer, e.g. the 4 bytes in
write_then_drop) is already in the kernel buffer before the close and is still delivered, followed by EOF. Previously, a genuinely-pending write kept the handle alive forever rather than completing in any observable way, so I don't think anything useful is lost — but this is the point @1c7718e7 wanted maintainer input on, and @Darksonn's "at the very least we must close the handle" is what I took as the go-ahead for this narrower fix. If you'd prefer the fullerflush()-based design discussed in the issue instead, say so and I'll drop this.What I verified, and what I couldn't
I don't have a Windows machine — please treat the runtime behaviour as unverified.
Verified:
cargo check --target x86_64-pc-windows-msvc --all-features --all-targets— clean.cargo clippy --target x86_64-pc-windows-msvc --all-features --all-targets— no new lints; the handful of remaining ones are pre-existing (impl<'a> Read for &'a NamedPipeetc.).cargo fmt --check— clean.cargo build --all-featuresandcargo test --all-featureson macOS — pass, though these don't touch any of the changed code.io/handleacquisition path, as described above.Not verified:
handle_closed_on_dropactually fails before this change and passes after. I'm relying on @1c7718e7's report that it reproduces on Windows 11 and Wine 11.5.tests/win_named_pipe.rsstill passing. I also picked up theconnect_twicerestructuring from the linked fix commit — it scans all events for a readableToken(0)instead of only the firstToken(0)event — on the assumption it's needed because closing the handle shifts event delivery timing. I can't confirm that, so it's worth a look; I'll drop it if it's unnecessary.Windows CI on this PR is the real test.
Courtesy note: this patch was prepared with AI assistance (Claude Code). The design is @1c7718e7's; I reviewed the lock ordering, the completion-handler paths and the flush-semantics implications myself, and the verification claims above are accurate about what was and wasn't run. I didn't see a CONTRIBUTING.md or AI policy in the repo — let me know if you'd like this handled differently.