diff options
| author | Luke Gruber <luke.gruber@shopify.com> | 2026-03-10 16:13:38 -0400 |
|---|---|---|
| committer | Luke Gruber <luke.gru@gmail.com> | 2026-03-11 09:24:18 -0400 |
| commit | d72a0fed6019e7e6b2453594babb89f406aed1d7 (patch) | |
| tree | ed3f50b18ec6f31d6026ed1d94cfd5a9ba4ea742 | |
| parent | 44b99d688be701088e613c5495ea871d14738e29 (diff) | |
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.
| -rw-r--r-- | thread_pthread.c | 12 |
1 files changed, 5 insertions, 7 deletions
diff --git a/thread_pthread.c b/thread_pthread.c index 38bc134fa8..9f6707ae5d 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 |
