Overview

Low
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in V8
DescriptionInappropriate implementation in V8
ComponentV8
Sub-componentProfiler
Bug ClassLogic Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker515086856
Fix commit33ca8a4017b7 (v8/v8) +139/-19
CWECWE-122 (Heap buffer overflow)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-20 (per vendor advisory)
Fix landed2026-05-22 (public git)
Disclosed2026-06-30
Patch gap39 days — fix public in source before the advisory

Background

Heap snapshot generator
the V8 profiler subsystem in src/profiler/heap-snapshot-generator.cc that walks the live heap and emits a graph of HeapEntry nodes connected by HeapGraphEdge edges for DevTools memory tooling.
Ephemeron
a key/value pair in a JSWeakMap/JSWeakSet backed by an EphemeronHashTable, where the value is retained only while the key is reachable, modeled in the snapshot by CreateEphemeronEdges().
Weak edge
a HeapGraphEdge of type kWeak emitted by SetWeakReference() 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.

Key insight
The single mistake was emitting both 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

  1. Take a heap snapshot A page or DevTools client that can trigger HeapProfiler::TakeHeapSnapshot() on a heap containing a JSWeakMap or JSWeakSet reaches the ephemeron-pair extraction loop shown in the hunk.
  2. Duplicate edges emitted For each pair the old code emits table→key and table→value kWeak edges via SetWeakReference() in addition to the ephemeron edges from CreateEphemeronEdges(), so the resulting graph misrepresents the retention relationship. (advisory: “Inappropriate implementation in V8”)
  3. 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

The flaw is an inappropriate implementation in the V8 heap-snapshot generator that emits redundant weak edges alongside ephemeron edges for 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

FunctionChangeNotes
if
src/profiler/heap-snapshot-generator.cc
modified
TEST_F
test/unittests/profiler/heap-snapshot-unittest.cc
modified

Files Changed

  • src/profiler/heap-snapshot-generator.cc
  • test/cctest/test-heap-profiler.cc
  • test/unittests/profiler/heap-snapshot-unittest.cc

Audit Directions

  • Double-emission of graph edges
    Audit other SetWeakReference/SetReference sites in heap-snapshot-generator.cc that also feed a specialized edge creator (e.g. CreateEphemeronEdges) for the same slot, and confirm the generic extractor is suppressed via MarkVisitedField.
  • Verifier consistency
    Wherever an edge is intentionally suppressed, check that V8_ENABLE_HEAP_SNAPSHOT_VERIFY paths call MarkReferenceCheckedWithoutChecking() so verification does not diverge from emission.
  • Ephemeron/weak-collection extraction
    Review handling of JSWeakMap, JSWeakSet, and EphemeronHashTable for IsUndefined(key) and null-entry guards to ensure edges are only created for live, resolvable key/value entries.
From 33ca8a4017b75d3c7e81f0f88760fe1871b016bf Mon Sep 17 00:00:00 2001
From: Dominik Inführ <[email protected]>
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 <[email protected]>
Commit-Queue: Dominik Inführ <[email protected]>
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);
Loading diff…

Regression Test / PoC

shipped with the fix
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
Loading diff…

Original Bug Report

reported by [email protected]

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.

View on issue tracker