Skip to content

Commit fdd2683

Browse files
committed
[Bug #22415] Restore ec->errinfo after handling interrupts
Interrupts are checked at arbitrary points, including while a jump (break/next/redo/return/raise/throw) is being propagated, and the payload of that jump lives in ec->errinfo. Handling an interrupt can run Ruby code: rb_signal_exec() runs a trap handler and threadptr_interrupt_exec_exec() runs interrupt_exec tasks. Neither preserves ec->errinfo, and any rescue or ensure clause that code enters sets it to nil. Thread.handle_interrupt checks for interrupts after its block returns but before re-raising the jump that left the block. If a signal handler runs in that window the throw data is replaced with nil and the VM dereferences nil as a vm_throw_data: THROW_DATA_CATCH_FRAME (obj=0x4) at vm_insnhelper.h:217 vm_exec_handle_exception (ec=..., errinfo=4) at vm.c:2880 vm_exec_loop / rb_vm_exec at vm.c:2829 / vm.c:2812 invoke_iseq_block_from_c / rb_yield / rb_ary_each rb_postponed_job_flush() and rb_exec_event_hooks() already save and restore ec->errinfo around the Ruby code they run; do the same for the rest of rb_threadptr_execute_interrupts(). The paths that set errinfo on purpose (rb_exc_raise, rb_threadptr_to_kill) jump out of the function and so do not reach the restore.
1 parent 7ad8db2 commit fdd2683

2 files changed

Lines changed: 61 additions & 0 deletions

File tree

‎test/ruby/test_thread.rb‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -822,6 +822,59 @@ def test_handle_interrupt_with_break
822822
end
823823
end
824824

825+
# Thread.handle_interrupt checks for interrupts after the block returns,
826+
# before re-raising the jump (break/return/raise) that left the block.
827+
# Running a signal handler there must not clobber ec->errinfo, which still
828+
# holds the payload of that jump.
829+
def test_handle_interrupt_with_return_and_trap_handler
830+
omit "needs fork" unless Process.respond_to?(:fork)
831+
omit "SIGUSR1 is not supported" unless Signal.list.key?("USR1")
832+
833+
assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}", timeout: 120)
834+
begin;
835+
trap(:USR1) do
836+
begin
837+
raise "entering a rescue clause clears errinfo"
838+
rescue
839+
end
840+
end
841+
842+
def return_from_handle_interrupt(array)
843+
array.each do |i|
844+
Thread.handle_interrupt(Exception => :immediate) { return i }
845+
end
846+
nil
847+
end
848+
849+
# Signal from another process: a thread of this process would only get
850+
# scheduled a few times a second while the main thread spins.
851+
pid = Process.pid
852+
killer = fork do
853+
begin
854+
# Process.ppid changes if the parent crashes.
855+
while Process.ppid == pid
856+
Process.kill(:USR1, pid)
857+
sleep 0.001
858+
end
859+
rescue Errno::ESRCH
860+
end
861+
exit!(0)
862+
end
863+
864+
returned = nil
865+
begin
866+
500_000.times do
867+
returned = return_from_handle_interrupt([1, 2, 3])
868+
break unless returned == 1
869+
end
870+
ensure
871+
Process.kill(:KILL, killer) rescue Errno::ESRCH
872+
Process.waitpid(killer)
873+
end
874+
assert_equal(1, returned)
875+
end;
876+
end
877+
825878
def test_handle_interrupt_blocking
826879
r = nil
827880
q = Thread::Queue.new

‎thread.c‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2416,6 +2416,11 @@ rb_threadptr_execute_interrupts(rb_thread_t *th, int blocking_timing)
24162416

24172417
if (th->ec->raised_flag) return ret;
24182418

2419+
/* Interrupts are checked at arbitrary points, including while a jump
2420+
* (break/next/redo/return/raise/throw) is being propagated.
2421+
* Save errinfo as the interrupt might run Ruby code that could clear it. */
2422+
const VALUE saved_errinfo = th->ec->errinfo;
2423+
24192424
while ((interrupt = threadptr_get_interrupts(th)) != 0) {
24202425
int sig;
24212426
int timer_interrupt;
@@ -2500,6 +2505,9 @@ rb_threadptr_execute_interrupts(rb_thread_t *th, int blocking_timing)
25002505
rb_thread_schedule_limits(limits_us);
25012506
}
25022507
}
2508+
2509+
th->ec->errinfo = saved_errinfo;
2510+
25032511
return ret;
25042512
}
25052513

0 commit comments

Comments
 (0)