Skip to content

Commit 81a23c5

Browse files
committed
rb_io_blocking_operation_exit should not execute with pending interrupts.
1 parent e093c31 commit 81a23c5

File tree

5 files changed

+166
-34
lines changed

5 files changed

+166
-34
lines changed

internal/io.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ struct rb_io_blocking_operation {
2525
// The linked list data structure.
2626
struct ccan_list_node list;
2727

28-
// The execution context of the blocking operation:
28+
// The execution context of the blocking operation.
2929
struct rb_execution_context_struct *ec;
3030
};
3131

scheduler.c

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,13 @@ rb_fiber_scheduler_unblock(VALUE scheduler, VALUE blocker, VALUE fiber)
422422
// If we explicitly preserve `errno` in `io_binwrite` and other similar functions (e.g. by returning it), this code is no longer needed. I hope in the future we will be able to remove it.
423423
int saved_errno = errno;
424424

425+
#ifdef RUBY_DEBUG
426+
rb_execution_context_t *ec = GET_EC();
427+
if (ec->interrupt_flag) {
428+
rb_bug("rb_fiber_scheduler_unblock called with interrupt flags set");
429+
}
430+
#endif
431+
425432
VALUE result = rb_funcall(scheduler, id_unblock, 2, blocker, fiber);
426433

427434
errno = saved_errno;
@@ -853,6 +860,13 @@ VALUE rb_fiber_scheduler_fiber_interrupt(VALUE scheduler, VALUE fiber, VALUE exc
853860
fiber, exception
854861
};
855862

863+
#ifdef RUBY_DEBUG
864+
rb_execution_context_t *ec = GET_EC();
865+
if (ec->interrupt_flag) {
866+
rb_bug("rb_fiber_scheduler_fiber_interrupt called with interrupt flags set");
867+
}
868+
#endif
869+
856870
return rb_check_funcall(scheduler, id_fiber_interrupt, 2, arguments);
857871
}
858872

test/fiber/test_io.rb

Lines changed: 29 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ def test_read
99
omit unless defined?(UNIXSocket)
1010

1111
i, o = UNIXSocket.pair
12-
if RUBY_PLATFORM=~/mswin|mingw/
12+
if RUBY_PLATFORM =~ /mswin|mingw/
1313
i.nonblock = true
1414
o.nonblock = true
1515
end
@@ -44,7 +44,7 @@ def test_heavy_read
4444
16.times.map do
4545
Thread.new do
4646
i, o = UNIXSocket.pair
47-
if RUBY_PLATFORM=~/mswin|mingw/
47+
if RUBY_PLATFORM =~ /mswin|mingw/
4848
i.nonblock = true
4949
o.nonblock = true
5050
end
@@ -67,7 +67,7 @@ def test_heavy_read
6767

6868
def test_epipe_on_read
6969
omit unless defined?(UNIXSocket)
70-
omit "nonblock=true isn't properly supported on Windows" if RUBY_PLATFORM=~/mswin|mingw/
70+
omit "nonblock=true isn't properly supported on Windows" if RUBY_PLATFORM =~ /mswin|mingw/
7171

7272
i, o = UNIXSocket.pair
7373

@@ -242,38 +242,37 @@ def test_close_while_reading_on_thread
242242
# Windows has UNIXSocket, but only with VS 2019+
243243
omit "UNIXSocket is not defined!" unless defined?(UNIXSocket)
244244

245-
i, o = Socket.pair(:UNIX, :STREAM)
246-
if RUBY_PLATFORM=~/mswin|mingw/
247-
i.nonblock = true
248-
o.nonblock = true
249-
end
245+
Socket.pair(:UNIX, :STREAM) do |i, o|
246+
if RUBY_PLATFORM =~ /mswin|mingw/
247+
i.nonblock = true
248+
o.nonblock = true
249+
end
250250

251-
reading_thread = Thread.new do
252-
Thread.current.report_on_exception = false
253-
i.wait_readable
254-
end
251+
reading_thread = Thread.new do
252+
Thread.current.report_on_exception = false
253+
i.wait_readable
254+
end
255255

256-
fs_thread = Thread.new do
257-
# Wait until the reading thread is blocked on read:
258-
Thread.pass until reading_thread.status == "sleep"
256+
scheduler_thread = Thread.new do
257+
# Wait until the reading thread is blocked on read:
258+
Thread.pass until reading_thread.status == "sleep"
259259

260-
scheduler = Scheduler.new
261-
Fiber.set_scheduler scheduler
262-
Fiber.schedule do
263-
i.close
260+
scheduler = Scheduler.new
261+
Fiber.set_scheduler scheduler
262+
Fiber.schedule do
263+
i.close
264+
end
264265
end
265-
end
266266

267-
assert_raise(IOError) { reading_thread.join }
268-
refute_nil fs_thread.join(5), "expected thread to terminate within 5 seconds"
267+
assert_raise(IOError) { reading_thread.join }
268+
refute_nil scheduler_thread.join(5), "expected thread to terminate within 5 seconds"
269269

270-
assert_predicate(i, :closed?)
271-
ensure
272-
fs_thread&.kill
273-
fs_thread&.join rescue nil
274-
reading_thread&.kill
275-
reading_thread&.join rescue nil
276-
i&.close
277-
o&.close
270+
assert_predicate(i, :closed?)
271+
ensure
272+
scheduler_thread&.kill
273+
scheduler_thread&.join rescue nil
274+
reading_thread&.kill
275+
reading_thread&.join rescue nil
276+
end
278277
end
279278
end

