CVE-2026-87612
Overview
Background
- Snapshot deserialization
- the process by which V8 reconstructs heap objects from a serialized byte stream, such as a code cache produced with
--cache=after-execute. - Reference descriptor
- transient deserializer state (
next_reference_is_weak_,next_reference_is_indirect_pointer_,next_reference_is_protected_pointer) recording how the next pointer slot should be written (strong, weak, indirect, or protected). - `RootArrayConstants`
- a snapshot bytecode that writes a slot by referencing a well-known object from the root table by index, avoiding a full encoded object.
- `WriteHeapPointer`
- the deserializer helper that writes a heap pointer into a slot honoring a
HeapObjectReferenceTypedescriptor and consuming (resetting) the pending reference state.
Root Cause Analysis
When the deserializer encountered a RootArrayConstants bytecode, Deserializer<IsolateT>::ReadRootArrayConstants unconditionally wrote the root object as a STRONG reference via slot_accessor.Write(heap_object, HeapObjectReferenceType::STRONG, ...), ignoring any pending reference descriptor set by a preceding weak/indirect/protected marker in the stream. Because the pending state was neither consulted nor reset, a slot that the serializer intended to hold a weak (or indirect/protected) reference was materialized as a strong direct pointer, and the stale descriptor could then bleed into the next slot write. This produced a type/tagging confusion where a slot’s actual bit representation no longer matched what surrounding code expected, violating the invariant that every deserialized slot is written exactly according to its declared reference kind.
The fix routes the write through WriteHeapPointer(slot_accessor, heap_object, GetAndResetNextReferenceDescriptor(), SKIP_WRITE_BARRIER), which applies the correct reference type and atomically consumes and clears the pending descriptor. Added DCHECKs in ReadRepeatedRoot further assert that no descriptor is left dangling at repeat-root boundaries.
ReadRootArrayConstants hardcoding a STRONG write instead of honoring and resetting the pending reference descriptor; the fix makes it write through WriteHeapPointer with GetAndResetNextReferenceDescriptor() so the slot’s reference kind is correct and the transient state cannot leak.Attack Path
- Craft a poisoned code cache
Produce or influence a serialized snapshot (e.g. via
--cache=after-execute) in which aRootArrayConstantsslot is preceded by a weak/indirect/protected reference marker. - Trigger deserialization
Cause V8 to deserialize the cache, reaching
ReadRootArrayConstants, which writes the root object asSTRONGand leaves the pending descriptor set. - Induce tag confusion The mismatched or leaked descriptor causes a slot to hold a pointer whose reference kind differs from what the engine assumes, or corrupts the following slot write.
- Confuse object type Subsequent code dereferences the mis-tagged slot as a different type than its true contents, yielding a type confusion primitive in the heap.
- Escalate Leverage the type confusion to read/write out of bounds or forge object fields, working toward controlled memory corruption in the renderer.
Impact Assessment
Files Changed
src/snapshot/deserializer.cctest/mjsunit/regress/regress-554236352.js
Audit Directions
- Unconsumed reference descriptorsAudit every deserializer bytecode handler that writes a slot to confirm it consumes and resets the pending reference descriptor via
GetAndResetNextReferenceDescriptorrather than hardcoding aHeapObjectReferenceType. - Hardcoded reference kindsSearch for direct
slot_accessor.Write(..., HeapObjectReferenceType::STRONG, ...)calls that ignore weak/indirect/protected markers and could mis-tag slots. - State leakage across handlersReview paths where transient deserializer flags can persist between consecutive slot writes, and add
DCHECKs at handler boundaries to assert they start clean.
Patch
From fbf1d9e1fdc72b2dca3a482b2658866fa4724815 Mon Sep 17 00:00:00 2001
From: Igor Sheludko <ishell@chromium.org>
Date: Wed, 02 Sep 2026 11:37:44 +0200
Subject: [PATCH] [snapshot] Reset weak reference descriptor in ReadRootArrayConstants
This CL updates ReadRootArrayConstants to write through WriteHeapPointer
with GetAndResetNextReferenceDescriptor(), ensuring the reference
descriptor is properly consumed and reset.
TAG=agy
CONV=6a397150-d53d-4615-b1b8-ce8c94c1a9dd
Fixed: 554236352
Change-Id: Idab7c9703b4d1210f8fd77564016bdc501543411
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8346067
Commit-Queue: Igor Sheludko <ishell@chromium.org>
Reviewed-by: Leszek Swirski <leszeks@chromium.org>
Cr-Commit-Position: refs/heads/main@{#109637}
---
diff --git a/src/snapshot/deserializer.cc b/src/snapshot/deserializer.cc
index fee3fa2..9db502d 100644
--- a/src/snapshot/deserializer.cc
+++ b/src/snapshot/deserializer.cc
@@ -943,6 +943,9 @@
template <typename SlotAccessor>
int Deserializer<IsolateT>::ReadRepeatedRoot(SlotAccessor slot_accessor,
int repeat_count) {
+ DCHECK(!next_reference_is_weak_);
+ DCHECK(!next_reference_is_indirect_pointer_);
+ DCHECK(!next_reference_is_protected_pointer);
CHECK_LE(2, repeat_count);
uint8_t id = source_.Get();
@@ -1602,8 +1605,9 @@
PrintF("%*sRootArrayConstants [%u] : %s\n", depth_, "",
static_cast<int>(root_index), RootsTable::name(root_index));
}
- return slot_accessor.Write(heap_object, HeapObjectReferenceType::STRONG, 0,
- SKIP_WRITE_BARRIER);
+ return WriteHeapPointer(slot_accessor, heap_object,
+ GetAndResetNextReferenceDescriptor(),
+ SKIP_WRITE_BARRIER);
}
template <typename IsolateT>
diff --git a/test/mjsunit/regress/regress-554236352.js b/test/mjsunit/regress/regress-554236352.js
new file mode 100644
index 0000000..63faeff
--- /dev/null
+++ b/test/mjsunit/regress/regress-554236352.js
@@ -0,0 +1,9 @@
+// Copyright 2026 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Flags: --cache=after-execute
+
+var R = Array.of(() => { 'use strict'; return eval('1'); }, "hello_world");
+assertEquals("hello_world", R[1]);
+assertEquals(1, R[0]());
Regression Test / PoC
diff --git a/test/mjsunit/regress/regress-554236352.js b/test/mjsunit/regress/regress-554236352.js
new file mode 100644
index 0000000..63faeff
--- /dev/null
+++ b/test/mjsunit/regress/regress-554236352.js
@@ -0,0 +1,9 @@
+// Copyright 2026 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Flags: --cache=after-execute
+
+var R = Array.of(() => { 'use strict'; return eval('1'); }, "hello_world");
+assertEquals("hello_world", R[1]);
+assertEquals(1, R[0]());