Issue #22225 has been updated by alanwu (Alan Wu). Status changed from Open to Feedback
That can leave generated code jumping back into code YJIT just invalidated.
This alone isn't problematic since `invalidate_block_version` patches in an exit at the invalidated block.
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.
This symptom sounds familiar to a bug we've fixed. Are you running into this issue with the latest release? ---------------------------------------- Bug #22225: YJIT regenerates a branch while a duplicate target still points to the invalidated block https://bugs.ruby-lang.org/issues/22225#change-118338 * Author: cosgroveb (Brian Cosgrove) * Status: Feedback * Assignee: jit * 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/