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
bootsnapto load iseqs, which uses theibf_load_iseq_eachfunction underneath. - After commit e35c528d721d209ed8531b10b46c2ac725ea7bf5 (added in 3.2), that function starts calling
rb_vm_pop_frameat the end of execution. - Because
rb_vm_pop_frametriggers 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
debuggem callsObjectSpace.each_iseqto activate aLineBreakpointfrom its own thread, it'd gain access to those unready iseqs and try to read their state, which would then cause theuninitialized InstructionSequenceerror.
Updated by peterzhu2118 (Peter Zhu) almost 3 years 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 almost 3 years 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:
-
debuggem callingObjectSpace.each_iseq(e.g. activating aLineBreakpoint). - A large amount of iseq being loaded from another thread (possibly through the
bootsnapgem). - 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) almost 3 years 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.