summaryrefslogtreecommitdiff
path: root/bootstraptest
diff options
context:
space:
mode:
authorRandy Stauner <randy@r4s6.net>2026-02-20 09:39:11 -0700
committerGitHub <noreply@github.com>2026-02-20 11:39:11 -0500
commite730ac41be4d427f06540e0f67fa16bbdced4789 (patch)
treeff57155873c727713532635b78cfd4d030759bab /bootstraptest
parent906176adb49abcd22bb1da082744ea9e413b96d9 (diff)
YJIT: Fix version_map use-after-free from mutable aliasing UB
Multiple YJIT functions created overlapping `&'static mut IseqPayload` references by calling `get_iseq_payload()` multiple times for the same iseq. Overlapping &mut is UB in rust's aliasing model, and as consequence, we trigered use-after-free on the `version_map` Vec header due to false claims of LLVM `noalias`. This manifested as crashes in various YJIT operations (block lookup, GC marking, block removal) that dereference the stale pointer. Fix by moving `delayed_deallocation` and `get_or_create_version_list` from free functions (which each call `get_iseq_payload()` internally) to methods on `IseqPayload` that operate through `&mut self`. This lets callers obtain a single payload reference and use it for all operations without creating overlapping mutable borrows. The three fixed call sites: 1. `rb_yjit_tracing_invalidate_all` (invariants.rs): The loop called `delayed_deallocation()` which internally called `get_iseq_payload()`, creating a second `&mut` overlapping with the outer `payload` reference. Fix: call `payload.delayed_deallocation()` method instead. 2. `add_block_version` (core.rs): Called `get_or_create_version_list()` then later `get_iseq_payload()` for pages, creating two references. Fix: use a single `get_or_create_iseq_payload()` call then call the `get_or_create_version_list()` method on it for both version_map and pages access. Also adds regression tests exercising tracing invalidation with on-stack methods and suspended fibers. [alan: edited commit message] Reviewed-by: Alan Wu <alanwu@ruby-lang.org>
Diffstat (limited to 'bootstraptest')
-rw-r--r--bootstraptest/test_yjit.rb49
1 files changed, 49 insertions, 0 deletions
diff --git a/bootstraptest/test_yjit.rb b/bootstraptest/test_yjit.rb
index cc7d9f1aeb..f19629ec0e 100644
--- a/bootstraptest/test_yjit.rb
+++ b/bootstraptest/test_yjit.rb
@@ -5486,3 +5486,52 @@ assert_normal_exit %q{
test
test
}
+
+# regression test for tracing invalidation with on-stack compiled methods
+# Exercises the on_stack_iseqs path in rb_yjit_tracing_invalidate_all
+# where delayed deallocation must not create aliasing &mut references
+# to IseqPayload (use-after-free of version_map backing storage).
+assert_normal_exit %q{
+ def deep = 42
+ def mid = deep
+ def outer = mid
+
+ # Compile all three methods with YJIT
+ 10.times { outer }
+
+ # Enable tracing from within a call chain so that outer/mid/deep
+ # are on the stack when rb_yjit_tracing_invalidate_all runs.
+ # This triggers the on_stack_iseqs (delayed deallocation) path.
+ def deep
+ TracePoint.new(:line) {}.enable
+ 42
+ end
+
+ outer
+
+ # After invalidation, verify YJIT can recompile and run correctly
+ def deep = 42
+ 10.times { outer }
+}
+
+# regression test for tracing invalidation with on-stack fibers
+# Suspended fibers have iseqs on their stack that must survive invalidation.
+assert_equal '42', %q{
+ def compiled_method
+ Fiber.yield
+ 42
+ end
+
+ # Compile the method
+ 10.times { compiled_method rescue nil }
+
+ fiber = Fiber.new { compiled_method }
+ fiber.resume # suspends inside compiled_method — it's now on the fiber's stack
+
+ # Enable tracing while compiled_method is on the fiber's stack.
+ # This triggers rb_yjit_tracing_invalidate_all with on-stack iseqs.
+ TracePoint.new(:call) {}.enable
+
+ # Resume the fiber — compiled_method's iseq must still be valid
+ fiber.resume.to_s
+}