Skip to content

Commit 4fd8090

Browse files
authored
Merge pull request jruby#9326 from kares/fix-npe-openfile_channel-10.0
[fix] NullPointerException from closed IO
2 parents 11783f6 + 280eeb5 commit 4fd8090

2 files changed

Lines changed: 40 additions & 7 deletions

File tree

core/src/main/java/org/jruby/util/io/OpenFile.java

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2578,37 +2578,39 @@ public ChannelFD fd() {
25782578
}
25792579

25802580
public Channel channel() {
2581-
assert(fd != null);
2581+
// MRI equivalent: rb_io_check_closed(fptr) + fptr->fd access in io.c
2582+
// when an IO was closed from another thread MRI raises IOError("closed stream") via io_fd_check_closed (io.c)
2583+
checkClosed();
25822584
return fd.ch;
25832585
}
25842586

25852587
public ReadableByteChannel readChannel() {
2586-
assert(fd != null);
2588+
checkClosed();
25872589
return fd.chRead;
25882590
}
25892591

25902592
public WritableByteChannel writeChannel() {
2591-
assert(fd != null);
2593+
checkClosed();
25922594
return fd.chWrite;
25932595
}
25942596

25952597
public SeekableByteChannel seekChannel() {
2596-
assert(fd != null);
2598+
checkClosed();
25972599
return fd.chSeek;
25982600
}
25992601

26002602
public SelectableChannel selectChannel() {
2601-
assert(fd != null);
2603+
checkClosed();
26022604
return fd.chSelect;
26032605
}
26042606

26052607
public FileChannel fileChannel() {
2606-
assert(fd != null);
2608+
checkClosed();
26072609
return fd.chFile;
26082610
}
26092611

26102612
public SocketChannel socketChannel() {
2611-
assert(fd != null);
2613+
checkClosed();
26122614
return fd.chSock;
26132615
}
26142616

test/jruby/test_io.rb

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,4 +651,35 @@ def out_stream.read(*n)
651651
assert_equal before, after, "no wrappers should have been registered"
652652
end
653653

654+
# https://github.com/jruby/jruby/issues/9324
655+
def test_accept_on_closed_server_raises_ioerror
656+
require 'socket'
657+
658+
# Concurrent threads create timing pressure needed to hit the race between one accept returning and the next start.
659+
500.times do
660+
tcp = TCPServer.new("127.0.0.1", 0)
661+
port = tcp.connect_address.ip_port
662+
663+
thread = Thread.new do
664+
Thread.current.abort_on_exception = false
665+
Thread.current.report_on_exception = false
666+
loop do
667+
c = tcp.accept
668+
c.close
669+
end
670+
rescue IOError, Errno::EBADF, Errno::EINVAL, Errno::ECONNABORTED, Errno::ENOTSOCK, Errno::ECONNRESET
671+
# expected on shutdown — before the fix, NullPointerException/AssertionError escapes here
672+
end
673+
674+
10.times { Thread.new { begin; TCPSocket.new("127.0.0.1", port).close; rescue; end } }
675+
Thread.pass
676+
tcp.close rescue nil
677+
678+
thread.join(2)
679+
thread.kill if thread.alive?
680+
end
681+
# If we reach here, no AssertionError killed the process
682+
assert true
683+
end
684+
654685
end

0 commit comments

Comments
 (0)