Bug #19348
closedGVL being released earlier than expected when loading iseqs
Description
When using the debug
gem in a Rails app with Ruby 3.2, I noticed that if the VS Code editor connects to the debugger during the app boot, this error could occur:
DEBUGGER: ReaderThreadError: uninitialized InstructionSequence
┃ DEBUGGER: Disconnected.
┃ ["/opt/rubies/ruby-3.2.0/lib/ruby/gems/3.2.0/gems/debug-1.7.1/lib/debug/breakpoint.rb:247:in `absolute_path'",
┃ "/opt/rubies/ruby-3.2.0/lib/ruby/gems/3.2.0/gems/debug-1.7.1/lib/debug/breakpoint.rb:247:in `block in iterate_iseq'",
┃ "/opt/rubies/ruby-3.2.0/lib/ruby/gems/3.2.0/gems/debug-1.7.1/lib/debug/breakpoint.rb:246:in `each_iseq'",
...
After investigating it with @peterzhu2118 (Peter Zhu), we found that it's because:
- During the Rails app's boot time, it uses
bootsnap
to load iseqs, which uses theibf_load_iseq_each
function underneath. - After commit e35c528d721d209ed8531b10b46c2ac725ea7bf5 (added in 3.2), that function starts calling
rb_vm_pop_frame
at the end of execution. - Because
rb_vm_pop_frame
triggers the release of GVL, iseqs that just being loaded now become accessible by other threads, even though they're not ready to be used. - Now, if the
debug
gem callsObjectSpace.each_iseq
to activate aLineBreakpoint
from its own thread, it'd gain access to those unready iseqs and try to read their state, which would then cause theuninitialized InstructionSequence
error.
Updated by peterzhu2118 (Peter Zhu) over 1 year ago
- Backport changed from 2.7: UNKNOWN, 3.0: UNKNOWN, 3.1: UNKNOWN, 3.2: UNKNOWN to 2.7: DONTNEED, 3.0: DONTNEED, 3.1: DONTNEED, 3.2: REQUIRED
Updated by Anonymous over 1 year ago
- Status changed from Open to Closed
Applied in changeset git|df6b72b8ff7af16a56fa48f3b4abb1d8850f4d1c.
Avoid checking interrupt when loading iseq
The interrupt check will unintentionally release the VM lock when loading an iseq.
And this will cause issues with the debug
gem's
ObjectSpace.each_iseq
method,
which wraps iseqs with a wrapper and exposes their internal states when they're actually not ready to be used.
And when that happens, errors like this would occur and kill the debug
gem's thread:
DEBUGGER: ReaderThreadError: uninitialized InstructionSequence
┃ DEBUGGER: Disconnected.
┃ ["/opt/rubies/ruby-3.2.0/lib/ruby/gems/3.2.0/gems/debug-1.7.1/lib/debug/breakpoint.rb:247:in `absolute_path'",
┃ "/opt/rubies/ruby-3.2.0/lib/ruby/gems/3.2.0/gems/debug-1.7.1/lib/debug/breakpoint.rb:247:in `block in iterate_iseq'",
┃ "/opt/rubies/ruby-3.2.0/lib/ruby/gems/3.2.0/gems/debug-1.7.1/lib/debug/breakpoint.rb:246:in `each_iseq'",
...
A way to reproduce the issue is to satisfy these conditions at the same time:
-
debug
gem callingObjectSpace.each_iseq
(e.g. activating aLineBreakpoint
). - A large amount of iseq being loaded from another thread (possibly through the
bootsnap
gem). - 1 and 2 iterating through the same iseq(s) at the same time.
Because this issue requires external dependencies and a rather complicated timing setup to reproduce, I wasn't able to write a test case for it.
But here's some pseudo code to help reproduce it:
require "debug/session"
Thread.new do
100.times do
ObjectSpace.each_iseq do |iseq|
iseq.absolute_path
end
end
end
sleep 0.1
load_a_bunch_of_iseq
possibly_through_bootsnap
[Bug #19348]
Co-authored-by: Peter Zhu peter@peterzhu.ca
Updated by naruse (Yui NARUSE) over 1 year ago
- Backport changed from 2.7: DONTNEED, 3.0: DONTNEED, 3.1: DONTNEED, 3.2: REQUIRED to 2.7: DONTNEED, 3.0: DONTNEED, 3.1: DONTNEED, 3.2: DONE
ruby_3_2 0090cb82b0bf477c29a659e34cf4427a3b1ceb27 merged revision(s) df6b72b8ff7af16a56fa48f3b4abb1d8850f4d1c.