From 7f88b345412e15104277dd4b3eb3a2a2e0e8c5cd Mon Sep 17 00:00:00 2001 From: Luke Gruber Date: Tue, 16 Sep 2025 13:06:03 -0400 Subject: [PATCH] Disallow pending interrupts to be checked during FiberScheduler#unblock Ractors can send signals at any time, so the previous debug assertion can fail if a Ractor sends a signal. ```ruby require 'async/scheduler' scheduler = Async::Scheduler.new Fiber.set_scheduler(scheduler) Signal.trap(:INT) do end q = Thread::Queue.new Thread.new do loop do Ractor.new do Process.kill(:INT, $$) end.value end end Fiber.schedule do Fiber.schedule do 1.upto(1000000) do |i| sleep 0.01 q.pop q.push(1) puts "1 iter push/pop" end end Fiber.schedule do 1.upto(1000000) do |i| sleep 0.01 q.push(i) q.pop puts "1 iter push/pop#2" end end end ``` --- depend | 1 + scheduler.c | 45 ++++++++++++++++++++++++++++++++++----------- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/depend b/depend index b9d91faa2a05a2..5c3a03c96219d1 100644 --- a/depend +++ b/depend @@ -15019,6 +15019,7 @@ scheduler.$(OBJEXT): {$(VPATH)}config.h scheduler.$(OBJEXT): {$(VPATH)}constant.h scheduler.$(OBJEXT): {$(VPATH)}defines.h scheduler.$(OBJEXT): {$(VPATH)}encoding.h +scheduler.$(OBJEXT): {$(VPATH)}eval_intern.h scheduler.$(OBJEXT): {$(VPATH)}fiber/scheduler.h scheduler.$(OBJEXT): {$(VPATH)}id.h scheduler.$(OBJEXT): {$(VPATH)}id_table.h diff --git a/scheduler.c b/scheduler.c index ddb205da885cee..19be9563f92d7b 100644 --- a/scheduler.c +++ b/scheduler.c @@ -9,6 +9,7 @@ **********************************************************************/ #include "vm_core.h" +#include "eval_intern.h" #include "ruby/fiber/scheduler.h" #include "ruby/io.h" #include "ruby/io/buffer.h" @@ -647,18 +648,29 @@ rb_fiber_scheduler_unblock(VALUE scheduler, VALUE blocker, VALUE fiber) { RUBY_ASSERT(rb_obj_is_fiber(fiber)); + VALUE result; + enum ruby_tag_type state; // `rb_fiber_scheduler_unblock` can be called from points where `errno` is expected to be preserved. Therefore, we should save and restore it. For example `io_binwrite` calls `rb_fiber_scheduler_unblock` and if `errno` is reset to 0 by user code, it will break the error handling in `io_write`. // 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. int saved_errno = errno; -#ifdef RUBY_DEBUG rb_execution_context_t *ec = GET_EC(); - if (RUBY_VM_INTERRUPTED(ec)) { - rb_bug("rb_fiber_scheduler_unblock called with pending interrupt"); + int old_interrupt_mask = ec->interrupt_mask; + ec->interrupt_mask |= PENDING_INTERRUPT_MASK; + + EC_PUSH_TAG(ec); + if ((state = EC_EXEC_TAG()) == TAG_NONE) { + result = rb_funcall(scheduler, id_unblock, 2, blocker, fiber); } -#endif + EC_POP_TAG(); - VALUE result = rb_funcall(scheduler, id_unblock, 2, blocker, fiber); + ec->interrupt_mask = old_interrupt_mask; + + if (state) { + EC_JUMP_TAG(ec, state); + } + + RUBY_VM_CHECK_INTS(ec); errno = saved_errno; @@ -1079,15 +1091,26 @@ VALUE rb_fiber_scheduler_fiber_interrupt(VALUE scheduler, VALUE fiber, VALUE exc VALUE arguments[] = { fiber, exception }; - -#ifdef RUBY_DEBUG + VALUE result; + enum ruby_tag_type state; rb_execution_context_t *ec = GET_EC(); - if (RUBY_VM_INTERRUPTED(ec)) { - rb_bug("rb_fiber_scheduler_fiber_interrupt called with pending interrupt"); + int old_interrupt_mask = ec->interrupt_mask; + ec->interrupt_mask |= PENDING_INTERRUPT_MASK; + + EC_PUSH_TAG(ec); + if ((state = EC_EXEC_TAG()) == TAG_NONE) { + result = rb_check_funcall(scheduler, id_fiber_interrupt, 2, arguments); } -#endif + EC_POP_TAG(); + + ec->interrupt_mask = old_interrupt_mask; - return rb_check_funcall(scheduler, id_fiber_interrupt, 2, arguments); + if (state) { + EC_JUMP_TAG(ec, state); + } + + RUBY_VM_CHECK_INTS(ec); + return result; } /*