Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 

README.md

CVE-2025-13223 -- V8 Property Array Extension Type Confusion (Chrome Renderer RCE)

Overview

CVE-2025-13223 is a type confusion in V8's optimizing compilers (Turbofan, Maglev, Turboshaft/Turbolev) affecting property array extension during map transitions. It was Chrome's 7th zero-day of 2025, listed in CISA KEV as actively exploited in the wild.

The Bug

Root Cause: AccessBuilder::ForPropertyArraySlot()

In src/compiler/access-builder.cc, the function ForPropertyArraySlot() generates FieldAccess descriptors for reading/writing properties stored in an object's out-of-object PropertyArray backing store.

Before the fix, this function used MachineType::AnyTagged() for ALL property slots, regardless of the property's actual Representation:

// VULNERABLE CODE (simplified)
FieldAccess AccessBuilder::ForPropertyArraySlot(int index) {
  // ...
  access.machine_type = MachineType::AnyTagged();  // BUG: always tagged
  return access;
}

However, V8 properties can have three different representations:

Representation Storage MachineType needed
Smi (small integer) Tagged pointer AnyTagged (ok)
HeapObject Tagged pointer TaggedPointer
Double Raw IEEE-754 double Float64

When a property has Representation::Double, it is stored as a raw 8-byte IEEE-754 value in the PropertyArray. Reading this with AnyTagged means the compiler-generated code interprets raw double bits as a tagged V8 pointer -- a classic type confusion.

The Fix

The fix (4cf93118) modifies ForPropertyArraySlot() to accept a Representation parameter and compute the correct MachineType:

// FIXED CODE (simplified)
FieldAccess AccessBuilder::ForPropertyArraySlot(
    int index, Representation representation) {
  // ...
  if (representation.IsDouble()) {
    access.machine_type = MachineType::Float64();
  } else if (representation.IsHeapObject()) {
    access.machine_type = MachineType::TaggedPointer();
  } else {
    access.machine_type = MachineType::AnyTagged();
  }
  return access;
}

The fix also updates all callers across 7 files:

  1. src/compiler/access-builder.cc -- Core fix
  2. src/compiler/access-builder.h -- Updated declaration
  3. src/compiler/js-native-context-specialization.cc -- Turbofan: walks descriptor array in lockstep with property array to pass correct Representation
  4. src/compiler/turboshaft/turbolev-early-lowering-reducer-inl.h -- Turboshaft: ExtendPropertiesBackingStore() now takes MapRef and iterates descriptors to find first out-of-object property
  5. src/compiler/turboshaft/turbolev-graph-builder.cc -- Passes old_map()
  6. src/maglev/maglev-graph-builder.cc -- Passes old_map()
  7. src/maglev/maglev-ir.h -- ExtendPropertiesBackingStore node stores old_map_ member for descriptor iteration

Why This is Exploitable

The type confusion between Double and Tagged representations means:

  1. addrOf: Store a JS object in a HeapObject property slot, but the compiler reads it as a Double. The raw tagged pointer bits leak as a floating-point value.

  2. fakeObj: Write a crafted double value (encoding a fake pointer) into what the compiler thinks is a Tagged slot. V8 then follows this "pointer" to attacker-controlled memory.

These two primitives are sufficient for full renderer RCE when combined with sandbox escape techniques.

Exploit Chain

Stage 1: Trigger the Type Confusion

Create objects with mixed property representations where some out-of-object properties are stored as doubles. Force property array extension via map transitions (adding new named properties). The spread operator {...x} with a __defineGetter__ callback creates the ideal conditions: the getter triggers massive map transitions that cause property array reallocation, and the compiler-generated copy code uses AnyTagged for double slots.

Stage 2: addrOf Primitive

Place a target JS object into an Object array (oobObjArr). Read the overlapping position through the corrupted double array (corrupted_arr). The raw tagged pointer bits are returned as a float64 value. Extract the compressed heap pointer using ftoi32().

