Skip to content

Commit 3901cbc

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 6203c21 commit 3901cbc

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
@@ -824,6 +824,59 @@ def test_handle_interrupt_with_break
824824
end
825825
end
826826

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

‎thread.c‎

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

28822882
if (th->ec->raised_flag) return ret;
28832883

2884+
/* Interrupts are checked at arbitrary points, including while a jump
2885+
* (break/next/redo/return/raise/throw) is being propagated.
2886+
* Save errinfo as the interrupt might run Ruby code that could clear it. */
2887+
const VALUE saved_errinfo = th->ec->errinfo;
2888+
28842889
while ((interrupt = threadptr_get_interrupts(th)) != 0) {
28852890
int sig;
28862891
int timer_interrupt;
@@ -2976,6 +2981,9 @@ rb_threadptr_execute_interrupts(rb_thread_t *th, int blocking_timing)
29762981
rb_thread_schedule_limits(limits_us);
29772982
}
29782983
}
2984+
2985+
th->ec->errinfo = saved_errinfo;
2986+
29792987
return ret;
29802988
}
29812989

0 commit comments

Comments
 (0)