Project

General

Profile

Actions

Bug #19348

closed

GVL being released earlier than expected when loading iseqs

Added by st0012 (Stan Lo) over 1 year ago. Updated about 1 year ago.

Status:
Closed
Assignee:
-
Target version:
-
[ruby-core:111849]

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:

  1. During the Rails app's boot time, it uses bootsnap to load iseqs, which uses the ibf_load_iseq_each function underneath.
  2. After commit e35c528d721d209ed8531b10b46c2ac725ea7bf5 (added in 3.2), that function starts calling rb_vm_pop_frame at the end of execution.
  3. 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.
  4. Now, if the debug gem calls ObjectSpace.each_iseq to activate a LineBreakpoint from its own thread, it'd gain access to those unready iseqs and try to read their state, which would then cause the uninitialized InstructionSequence error.
Actions #1

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
Actions #2

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:

  1. debug gem calling ObjectSpace.each_iseq (e.g. activating a LineBreakpoint).
  2. A large amount of iseq being loaded from another thread (possibly through the bootsnap gem).
  3. 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

Updated by naruse (Yui NARUSE) about 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.

Actions

Also available in: Atom PDF

Like0
Like0Like0Like0