Do not report a channel closed underneath us as EBADF on close - #9723
Open
aminmansuri wants to merge 4 commits into
Open
aminmansuri wants to merge 4 commits into
aminmansuri wants to merge 4 commits into
Conversation
Thread#kill unblocks IO by interrupting the thread, and the JDK closes an interruptible channel when it does. Ruby never closed that stream, so its close must succeed rather than raise Errno::EBADF out of an ensure.
The early isOpen check skipped the whole close branch, so the last referrer left its wrapper registrations behind. Drop the check rather than guard the branch: Channel#close is a documented no-op on an already closed channel.
Close the channel only while it is open: a second close throws IllegalStateException from some channels and re-enters a user object's close. Unregister by identity so a stale close cannot evict a live wrapper that has taken over a recycled fileno.
Contributor
Author
|
My interest in the fix is primarily eradicating the build errors. |
kares
reviewed
Sep 21, 2026
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.
Fixes #9722.
Thread#killunblocks IO by interrupting the native thread, and the JDK closes the interruptible channel in response. Ruby never closed that stream, but theensurethat follows callsIO#close, andChannelFD.finish()raisedClosedChannelException— surfacing asErrno::EBADF, whichThread#jointhen re-raises into the caller.The close is now skipped when the channel has already gone. Two details:
isOpen()guardsch.close()only, not the branch, so the fileno bookkeeping still runs. It cannot be dropped either:FileDescriptorByteChannel.close()throws on a second close, andIOChannel.close()would call a user object twice.unregisterWrappernow takes the wrapper and removes only if the fileno still maps to it. By key alone, a stale close evicts a live wrapper on a recycled descriptor and a later close closes an unrelated file.The one-argument
unregisterWrapper(int)is left in place, now unused and unsafe unless the caller owns the mapping — deprecating it seemed an API call to leave to you.Evidence
Forcing test red before, green after. 800 reproduction rounds: 14 failures on Java 21 and 12 on Java 25 before, 0 after; CRuby 0 in 400. MRI
net/httpreproduces the defect on base at about 1 run in 10, absent across 10 runs after.test_io.rb49, MRIruby/test_io.rb200, stdlibnet socket io fiber362 — all green; io, file and socket specs identical.Limits
Only
closeis fixed. After the interrupt the stream still reportsclosed? == falseand still failswriteandselect. Andio.to_channel.close; io.closeno longer raisesErrno::EBADF, which CRuby does — a lost diagnostic.The cause is the interrupt itself, as the
FIXMEatOpenFile.java:1466says. GivingwriteInternalthe pre-selectreadInternalhas would fix all four, at the cost of a select per write. Happy to take that on if you want it.