Stage 3: Arbitrary V8 Heap R/W

Overwrite the backing store pointer of oobDblArr through the corrupted array to point at any V8 heap address. Then oobDblArr[0] reads/writes at that address.

Stage 4: V8 Sandbox Escape (DOMRect/AudioBuffer)

Confuse DOMRect with DOMArrayBuffer by swapping their Blink wrapper instance pointers. DOMRect.x then controls the raw (non-sandboxed) base address of an AudioBuffer channel. AudioBuffer.copyFromChannel() and copyToChannel() provide full process-wide arbitrary read/write outside the V8 sandbox.

Stage 5: Code Execution (WASM Dispatch Table)

Locate the WASM import dispatch table in the V8 trusted cage. Overwrite the code entry point for an imported function to redirect execution into shellcode embedded as IEEE-754 doubles in a WASM array's data section. Call the exported WASM function to trigger the shellcode.

The WASM module contains x86_64 Linux shellcode that calls:

execve("/bin/xcalc", ["DISPLAY=:0.0"], NULL)

Files

File Description
trigger.html Type confusion trigger only (no exploitation)
exploit.html Full exploit chain -- trigger through RCE
setup-chrome-13223.ps1 Windows setup (downloads Chrome 141, creates run script)
README.md This file

Setup

Windows

.\setup-chrome-13223.ps1
.\run-exploit.bat

Downloads Chrome 141.0.7438.0 for Testing (win64). The type confusion and all memory corruption primitives work. The shellcode is Linux x86_64 so the final stage will crash on Windows rather than executing xcalc, but this confirms the full chain up to code execution.

Linux x86_64

# Download a vulnerable Chrome for Testing build
# Chrome 141 is the last major version before the fix in 142.0.7444.175
python3 -m http.server 8098 &
google-chrome --no-sandbox --disable-gpu \
  --user-data-dir=/tmp/chrome-13223 \
  http://localhost:8098/exploit.html

Manual Testing (trigger only)

python3 -m http.server 8098 &
chrome --no-sandbox --js-flags="--turbofan --no-lazy" \
  http://localhost:8098/trigger.html

The --turbofan --no-lazy flags force eager Turbofan compilation which makes the type confusion more reliable.

Comparison with CVE-2024-5830

Aspect CVE-2024-5830 CVE-2025-13223
Bug class Object transition type confusion Property array extension type confusion
Root cause Map transition during spread ForPropertyArraySlot() ignores Representation
Trigger __defineGetter__ + spread Property array extension during optimization
Affected compiler V8 (runtime) Turbofan, Maglev, Turboshaft (all optimizers)
Sandbox escape DOMRect/AudioBuffer DOMRect/AudioBuffer (same technique)
Code exec WASM dispatch table WASM dispatch table (same technique)
Chrome fix 125.0.6422.113 142.0.7444.175

Platform Notes

  • Linux x86_64: Full RCE -- launches xcalc via execve.
  • Windows x86_64: All primitives work (type confusion, addrOf, R/W, sandbox escape). Shellcode crashes because it is Linux-specific. Replace the WASM module with Windows shellcode for full Windows RCE.
  • aarch64: Requires aarch64 shellcode replacement. The V8 bug and all JavaScript-level primitives are architecture-independent.

Hardcoded Addresses

The exploit contains V8 heap addresses specific to Chrome 141 Linux x86_64:

Constant Value Purpose
dblArrMap 0x25510d Map for PACKED_DOUBLE_ELEMENTS arrays
objArrMap 0x25518d Map for PACKED_ELEMENTS arrays
fakeDblArrayAddr 0x4881d Address of fake double array
nameAddr 0xdc5 NameDictionary shape address
trustedOffset 0x12e9d0 TrustedCage base - DOMRect type info
startAddr 0x40b00 WASM dispatch table search start

These may need adjustment for different Chrome builds or platforms. Use --js-flags="--allow-natives-syntax" and %DebugPrint() to find correct values for a specific build.