mirror of
https://github.com/ruby/ruby.git
synced 2026-10-29 08:55:35 +08:00
Multiple YJIT functions created overlapping `&'static mut IseqPayload`
references by calling `get_iseq_payload()` multiple times for the same
iseq. LLVM's noalias optimizations could then cache stale version_map
Vec headers, causing the version_map to point to freed backing storage.
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.
3. `invalidate_block_version` (core.rs): Called `remove_block_version()`
(which calls `get_iseq_payload()`) then `delayed_deallocation()`
(which also calls `get_iseq_payload()`), creating two references.
Fix: get the payload once, inline the version_map removal, and call
`payload.delayed_deallocation()` method for the rest.
Also adds regression tests exercising tracing invalidation with
on-stack methods and suspended fibers.
(cherry picked from commit a6fcad276a)