From: "cosgroveb (Brian Cosgrove) via ruby-core" Date: 2026-08-03T14:13:21+00:00 Subject: [ruby-core:126228] [Ruby Bug#22225] YJIT regenerates a branch while a duplicate target still points to the invalidated block Issue #22225 has been updated by cosgroveb (Brian Cosgrove). cosgroveb (Brian Cosgrove) wrote: > YJIT adds one incoming-list entry for each branch target that points to a block. When both targets point to the same block, that block's incoming list contains the same `BranchRef` twice. > > `invalidate_block_version` processes those entries separately. For the first entry, it redirects one target and immediately calls `regenerate_branch`. The other target still references > the invalidated block. > > The following assertion fails immediately before that call: > > ```patch > diff --git a/yjit/src/core.rs b/yjit/src/core.rs > index d08cd1fb26..4555081ae2 100644 > --- a/yjit/src/core.rs > +++ b/yjit/src/core.rs > @@ -4282,6 +4282,18 @@ pub fn invalidate_block_version(blockref: &BlockRef) { > } > } > > + assert!( > + branch.targets.iter().all(|target| unsafe { > + // SAFETY: no mutation. > + target > + .ref_unchecked() > + .as_ref() > + .and_then(|target| target.get_block()) > + != Some(*blockref) > + }), > + "regeneration started while a branch target still referenced the invalidated block" > + ); > + > // Rewrite the branch with the new jump target address > let old_branch_size = branch.code_size(); > regenerate_branch(cb, branch); > ``` > > This Ruby script triggers the assertion: > > ```ruby > # duplicate_target_invalidation.rb > module DuplicateTargetInvalidation > TARGET = 1 > > def self.call(condition) > if condition > 1 > else > 2 > end > > TARGET > end > end > > 3.times do |index| > DuplicateTargetInvalidation.call(index.even?) > end > > DuplicateTargetInvalidation.send(:remove_const, :TARGET) > > puts "completed" > ``` > > Save the patch as `assert-no-invalidated-target-before-regeneration.patch` and the script as `duplicate_target_invalidation.rb`. From CRuby `ruby 4.1.0dev`: > > ```sh > git apply assert-no-invalidated-target-before-regeneration.patch > ./autogen.sh > mkdir build > cd build > ../configure --enable-yjit=dev > make -j"$(nproc)" > ./ruby --yjit-call-threshold=1 ../duplicate_target_invalidation.rb > ``` > > The process aborts before printing `completed`: > > ```text > [BUG] YJIT: panicked at yjit/src/core.rs:4285:9: > regeneration started while a branch target still referenced the invalidated block > ``` > > > regenerate_branch runs with one target redirected to a stub and the other still pointing at the invalidated block. That can leave generated code jumping back into code YJIT just invalidated. Apologies for the rather in the weeds report here but: 1. Is this expected behavior? 2. This does result in an infinite loop/self-jump in a much more elaborate repro and in my CI environment (a large test suite > 100k examples often re-defining things. I wanted to keep my report laser-focused and reduced to the smallest example possible of the state that produces such conditions, in the hope that a succinct report aids maintainers in triage. ---------------------------------------- Bug #22225: YJIT regenerates a branch while a duplicate target still points to the invalidated block https://bugs.ruby-lang.org/issues/22225#change-118313 * Author: cosgroveb (Brian Cosgrove) * Status: Open * ruby -v: ruby 4.1.0dev (2026-08-03T03:11:21Z master f622e12503) +YJIT dev +PRISM [aarch64-linux] * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- YJIT adds one incoming-list entry for each branch target that points to a block. When both targets point to the same block, that block's incoming list contains the same `BranchRef` twice. `invalidate_block_version` processes those entries separately. For the first entry, it redirects one target and immediately calls `regenerate_branch`. The other target still references the invalidated block. The following assertion fails immediately before that call: ```patch diff --git a/yjit/src/core.rs b/yjit/src/core.rs index d08cd1fb26..4555081ae2 100644 --- a/yjit/src/core.rs +++ b/yjit/src/core.rs @@ -4282,6 +4282,18 @@ pub fn invalidate_block_version(blockref: &BlockRef) { } } + assert!( + branch.targets.iter().all(|target| unsafe { + // SAFETY: no mutation. + target + .ref_unchecked() + .as_ref() + .and_then(|target| target.get_block()) + != Some(*blockref) + }), + "regeneration started while a branch target still referenced the invalidated block" + ); + // Rewrite the branch with the new jump target address let old_branch_size = branch.code_size(); regenerate_branch(cb, branch); ``` This Ruby script triggers the assertion: ```ruby # duplicate_target_invalidation.rb module DuplicateTargetInvalidation TARGET = 1 def self.call(condition) if condition 1 else 2 end TARGET end end 3.times do |index| DuplicateTargetInvalidation.call(index.even?) end DuplicateTargetInvalidation.send(:remove_const, :TARGET) puts "completed" ``` Save the patch as `assert-no-invalidated-target-before-regeneration.patch` and the script as `duplicate_target_invalidation.rb`. From CRuby `ruby 4.1.0dev`: ```sh git apply assert-no-invalidated-target-before-regeneration.patch ./autogen.sh mkdir build cd build ../configure --enable-yjit=dev make -j"$(nproc)" ./ruby --yjit-call-threshold=1 ../duplicate_target_invalidation.rb ``` The process aborts before printing `completed`: ```text [BUG] YJIT: panicked at yjit/src/core.rs:4285:9: regeneration started while a branch target still referenced the invalidated block ``` regenerate_branch runs with one target redirected to a stub and the other still pointing at the invalidated block. That can leave generated code jumping back into code YJIT just invalidated. -- https://bugs.ruby-lang.org/ ______________________________________________ ruby-core mailing list -- ruby-core@ml.ruby-lang.org To unsubscribe send an email to ruby-core-leave@ml.ruby-lang.org ruby-core info -- https://ml.ruby-lang.org/mailman3/lists/ruby-core.ml.ruby-lang.org/