Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 6 additions & 8 deletions core/src/main/java/org/jruby/util/io/ChannelFD.java
Original file line number Diff line number Diff line change
Expand Up @@ -136,24 +136,22 @@ private void finish() throws IOException {
throw new ClosedChannelException();
}

// if channel is already closed, we're no longer valid
if (!ch.isOpen()) {
throw new ClosedChannelException();
}

// otherwise decrement and possibly close as normal
int count = refs.decrementAndGet();

if (count <= 0) {
// if we're the last referrer, close the channel
try {
ch.close();
// The JDK closes an interruptible channel when the thread blocked on it is
// interrupted, which is how Thread#kill unblocks IO, so Ruby never closed
// that stream and there is nothing left for us to close.
if (ch.isOpen()) ch.close();
if (needsClosing) {
filenoUtil.closeFilenoHandle(realFileno);
}
} finally {
filenoUtil.unregisterWrapper(realFileno);
filenoUtil.unregisterWrapper(fakeFileno);
filenoUtil.unregisterWrapper(realFileno, this);
filenoUtil.unregisterWrapper(fakeFileno, this);
}
}
}
Expand Down
7 changes: 7 additions & 0 deletions core/src/main/java/org/jruby/util/io/FilenoUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,13 @@ public void unregisterWrapper(int fileno) {
filenoMap.remove(fileno);
}

// Remove only if the fileno still maps to this wrapper, so a stale close cannot evict a
// live wrapper that has taken over a recycled descriptor number.
public void unregisterWrapper(int fileno, ChannelFD wrapper) {
if (fileno == -1) return;
filenoMap.remove(fileno, wrapper);
}

// Used by testing. See test/jruby/test_io.rb, test_io_copy_stream_does_not_leak_io_like_objects
public int getNumberOfWrappers() {
return filenoMap.size();
Expand Down
25 changes: 25 additions & 0 deletions test/jruby/test_io.rb
Original file line number Diff line number Diff line change
Expand Up @@ -698,4 +698,29 @@ def test_accept_on_closed_server_raises_ioerror
assert true
end

# Thread#kill unblocks a write by interrupting the thread, and the JDK closes the
# interruptible channel in response. Ruby never closed the stream, so the close in
# the ensure must succeed rather than report the vanished channel as Errno::EBADF.
def test_close_after_kill_during_blocking_write
read, write = IO.pipe
@to_close << read
@to_close << write

started = Queue.new
thread = Thread.new do
begin
started << true
write.write('x' * 1_000_000) # larger than the pipe buffer, so it blocks
ensure
write.close
end
end

started.pop
sleep 0.2 # let the write fill the buffer and block
thread.kill

assert_nothing_raised { thread.join }
end

end
Loading