From d72a0fed6019e7e6b2453594babb89f406aed1d7 Mon Sep 17 00:00:00 2001 From: Luke Gruber Date: Tue, 10 Mar 2026 16:13:38 -0400 Subject: [PATCH] Always take th->interrupt_lock in ubf_clear Patch 08372635f7 fixed a race condition on ubfs, but it's only valid if right after a call to `ubf_clear`, we assume the ubf function cannot be in the middle of running. This patch removes an optimization in `ubf_clear` that violates that assumption. In short, `ubf_clear` needs to take `th->interrupt_lock` unconditionally both to avoid deadlocks and to be able to reason about when ubfs can be run. This should fix CI errors like https://ci.rvm.jp/results/trunk-jemalloc@ruby-sp2-noble-docker/6242153. The error was in test_timeout.rb, which had a deadlock during VM shutdown. ```ruby r = Ractor.new do begin Timeout.timeout(0.1) { sleep } rescue Timeout::Error :ok end end.value assert_equal :ok, r ``` The deadlock happened during `rb_ractor_terminate_interrupt_main_thread` with 2 ractors: 1) r1 t1: UBF called with t2->interrupt_lock (ubf = ubf_waiting) 2) r2 t2: ubf cleared from previous thread_sched_wait_events_call (but no lock taken, because of optimization) 3) r2 t2: thread_sched_wait_events: acquire thread_sched_lock(t2) (caller calling native_sleep() in loop) 4) r2 t2: ubf_set: try to acquire t2->interrupt_lock [block] 5) r1 t1: try to acquire thread_sched_lock(t2) [block, deadlock] t2 needs to block on t2->interrupt_lock in step 2 until the ubf has completed. Only then can it register a new ubf in the next `native_sleep` iteration. --- thread_pthread.c | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/thread_pthread.c b/thread_pthread.c index 38bc134fa83b38..9f6707ae5d1f3c 100644 --- a/thread_pthread.c +++ b/thread_pthread.c @@ -1055,14 +1055,12 @@ ubf_set(rb_thread_t *th, rb_unblock_function_t *func, void *arg) static void ubf_clear(rb_thread_t *th) { - if (th->unblock.func) { - rb_native_mutex_lock(&th->interrupt_lock); - { - th->unblock.func = NULL; - th->unblock.arg = NULL; - } - rb_native_mutex_unlock(&th->interrupt_lock); + rb_native_mutex_lock(&th->interrupt_lock); + { + th->unblock.func = NULL; + th->unblock.arg = NULL; } + rb_native_mutex_unlock(&th->interrupt_lock); } static void