Bug #22225
closedYJIT regenerates a branch while a duplicate target still points to the invalidated block
Description
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:
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:
# 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:
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:
[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.