CVE-2026-14415
Overview
Background
- Heap snapshot generator
- the V8 profiler subsystem in
src/profiler/heap-snapshot-generator.ccthat walks the live heap and emits a graph ofHeapEntrynodes connected byHeapGraphEdgeedges for DevTools memory tooling. - Ephemeron
- a key/value pair in a
JSWeakMap/JSWeakSetbacked by anEphemeronHashTable, where the value is retained only while the key is reachable, modeled in the snapshot byCreateEphemeronEdges(). - Weak edge
- a
HeapGraphEdgeof typekWeakemitted bySetWeakReference()that names a slot which does not keep its target alive. - `MarkVisitedField`
- a generator helper that records a field offset as already handled so the generic reference-extraction pass does not auto-emit an edge for it.
Root Cause Analysis
For each ephemeron pair in an EphemeronHashTable, the pre-patch code in the pair-extraction loop called SetWeakReference(entry, key_index, key, ...) and SetWeakReference(entry, value_index, value, ...) to emit kWeak edges from the table to both the key and the value, and then additionally called CreateEphemeronEdges(table_entry, key_entry, value_entry) to emit the ephemeron edges. This produced redundant, overlapping edges: the table pointed at the key and value via both a weak edge and an ephemeron edge, which the commit message describes as “a bit confusing in the heap snapshot” and a duplicate of the ephemeron edges.
The fix replaces the two SetWeakReference calls with MarkVisitedField(table->OffsetOfElementAt(key_index)) and MarkVisitedField(table->OffsetOfElementAt(value_index)), suppressing the auto-generated table→key and table→value edges, and lets CreateEphemeronEdges() be the sole producer of pair edges. The emission guard is also restructured: the ephemeron edges are now created under if (key_entry && !IsUndefined(key)) with an inner if (value_entry), and when the verifier is enabled the table→key reference is recorded via MarkReferenceCheckedWithoutChecking() so verification does not flag the now-suppressed edge. This is a snapshot-fidelity/logic correction, not a memory-safety fix; no dereference or bounds change is shown in the diff.
kWeak edges and ephemeron edges for the same key/value slots, double-representing every JSWeakMap/JSWeakSet entry; the fix marks those slots visited so only CreateEphemeronEdges() describes the pair.Attack Path
- Take a heap snapshot
A page or DevTools client that can trigger
HeapProfiler::TakeHeapSnapshot()on a heap containing aJSWeakMaporJSWeakSetreaches the ephemeron-pair extraction loop shown in the hunk. - Duplicate edges emitted
For each pair the old code emits table→key and table→value
kWeakedges viaSetWeakReference()in addition to the ephemeron edges fromCreateEphemeronEdges(), so the resulting graph misrepresents the retention relationship. (advisory: “Inappropriate implementation in V8”) - Downstream consumption Any tooling or retained-size accounting that walks these duplicated edges observes an inaccurate/confusing graph; the diff demonstrates only the incorrect edge set, not a concrete memory-safety sink. (inferred)
Impact Assessment
JSWeakMap/JSWeakSet entries, producing a confusing and duplicated memory graph in the renderer/profiler context; the precondition is generating a heap snapshot of a heap containing such collections. The advisory classifies the bug as LogicError / “Inappropriate implementation in V8” and reports no memory-corruption primitive; the diff itself demonstrates only edge duplication, so any further consequence is (inferred) and not established by the patch. No exploited-in-the-wild status is stated in the advisory text provided.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/profiler/heap-snapshot-generator.cc |
modified | |
TEST_Ftest/unittests/profiler/heap-snapshot-unittest.cc |
modified |
Files Changed
src/profiler/heap-snapshot-generator.cctest/cctest/test-heap-profiler.cctest/unittests/profiler/heap-snapshot-unittest.cc
Audit Directions
- Double-emission of graph edgesAudit other
SetWeakReference/SetReferencesites inheap-snapshot-generator.ccthat also feed a specialized edge creator (e.g.CreateEphemeronEdges) for the same slot, and confirm the generic extractor is suppressed viaMarkVisitedField. - Verifier consistencyWherever an edge is intentionally suppressed, check that
V8_ENABLE_HEAP_SNAPSHOT_VERIFYpaths callMarkReferenceCheckedWithoutChecking()so verification does not diverge from emission. - Ephemeron/weak-collection extractionReview handling of
JSWeakMap,JSWeakSet, andEphemeronHashTableforIsUndefined(key)and null-entry guards to ensure edges are only created for live, resolvable key/value entries.
Patch
From 33ca8a4017b75d3c7e81f0f88760fe1871b016bf Mon Sep 17 00:00:00 2001
From: Dominik Inführ <dinfuehr@chromium.org>
Date: Thu, 21 May 2026 15:56:25 +0200
Subject: [PATCH] [profiler] Drop weak edges from JSWeakSet/JSWeakMap
For JSWeakMap (and JSWeakSet) we are emitting ephemeron edges for each
ephemeron pair: 1) table -> value 2) key -> value. In addition, we
also emitted weak edges from the table both to each key and each value.
In this CL we drop those weak edges since they were anyways a bit confusing in the heap snapshot and just duplicate the ephemeron edges.
Bug: 515086856
Change-Id: I09b9023a5e081da8b26621b0d3515c3661b5d409
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7867594
Reviewed-by: Michael Lippautz <mlippautz@chromium.org>
Commit-Queue: Dominik Inführ <dinfuehr@chromium.org>
Cr-Commit-Position: refs/heads/main@{#107511}
---
diff --git a/src/profiler/heap-snapshot-generator.cc b/src/profiler/heap-snapshot-generator.cc
index d910e4b..a4b363f 100644
--- a/src/profiler/heap-snapshot-generator.cc
+++ b/src/profiler/heap-snapshot-generator.cc
@@ -1768,15 +1768,23 @@
int value_index = EphemeronHashTable::EntryToValueIndex(i);
Tagged<Object> key = table->get(key_index);
Tagged<Object> value = table->get(value_index);
- SetWeakReference(entry, key_index, key,
- table->OffsetOfElementAt(key_index));
- SetWeakReference(entry, value_index, value,
- table->OffsetOfElementAt(value_index));
+ // This stops auto-generating edges for the key and value from the table.
+ // All edges are create by CreateEphmeronEdges() below.
+ MarkVisitedField(table->OffsetOfElementAt(key_index));
+ MarkVisitedField(table->OffsetOfElementAt(value_index));
HeapEntry* key_entry = GetEntry(key);
HeapEntry* value_entry = GetEntry(value);
HeapEntry* table_entry = GetEntry(table);
- if (key_entry && value_entry && !IsUndefined(key)) {
- generator_->CreateEphemeronEdges(table_entry, key_entry, value_entry);
+ if (key_entry && !IsUndefined(key)) {
+#ifdef V8_ENABLE_HEAP_SNAPSHOT_VERIFY
+ if (generator_->verifier() != nullptr) {
+ generator_->verifier()->MarkReferenceCheckedWithoutChecking(
+ table, Cast<HeapObject>(key));
+ }
+#endif
+ if (value_entry) {
+ generator_->CreateEphemeronEdges(table_entry, key_entry, value_entry);
+ }
}
}
}
diff --git a/test/cctest/test-heap-profiler.cc b/test/cctest/test-heap-profiler.cc
index 479dc52..ce63e3e 100644
--- a/test/cctest/test-heap-profiler.cc
+++ b/test/cctest/test-heap-profiler.cc
@@ -31,6 +31,7 @@
#include <memory>
#include <optional>
+#include <string>
#include <vector>
#include "include/v8-function.h"
@@ -222,6 +223,28 @@
return nullptr;
}
+const v8::HeapGraphEdge* FindEphemeronEdge(v8::Isolate* isolate,
+ const v8::HeapGraphNode* table,
+ const v8::HeapGraphNode* key) {
+ v8::String::Utf8Value key_name(isolate, key->GetName());
+ std::string prefix = "part of key (" + std::string(*key_name) + " @" +
+ std::to_string(key->GetId()) + ") -> value (";
+ std::string suffix =
+ ") pair in WeakMap (table @" + std::to_string(table->GetId()) + ")";
+
+ for (int i = 0, count = table->GetChildrenCount(); i < count; ++i) {
+ const v8::HeapGraphEdge* prop = table->GetChild(i);
+ if (prop->GetType() != v8::HeapGraphEdge::kInternal) continue;
+ v8::String::Utf8Value name(isolate, prop->GetName());
+ std::string name_str(*name);
+ if (name_str.find(prefix) != std::string::npos &&
+ name_str.find(suffix) != std::string::npos) {
+ return prop;
+ }
+ }
+ return nullptr;
+}
+
// The following functions are not Wasm-specific, but are only used in a
// Wasm-specific test. As long as this is the case we only define them if Wasm
// is enabled to avoid warnings about unused functions.
@@ -933,16 +956,19 @@
const v8::HeapGraphNode* ws_table =
GetProperty(env.isolate(), ws, v8::HeapGraphEdge::kInternal, "table");
CHECK_EQ(v8::HeapGraphNode::kArray, ws_table->GetType());
- CHECK_GT(ws_table->GetChildrenCount(), 0);
- int weak_entries = 0;
+ CHECK_EQ(3, ws_table->GetChildrenCount());
+ // Verify that WeakSet table has NO edges to k and v.
for (int i = 0, count = ws_table->GetChildrenCount(); i < count; ++i) {
const v8::HeapGraphEdge* prop = ws_table->GetChild(i);
- if (prop->GetType() != v8::HeapGraphEdge::kWeak) continue;
- if (k->GetId() == prop->GetToNode()->GetId()) {
- ++weak_entries;
- }
+ CHECK_NE(k->GetId(), prop->GetToNode()->GetId());
}
- CHECK_EQ(1, weak_entries);
+ const v8::HeapGraphEdge* edge_k =
+ FindEphemeronEdge(env.isolate(), ws_table, k);
+ const v8::HeapGraphEdge* edge_v =
+ FindEphemeronEdge(env.isolate(), ws_table, v);
+ CHECK_NOT_NULL(edge_k);
+ CHECK_NOT_NULL(edge_v);
+ CHECK_EQ(edge_k->GetToNode()->GetId(), edge_v->GetToNode()->GetId());
const v8::HeapGraphNode* ws_s =
GetProperty(env.isolate(), ws, v8::HeapGraphEdge::kProperty, "str");
CHECK(ws_s);
@@ -958,16 +984,32 @@
GetProperty(env.isolate(), wm, v8::HeapGraphEdge::kInternal, "table");
CHECK_EQ(v8::HeapGraphNode::kArray, wm_table->GetType());
CHECK_GT(wm_table->GetChildrenCount(), 0);
- weak_entries = 0;
+ int wm_ephemeron_edges_to_v = 0;
for (int i = 0, count = wm_table->GetChildrenCount(); i < count; ++i) {
const v8::HeapGraphEdge* prop = wm_table->GetChild(i);
- if (prop->GetType() != v8::HeapGraphEdge::kWeak) continue;
- const v8::SnapshotObjectId to_node_id = prop->GetToNode()->GetId();
- if (to_node_id == k->GetId() || to_node_id == v->GetId()) {
- ++weak_entries;
+ // Verify NO edge to k.
+ CHECK_NE(k->GetId(), prop->GetToNode()->GetId());
+ if (prop->GetToNode()->GetId() == v->GetId()) {
+ CHECK_EQ(v8::HeapGraphEdge::kInternal, prop->GetType());
+ v8::String::Utf8Value prop_name(env.isolate(), prop->GetName());
+ CHECK_NOT_NULL(strstr(*prop_name, "pair in WeakMap"));
+ ++wm_ephemeron_edges_to_v;
}
}
- CHECK_EQ(2, weak_entries); // Key and value are weak.
+ CHECK_EQ(1, wm_ephemeron_edges_to_v);
+
+ // Verify that k has an ephemeron edge to v.
+ int k_ephemeron_edges_to_v = 0;
+ for (int i = 0, count = k->GetChildrenCount(); i < count; ++i) {
+ const v8::HeapGraphEdge* prop = k->GetChild(i);
+ if (prop->GetToNode()->GetId() == v->GetId()) {
+ CHECK_EQ(v8::HeapGraphEdge::kInternal, prop->GetType());
+ v8::String::Utf8Value prop_name(env.isolate(), prop->GetName());
+ CHECK_NOT_NULL(strstr(*prop_name, "pair in WeakMap"));
+ ++k_ephemeron_edges_to_v;
+ }
+ }
+ CHECK_EQ(1, k_ephemeron_edges_to_v);
const v8::HeapGraphNode* wm_s =
GetProperty(env.isolate(), wm, v8::HeapGraphEdge::kProperty, "str");
CHECK(wm_s);
diff --git a/test/unittests/profiler/heap-snapshot-unittest.cc b/test/unittests/profiler/heap-snapshot-unittest.cc
index 3c4494c..0d1e357 100644
--- a/test/unittests/profiler/heap-snapshot-unittest.cc
+++ b/test/unittests/profiler/heap-snapshot-unittest.cc
@@ -4,6 +4,8 @@
#include <json/json.h>
+#include "src/objects/hash-table-inl.h"
+#include "src/objects/js-collection-inl.h"
#include "src/profiler/heap-profiler.h"
#include "src/profiler/heap-snapshot-generator.h"
#include "test/unittests/profiler/heap-snapshot-utils.h"
@@ -484,4 +486,72 @@
EXPECT_STRNE("%s / %s", entry->name());
}
+TEST_F(HeapSnapshotTest, EphemeronHashTableEdges) {
+ HandleScope scope(i_isolate());
+
+ v8::Local<v8::Value> weak_map_val =
+ RunJS("const weak_map = new WeakMap(); weak_map");
+ v8::Local<v8::Value> key_val = RunJS("const key = {name: 'my_key'}; key");
+ v8::Local<v8::Value> val_val = RunJS("const val = {name: 'my_val'}; val");
+
+ Handle<JSWeakMap> weak_map =
+ Cast<JSWeakMap>(v8::Utils::OpenHandle(*weak_map_val));
+ Handle<JSObject> key = Cast<JSObject>(v8::Utils::OpenHandle(*key_val));
+ Handle<JSObject> val = Cast<JSObject>(v8::Utils::OpenHandle(*val_val));
+
+ RunJS("weak_map.set(key, val);");
+
+ HeapSnapshot* snapshot = TakeHeapSnapshot();
+
+ const HeapEntry* weak_map_entry =
+ GetEntryFor(i_isolate(), snapshot, *weak_map);
+ const HeapEntry* key_entry = GetEntryFor(i_isolate(), snapshot, *key);
+ const HeapEntry* val_entry = GetEntryFor(i_isolate(), snapshot, *val);
+
+ ASSERT_NE(nullptr, weak_map_entry);
Regression Test / PoC
diff --git a/test/unittests/profiler/heap-snapshot-unittest.cc b/test/unittests/profiler/heap-snapshot-unittest.cc
index 3c4494c..0d1e357 100644
--- a/test/unittests/profiler/heap-snapshot-unittest.cc
+++ b/test/unittests/profiler/heap-snapshot-unittest.cc
@@ -4,6 +4,8 @@
#include <json/json.h>
+#include "src/objects/hash-table-inl.h"
+#include "src/objects/js-collection-inl.h"
#include "src/profiler/heap-profiler.h"
#include "src/profiler/heap-snapshot-generator.h"
#include "test/unittests/profiler/heap-snapshot-utils.h"
@@ -484,4 +486,72 @@
EXPECT_STRNE("%s / %s", entry->name());
}
+TEST_F(HeapSnapshotTest, EphemeronHashTableEdges) {
+ HandleScope scope(i_isolate());
+
+ v8::Local<v8::Value> weak_map_val =
+ RunJS("const weak_map = new WeakMap(); weak_map");
+ v8::Local<v8::Value> key_val = RunJS("const key = {name: 'my_key'}; key");
+ v8::Local<v8::Value> val_val = RunJS("const val = {name: 'my_val'}; val");
+
+ Handle<JSWeakMap> weak_map =
+ Cast<JSWeakMap>(v8::Utils::OpenHandle(*weak_map_val));
+ Handle<JSObject> key = Cast<JSObject>(v8::Utils::OpenHandle(*key_val));
+ Handle<JSObject> val = Cast<JSObject>(v8::Utils::OpenHandle(*val_val));
+
+ RunJS("weak_map.set(key, val);");
+
+ HeapSnapshot* snapshot = TakeHeapSnapshot();
+
+ const HeapEntry* weak_map_entry =
+ GetEntryFor(i_isolate(), snapshot, *weak_map);
+ const HeapEntry* key_entry = GetEntryFor(i_isolate(), snapshot, *key);
+ const HeapEntry* val_entry = GetEntryFor(i_isolate(), snapshot, *val);
+
+ ASSERT_NE(nullptr, weak_map_entry);
+ ASSERT_NE(nullptr, key_entry);
+ ASSERT_NE(nullptr, val_entry);
+
+ // The WeakMap has 3 edges: map + proto + table.
+ EXPECT_EQ(3, weak_map_entry->children_count());
+ EXPECT_EQ(nullptr, FindFirstEdgeTo(*weak_map_entry, *key_entry));
+ EXPECT_EQ(nullptr, FindFirstEdgeTo(*weak_map_entry, *val_entry));
+
+ Tagged<EphemeronHashTable> table =
+ Cast<EphemeronHashTable>(weak_map->table());
+ const HeapEntry* table_entry = GetEntryFor(i_isolate(), snapshot, table);
+ ASSERT_NE(nullptr, table_entry);
+
+ // There should be exactly 2 edges: 1) the map and 2) the ephemeron edge from
+ // table -> value.
+ EXPECT_EQ(2, table_entry->children_count());
+
+ // Verify that there are NO edges from table to key.
+ const HeapGraphEdge* table_to_key_edge =
+ FindFirstEdgeTo(*table_entry, *key_entry);
+ EXPECT_EQ(nullptr, table_to_key_edge);
+
+ std::string expected_name =
+ " / part of key (" + std::string(key_entry->name()) + " @" +
+ std::to_string(key_entry->id()) + ") -> value (" +
+ std::string(val_entry->name()) + " @" + std::to_string(val_entry->id()) +
+ ") pair in WeakMap (table @" + std::to_string(table_entry->id()) + ")";
+
+ // Verify that the only edge from table to val is the ephemeron edge.
+ const HeapGraphEdge* table_to_val_edge =
+ FindFirstEdgeTo(*table_entry, *val_entry);
+ ASSERT_NE(nullptr, table_to_val_edge);
+ EXPECT_EQ(HeapGraphEdge::kInternal, table_to_val_edge->type());
+ EXPECT_TRUE(
+ std::string_view(table_to_val_edge->name()).ends_with(expected_name));
+
+ // Verify that the edge from key to val is the ephemeron edge.
+ const HeapGraphEdge* key_to_val_edge =
+ FindFirstEdgeTo(*key_entry, *val_entry);
+ ASSERT_NE(nullptr, key_to_val_edge);
+ EXPECT_EQ(HeapGraphEdge::kInternal, key_to_val_edge->type());
+ EXPECT_TRUE(
+ std::string_view(key_to_val_edge->name()).ends_with(expected_name));
+}
+
} // namespace v8::internal
Original Bug Report
CHECK failure: reference_summary_.weak_references().find(target) != reference_summary_.weak_ref
Detailed Report: https://clusterfuzz.com/testcase?key=6189094145130496
Fuzzer: big_sleep Job Type: linux_asan_d8_dbg Platform Id: linux
Crash Type: CHECK failure Crash Address: Crash State: reference_summary_.weak_references().find(target) != reference_summary_.weak_ref v8::internal::HeapEntryVerifier::CheckWeakReference v8::internal::V8HeapExplorer::SetWeakReference
Sanitizer: address (ASAN)
Regressed: https://clusterfuzz.com/revisions?job=linux_asan_d8_dbg&range=100346:100347
Reproducer Testcase: https://clusterfuzz.com/download?testcase_id=6189094145130496
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.