CVE-2026-79072
Overview
Background
- Heap snapshot verification
- debug-only code in
heap-snapshot-generator.cc, compiled under#ifdef V8_ENABLE_HEAP_SNAPSHOT_VERIFY, that cross-checks the edges a heap snapshot records against the references a GC marking visitor reports for the same object. - `HeapEntryVerifier`
- the verifier class. It collects a
reference_summary_of strong and weak references from the marking visitor and achecked_objects_set of targets the snapshot verified, then usesCHECK_NEto require that every summarized reference was checked. - Shared space / client isolate
- with shared heaps enabled (for example via
--shared-string-table), objects such as shared strings live in a writable shared space owned by a shared-space isolate. A client isolate’s marking visitor does not follow references into that space. - Ephemeron (`WeakMap`) visiting
- according to the commit message,
MarkingVisitorBase::VisitEphemeronHashTablecallsAddWeakReferenceForReferenceSummarizer()on an entry’s value before it bails out for shared objects on client isolates, so shared values still end up in the weak-reference summary.
Root Cause Analysis
The reference-verification routine, which the commit message calls VerifyReference(), already skipped targets in read-only space and, on client isolates, targets in writable shared space. Its enclosing function name is not visible in the hunk. Because of that skip, references into the shared heap were never added to checked_objects_. HeapEntryVerifier::CheckAllReferencesWereChecked applied only the read-only exclusion before its CHECK_NE, not the shared-space one. A WeakMap whose value is a shared object reproduces this: the regression test uses a string under --shared-string-table. The marking visitor places the shared value in weak_references(), the snapshot side skips it, and the final consistency CHECK fails, so the process aborts during a major-snapshot GC.
The fix moves both exclusions into one helper, ShouldVerifyReferenceTo, and uses it in both places, so the two sides now agree on which references can be verified.
The patch loosens the verifier’s final check: it skips more objects. It does not change any runtime security check.
CHECK failure. The fix puts the skip rule in one place, ShouldVerifyReferenceTo, and both halves now use it.Attack Path
- Shared heap active
V8 runs with a shared space and a client isolate, which the test sets up with the non-default
--shared-string-tableflag. - WeakMap with shared value
script stores a value that lives in the shared heap, such as a shared string, in a
WeakMapkeyed by an ordinary heap object. - Heap snapshot taken
a heap snapshot is generated in a build with
V8_ENABLE_HEAP_SNAPSHOT_VERIFYenabled. The test does this withgc({type: 'major-snapshot'})under--expose-gc; in a browser it would correspond to a DevTools memory snapshot (inferred). - Verifier mismatch aborts
the ephemeron visitor records the shared value as a weak reference, the snapshot side skips it, and the
CHECK_NEinCheckAllReferencesWereCheckedfails. The process crashes with a controlledCHECKabort.
Impact Assessment
CHECK failure that crashes the process. That is a denial of service in the process taking the snapshot, for example a renderer being profiled (inferred). No memory corruption is involved. The vendor advisory rates this high severity and does not report in-the-wild exploitation; NVD scores it CVSS 8.1. Several preconditions limit exposure: the code exists only in builds compiled with V8_ENABLE_HEAP_SNAPSHOT_VERIFY (normally debug/verification builds, not release; inferred), shared-heap features must be enabled, and a heap snapshot must be requested, which ordinary web content cannot do on its own.Changed Functions
| Function | Change | Notes |
|---|---|---|
HeapEntryVerifiersrc/profiler/heap-snapshot-generator.cc |
modified |
Files Changed
test/mjsunit/shared-memory/regress/regress-528397177.jssrc/profiler/heap-snapshot-generator.cc
Audit Directions
- Paired skip rules in verifierswherever a producer and a consumer must agree on which objects to ignore (snapshot vs. marking visitor, serializer vs. deserializer), check that the filters are shared code and not duplicated conditions that can drift apart.
- Visitor side effects before bail-outslook for marking or visiting code such as
VisitEphemeronHashTablethat records, summarizes or publishes an object before an early return for shared or read-only objects. Downstream consumers may assume the bail-out happened first. - Shared-heap special cases in V8 toolingaudit profiler, heap-statistics and debug-verification paths for client-isolate handling of
InWritableSharedSpace()objects. These paths are tested less often with--shared-string-tableand similar flags.
Patch
From 6081a378d25f70dc1ca154d833955b24c1962377 Mon Sep 17 00:00:00 2001 From: Dominik Inführ <[email protected]> Date: Thu, 02 Jul 2026 12:35:32 +0200 Subject: [PATCH] [profiler] Skip shared objects in client isolates during verification On client isolates in VerifyReference() we skip references into the shared heap. However, we didn't then skip them in CheckAllReferencesWereChecked(). In MarkingVisitorBase::VisitEphemeronHashTable we invoke AddWeakReferenceForReferenceSummarizer() for the value unconditionally before bailing out for shared objects on client isolates. Bug: 528397177 Change-Id: I6593c2d3dfe146e2241d03ee04ec7cb957880d15 Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8025611 Commit-Queue: Michael Lippautz <[email protected]> Auto-Submit: Dominik Inführ <[email protected]> Reviewed-by: Michael Lippautz <[email protected]> Cr-Commit-Position: refs/heads/main@{#108392} --- diff --git a/src/profiler/heap-snapshot-generator.cc b/src/profiler/heap-snapshot-generator.cc index d4a5161..cd9a885 100644 --- a/src/profiler/heap-snapshot-generator.cc +++ b/src/profiler/heap-snapshot-generator.cc @@ -68,6 +68,20 @@ namespace v8::internal { #ifdef V8_ENABLE_HEAP_SNAPSHOT_VERIFY +bool ShouldVerifyReferenceTo(Isolate* isolate, Tagged<HeapObject> obj) { + // We can't verify pointers into read-only space, because marking visitors + // might not mark those. For example, every Map has a pointer to the MetaMap, + // but marking visitors don't bother with following that link. + if (MemoryChunk::FromHeapObject(obj)->InReadOnlySpace()) return false; + // For client isolates we cannot verify references into the shared heap + // because the marking visitor doesn't follow such references. + if (isolate->has_shared_space() && !isolate->is_shared_space_isolate() && + MemoryChunk::FromHeapObject(obj)->InWritableSharedSpace()) { + return false; + } + return true; +} + class HeapEntryVerifier { public: HeapEntryVerifier(HeapSnapshotGenerator* generator, Tagged<HeapObject> obj) @@ -142,17 +156,15 @@ // This ensures that there aren't retaining relationships found by the marking // visitor which were omitted from the heap snapshot. void CheckAllReferencesWereChecked() { - // Both loops below skip pointers to read-only objects, because the heap - // snapshot deliberately omits many of those (see IsEssentialObject). - // Read-only objects can't ever retain normal read-write objects, so these - // are fine to skip. + Isolate* isolate = generator_->heap()->isolate(); + for (Tagged<HeapObject> obj : reference_summary_.strong_references()) { - if (!MemoryChunk::FromHeapObject(obj)->InReadOnlySpace()) { + if (ShouldVerifyReferenceTo(isolate, obj)) { CHECK_NE(checked_objects_.find(obj), checked_objects_.end()); } } for (Tagged<HeapObject> obj : reference_summary_.weak_references()) { - if (!MemoryChunk::FromHeapObject(obj)->InReadOnlySpace()) { + if (ShouldVerifyReferenceTo(isolate, obj)) { CHECK_NE(checked_objects_.find(obj), checked_objects_.end()); } } @@ -329,17 +341,10 @@ } Tagged<HeapObject> from_obj = Cast<HeapObject>(Tagged<Object>(from_address)); Tagged<HeapObject> to_obj = Cast<HeapObject>(Tagged<Object>(to_address)); - if (MemoryChunk::FromHeapObject(to_obj)->InReadOnlySpace() || - (!isolate()->is_shared_space_isolate() && - MemoryChunk::FromHeapObject(to_obj)->InWritableSharedSpace())) { + if (!ShouldVerifyReferenceTo(isolate(), to_obj)) { // We can't verify pointers into read-only space or shared space (for client - // isolates), because marking visitors might not mark those. For example, - // every Map has a pointer to the MetaMap, but marking visitors don't bother - // with following that link. Read-only and shared objects are immortal (or - // managed externally) and can never point to things outside of their - // respective spaces, so ignoring these objects is safe from the perspective - // of ensuring accurate retaining paths for normal read-write objects. - // Therefore, do nothing. + // isolates), because marking visitors might not mark those. See + // ShouldVerifyReferenceTo for more details. Therefore, do nothing. } else if (verification == kEphemeron) { // Ephemerons can't be verified because they aren't marked directly by the // marking visitor. diff --git a/test/mjsunit/shared-memory/regress/regress-528397177.js b/test/mjsunit/shared-memory/regress/regress-528397177.js new file mode 100644 index 0000000..e0f2c0d --- /dev/null +++ b/test/mjsunit/shared-memory/regress/regress-528397177.js @@ -0,0 +1,13 @@ +// 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: --expose-gc --shared-string-table --cache + +const value = "some value"; +const map = new WeakMap(); +const key = () => {}; +map.set(key, value); +gc({ + type: 'major-snapshot', +});
Regression Test / PoC
diff --git a/test/mjsunit/shared-memory/regress/regress-528397177.js b/test/mjsunit/shared-memory/regress/regress-528397177.js
new file mode 100644
index 0000000..e0f2c0d
--- /dev/null
+++ b/test/mjsunit/shared-memory/regress/regress-528397177.js
@@ -0,0 +1,13 @@
+// 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: --expose-gc --shared-string-table --cache
+
+const value = "some value";
+const map = new WeakMap();
+const key = () => {};
+map.set(key, value);
+gc({
+ type: 'major-snapshot',
+});
Original Bug Report
CHECK failure: checked_objects_.find(obj) != checked_objects_.end() in heap-snapshot-generator.
Detailed Report: https://clusterfuzz.com/testcase?key=4967680113147904
Fuzzer: ochang_js_fuzzer_test Job Type: linux32_asan_d8_dbg Platform Id: linux
Crash Type: CHECK failure Crash Address: Crash State: checked_objects_.find(obj) != checked_objects_.end() in heap-snapshot-generator. v8::internal::HeapEntryVerifier::CheckAllReferencesWereChecked v8::internal::HeapEntryVerifier::~HeapEntryVerifier
Sanitizer: address (ASAN)
Regressed: https://clusterfuzz.com/revisions?job=linux32_asan_d8_dbg&range=107629:107630
Reproducer Testcase: https://clusterfuzz.com/download?testcase_id=4967680113147904
Issue filed automatically.
To reproduce this, please build the target in this report and run it against the reproducer testcase. Please use the GN arguments provided at bottom of this report when building the binary.
If you have trouble reproducing, please also export the environment variables listed under “[Environment]” in the crash stacktrace.
If you have any feedback on reproducing test cases, let us know at https://forms.gle/Yh3qCYFveHj6E5jz5 so we can improve.