test/fiber/test_io_close.rb

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# frozen_string_literal: true
2+
require 'test/unit'
3+
require_relative 'scheduler'
4+
5+
class TestFiberIOClose < Test::Unit::TestCase
6+
def with_socket_pair(&block)
7+
omit "UNIXSocket is not defined!" unless defined?(UNIXSocket)
8+
9+
UNIXSocket.pair do |i, o|
10+
if RUBY_PLATFORM =~ /mswin|mingw/
11+
i.nonblock = true
12+
o.nonblock = true
13+
end
14+
15+
yield i, o
16+
end
17+
end
18+
19+
# Problematic on Windows.
20+
def test_io_close_across_fibers
21+
omit "Interrupting a io_wait read is not supported!" if RUBY_PLATFORM =~ /mswin|mingw/
22+
23+
with_socket_pair do |i, o|
24+
error = nil
25+
26+
thread = Thread.new do
27+
scheduler = Scheduler.new
28+
Fiber.set_scheduler scheduler
29+
30+
Fiber.schedule do
31+
i.read
32+
rescue => error
33+
# Ignore.
34+
end
35+
36+
Fiber.schedule do
37+
i.close
38+
end
39+
end
40+
41+
thread.join
42+
43+
assert_instance_of IOError, error
44+
assert_match(/closed/, error.message)
45+
end
46+
end
47+
48+
# Okay on all platforms.
49+
def test_io_close_blocking_thread
50+
omit "Interrupting a io_wait read is not supported!" if RUBY_PLATFORM =~ /mswin|mingw/
51+
52+
with_socket_pair do |i, o|
53+
error = nil
54+
55+
reading_thread = Thread.new do
56+
i.read
57+
rescue => error
58+
# Ignore.
59+
end
60+
61+
Thread.pass until reading_thread.status == 'sleep'
62+
63+
thread = Thread.new do
64+
scheduler = Scheduler.new
65+
Fiber.set_scheduler scheduler
66+
67+
Fiber.schedule do
68+
i.close
69+
end
70+
end
71+
72+
thread.join
73+
reading_thread.join
74+
75+
assert_instance_of IOError, error
76+
assert_match(/closed/, error.message)
77+
end
78+
end
79+
80+
# Problematic on Windows.
81+
def test_io_close_blocking_fiber
82+
omit "Interrupting a io_wait read is not supported!" if RUBY_PLATFORM =~ /mswin|mingw/
83+
84+
with_socket_pair do |i, o|
85+
error = nil
86+
87+
thread = Thread.new do
88+
scheduler = Scheduler.new
89+
Fiber.set_scheduler scheduler
90+
91+
Fiber.schedule do
92+
begin
93+
i.read
94+
rescue => error
95+
# Ignore.
96+
end
97+
end
98+
end
99+
100+
Thread.pass until thread.status == 'sleep'
101+
102+
i.close
103+
104+
thread.join
105+
106+
assert_instance_of IOError, error
107+
assert_match(/closed/, error.message)
108+
end
109+
end
110+
end

thread.c

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,10 @@ static inline void blocking_region_end(rb_thread_t *th, struct rb_blocking_regio
207207
static inline int
208208
vm_check_ints_blocking(rb_execution_context_t *ec)
209209
{
210+
#ifdef RUBY_ASSERT_CRITICAL_SECTION
211+
VM_ASSERT(ruby_assert_critical_section_entered == 0);
212+
#endif
213+
210214
rb_thread_t *th = rb_ec_thread_ptr(ec);
211215

212216
if (LIKELY(rb_threadptr_pending_interrupt_empty_p(th))) {
@@ -1947,6 +1951,9 @@ rb_thread_io_blocking_call(struct rb_io* io, rb_blocking_function_t *func, void
19471951
RUBY_VM_CHECK_INTS_BLOCKING(ec);
19481952
goto retry;
19491953
}
1954+
1955+
RUBY_VM_CHECK_INTS_BLOCKING(ec);
1956+
19501957
state = saved_state;
19511958
}
19521959
EC_POP_TAG();
@@ -1961,9 +1968,6 @@ rb_thread_io_blocking_call(struct rb_io* io, rb_blocking_function_t *func, void
19611968
EC_JUMP_TAG(ec, state);
19621969
}
19631970

1964-
/* TODO: check func() */
1965-
RUBY_VM_CHECK_INTS_BLOCKING(ec);
1966-
19671971
// If the error was a timeout, we raise a specific exception for that:
19681972
if (saved_errno == ETIMEDOUT) {
19691973
rb_raise(rb_eIOTimeoutError, "Blocking operation timed out!");
@@ -4471,6 +4475,8 @@ do_select(VALUE p)
44714475
RUBY_VM_CHECK_INTS_BLOCKING(set->th->ec); /* may raise */
44724476
} while (wait_retryable(&result, lerrno, to, endtime) && do_select_update());
44734477

4478+
RUBY_VM_CHECK_INTS_BLOCKING(set->th->ec);
4479+
44744480
if (result < 0) {
44754481
errno = lerrno;
44764482
}
@@ -4591,7 +4597,10 @@ thread_io_wait(struct rb_io *io, int fd, int events, struct timeval *timeout)
45914597

45924598
RUBY_VM_CHECK_INTS_BLOCKING(ec);
45934599
} while (wait_retryable(&result, lerrno, to, end));
4600+
4601+
RUBY_VM_CHECK_INTS_BLOCKING(ec);
45944602
}
4603+
45954604
EC_POP_TAG();
45964605
}
45974606

0 commit comments

Comments
 (0)