Skip to content

Commit 2ba98f8

Browse files
authored
Merge pull request jruby#9325 from kares/fix-npe-openfile_channel-10.0
[fix] NullPointerException from closed IO
2 parents 9f1e76d + 280eeb5 commit 2ba98f8

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
@@ -2547,37 +2547,39 @@ public ChannelFD fd() {
25472547
}
25482548

25492549
public Channel channel() {
2550-
assert(fd != null);
2550+
// MRI equivalent: rb_io_check_closed(fptr) + fptr->fd access in io.c
2551+
// when an IO was closed from another thread MRI raises IOError("closed stream") via io_fd_check_closed (io.c)
2552+
checkClosed();
25512553
return fd.ch;
25522554
}
25532555

25542556
public ReadableByteChannel readChannel() {
2555-
assert(fd != null);
2557+
checkClosed();
25562558
return fd.chRead;
25572559
}
25582560

25592561
public WritableByteChannel writeChannel() {
2560-
assert(fd != null);
2562+
checkClosed();
25612563
return fd.chWrite;
25622564
}
25632565

25642566
public SeekableByteChannel seekChannel() {
2565-
assert(fd != null);
2567+
checkClosed();
25662568
return fd.chSeek;
25672569
}
25682570

25692571
public SelectableChannel selectChannel() {
2570-
assert(fd != null);
2572+
checkClosed();
25712573
return fd.chSelect;
25722574
}
25732575

25742576
public FileChannel fileChannel() {
2575-
assert(fd != null);
2577+
checkClosed();
25762578
return fd.chFile;
25772579
}
25782580

25792581
public SocketChannel socketChannel() {
2580-
assert(fd != null);
2582+
checkClosed();
25812583
return fd.chSock;
25822584
}
25832585

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)