How property getter transitions in V8's Turbofan JIT compiler tricked optimized native code into reading raw pointers as floating-point numbers, yielding in-the-wild remote code execution.
V8 makes JavaScript super fast by making assumptions. If you pass an array of numbers to a function 1,000 times, V8 turns that function into machine code that skips all safety checks. An attacker creates a sneaky property that changes the array from 'numbers' to 'object pointers' right in the middle of execution. The machine code treats an object's memory address as a number, letting the attacker read and write raw computer memory.
The attacker runs a loop passing an array of floating-point numbers (`PACKED_DOUBLE_ELEMENTS`) to a function until Turbofan compiles it to unverified native assembly.
Inside a customized property getter, the attacker alters the array layout by storing an object reference, changing the map to `PACKED_ELEMENTS`.
Because Turbofan omitted a dynamic map transition check, the compiled native loop continues to read the array as raw 64-bit IEEE floats.
The float values represent raw pointers. The attacker constructs an `addrof` (read address) and `fakeobj` (corrupt address) primitive to break out of the V8 sandbox.
Delivered in clean, readable high-level source code (no raw assembly or binary diffs).
// VULNERABLE: Simplified C++ Turbofan Graph Optimization (v8/src/compiler/)
// The optimizer assumed Map state remained unchanged across property accesses!
Reduction JSNativeContextSpecialization::ReduceNamedAccess(Node* node) {
MapRef receiver_map = GetReceiverMap(node);
// VULNERABILITY:
// If property access invokes a custom JS getter that mutates the object's Map,
// Turbofan fails to emit an effect-dependent map verification node!
if (receiver_map.is_stable()) {
// Relies on static stability without guarding against dynamic in-getter mutation
return BuildPropertyLoad(node, receiver_map);
}
return NoChange();
}
// SECURE: Explicit Map Transition Guard Insertion in Turbofan Graph
Reduction JSNativeContextSpecialization::ReduceNamedAccess(Node* node) {
MapRef receiver_map = GetReceiverMap(node);
// FIX: Verify map stability AND emit runtime type transition guard
if (receiver_map.is_stable()) {
dependencies()->DependOnStableMap(receiver_map);
// Explicitly insert dynamic Map check node before unboxing properties
Node* effect = NodeProperties::GetEffectInput(node);
Node* check = graph()->NewNode(
simplified()->CheckMaps(CheckMapsFlag::kNone,
ZoneHandleSet<Map>(receiver_map.object())),
receiver, effect, control);
// If an in-flight getter changes the object layout, force JIT deoptimization!
NodeProperties::ReplaceEffectInput(node, check);
return BuildPropertyLoad(node, check, receiver_map);
}
return NoChange();
}