CVE-2024-4947 / Chrome Zero-Day

CVE-2024-4947: Chrome V8 JIT Compiler Type Confusion Teardown

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.

💡 Plain English Explainer (ELI5)

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.

Core Concepts & Subsystem Terms

Turbofan JIT
The optimizing compiler in Google's V8 engine that converts JavaScript bytecode into high-speed architecture-specific machine code.
Hidden Class / Map
V8's internal metadata object tracking the layout, property names, and element types of JavaScript objects in memory.
Type Confusion
A memory corruption flaw occurring when code treats a memory region initialized as Type A as if it were Type B.
Map Deprecating Transition
When dynamic modification of an object property causes its internal layout to mutate, requiring previously compiled JIT code to deoptimize.

Step-by-Step Exploit Mechanics

Step 1

1. JIT Warm-Up

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.

Step 2

2. Prototype Getter Interception

Inside a customized property getter, the attacker alters the array layout by storing an object reference, changing the map to `PACKED_ELEMENTS`.

Step 3

3. Unchecked Native Execution

Because Turbofan omitted a dynamic map transition check, the compiled native loop continues to read the array as raw 64-bit IEEE floats.

Step 4

4. Arbitrary Read/Write Primitive

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.

Source Code: Fatal Flaw vs. High-Level Fix

Delivered in clean, readable high-level source code (no raw assembly or binary diffs).

UNPATCHED FLAW
// 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();
}
HARDENED PATCH
// 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();
}

Engineering & System Hardening Checklist

← Browse Full Security Directory Explore All Source Teardowns →