Skip to content

Commit 6ab97cf

Browse files
Simplify test: raise immediately from blocking_operation_wait, no operation
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent e379f14 commit 6ab97cf

1 file changed

Lines changed: 17 additions & 56 deletions

File tree

test/fiber/test_blocking_operation_exception.rb

Lines changed: 17 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -3,89 +3,50 @@
33
# Test that rb_fiber_scheduler_blocking_operation_wait correctly handles
44
# exceptions raised by the scheduler's blocking_operation_wait method.
55
#
6-
# When the scheduler raises (e.g. due to fiber interrupt), the exception
7-
# propagates through rb_funcall in the C function, bypassing the cleanup
8-
# code that nulls out operation->function, operation->state, etc.
9-
# operation->state points to a stack-allocated struct in the caller
10-
# (rb_nogvl), so after the exception unwinds that frame the pointer is
11-
# dangling. Any subsequent dereference — including from dfree during GC
12-
# or a re-execution of the operation — causes a segfault.
6+
# When blocking_operation_wait raises, rb_funcall propagates the exception,
7+
# bypassing the cleanup code that nulls out operation->function/state/data.
8+
# operation->state points to a stack-allocated struct in the caller (rb_nogvl),
9+
# so after the exception unwinds that caller's frame the pointer is dangling.
10+
# Any subsequent dereference of those fields (e.g. from a scheduler that
11+
# tracks and cancels blocking operations during io_close) causes a segfault.
1312

1413
require "test/unit"
1514
require_relative "scheduler"
1615

1716
class TestBlockingOperationException < Test::Unit::TestCase
18-
# Scheduler whose blocking_operation_wait raises after completing the work,
19-
# simulating a fiber interrupt arriving after the operation finishes.
20-
class InterruptingScheduler < Scheduler
17+
# Scheduler that raises immediately from blocking_operation_wait without
18+
# executing the operation — the simplest possible trigger for the bug.
19+
class RaisingScheduler < Scheduler
2120
def blocking_operation_wait(blocking_operation)
22-
super
2321
raise Interrupt, "simulated fiber interrupt"
2422
end
2523
end
2624

27-
# Reproduce the tcp_socket.rb pattern: a blocking IO operation (large
28-
# IO::Buffer copy) raises through the scheduler, then we close a socket.
29-
# If the exception corrupted scheduler state, server.close may crash.
30-
def test_blocking_operation_exception_with_socket_close
25+
def test_blocking_operation_wait_exception
3126
skip "IO::Buffer not available" unless defined?(IO::Buffer)
3227

33-
size = 2 * 1024 * 1024 # 2 MiB — large enough to trigger blocking_operation_wait
34-
source = IO::Buffer.new(size)
35-
dest = IO::Buffer.new(size)
36-
source.clear(65) # fill with 'A'
37-
38-
server = TCPServer.new("127.0.0.1", 0)
39-
caught = []
40-
41-
# Run the blocking operation that raises inside a scheduled thread
42-
Thread.new do
43-
Fiber.set_scheduler(InterruptingScheduler.new)
44-
Fiber.schedule do
45-
dest.copy(source, 0, size, 0)
46-
rescue Interrupt => e
47-
caught << e
48-
end
49-
end.join
50-
51-
assert_equal 1, caught.size, "Expected exactly one Interrupt"
52-
53-
# After the exception propagated through rb_fiber_scheduler_blocking_operation_wait,
54-
# operation->state is a dangling pointer. Trigger GC and then close a socket:
55-
# if the dangling pointer is somehow dereferenced during these operations,
56-
# ASAN/valgrind/the OS will detect it.
57-
GC.start(full_mark: true, immediate_sweep: true)
58-
GC.compact if GC.respond_to?(:compact)
59-
60-
server.close # mirrors tcp_socket.rb:48
61-
62-
assert_equal size, dest.size
63-
ensure
64-
server&.close rescue nil
65-
end
66-
67-
def test_blocking_operation_exception_does_not_corrupt_state
68-
skip "IO::Buffer not available" unless defined?(IO::Buffer)
69-
70-
size = 2 * 1024 * 1024
28+
size = 2 * 1024 * 1024 # 2 MiB — triggers the scheduler hook via rb_nogvl
7129
source = IO::Buffer.new(size)
7230
dest = IO::Buffer.new(size)
7331
source.clear(65)
7432

7533
caught = []
7634

7735
Thread.new do
78-
Fiber.set_scheduler(InterruptingScheduler.new)
36+
Fiber.set_scheduler(RaisingScheduler.new)
7937
Fiber.schedule do
8038
dest.copy(source, 0, size, 0)
8139
rescue Interrupt => e
8240
caught << e
8341
end
8442
end.join
8543

86-
assert_equal 1, caught.size
44+
assert_equal 1, caught.size, "Expected the Interrupt to propagate to Ruby"
45+
46+
# Force GC to run dfree on the blocking_operation TypedData.
47+
# With ASAN or a scheduler that dereferences operation->state during
48+
# cleanup (e.g. io-event's io_close), this is where the crash manifests.
8749
GC.start(full_mark: true, immediate_sweep: true)
8850
GC.compact if GC.respond_to?(:compact)
89-
assert_equal size, dest.size
9051
end
9152
end

0 commit comments

Comments
 (0)