Overview

Medium
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in V8
DescriptionUse after free in V8
ComponentV8
Sub-componentObjects
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
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.
Tracker520656237
Fix commit9634c1ea1c47 (v8/v8) +20/-2
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
Creditedwang1r && lhfff
Reported2026-06-07 · bounty $500 (per vendor advisory)
Fix landed2026-06-11 (public git)
Disclosed2026-07-29
Patch gap48 days — fix public in source before the advisory

Background

Heap snapshot
V8’s mechanism for capturing the live object graph, during which allocation is forbidden so the heap graph stays stable and traversable.
`@@toStringTag`
the well-known Symbol.toStringTag property V8 reads to derive an object’s constructor/tag name, which the snapshotter queries while labeling nodes.
Out-of-object double property
a double-valued field stored in an object’s separate property backing store (not inline in the object), whose retrieval boxes the raw double into a freshly allocated HeapNumber.
`AllowAllocation::kNo`
the flag LookupIterator::FetchValue receives to signal that no heap allocation may occur while reading a property value.

Root Cause Analysis

While taking a heap snapshot, V8 calls LookupIterator::FetchValue with AllowAllocation::kNo to read a custom @@toStringTag and compute the constructor name without allocating. The pre-patch guard only returned undefined early when a double field was in-object (field_index.is_inobject() && field_index.is_double()), so an out-of-object double property fell through to JSObject::FastPropertyAt. Reading an out-of-object double still boxes the value into a new HeapNumber, which allocates during the no-allocation snapshot window and violates the invariant that no object is created while the heap graph is frozen, tripping a DCHECK and leaving V8 operating on a potentially stale/freed heap state (use-after-free).

The fix drops the field_index.is_inobject() condition so the early undefined return covers any double field, in-object or out-of-object, guaranteeing FetchValue never allocates when allow_allocation == AllowAllocation::kNo.

Key insight
The single mistake was scoping the no-allocation early-return to in-object double fields only, ignoring that out-of-object double fields also require boxing into a HeapNumber; the fix removes the is_inobject() check so every double field short-circuits to undefined and no allocation can occur.

Attack Path

  1. Craft the object shape Create an object with several defined properties plus a Symbol.toStringTag whose value is a large integer that V8 stores as an out-of-object double field.
  2. Keep it reachable Retain the object in a live variable so the heap snapshotter visits it and attempts to read its constructor tag.
  3. Trigger the snapshot Invoke a heap snapshot (e.g. via %TakeHeapSnapshot or DevTools), driving LookupIterator::FetchValue with AllowAllocation::kNo.
  4. Force the boxing allocation The out-of-object double bypasses the in-object-only guard and reaches JSObject::FastPropertyAt, which allocates a HeapNumber mid-snapshot.
  5. Corrupt heap state The disallowed allocation during the frozen graph traversal breaks the snapshot invariant, yielding a use-after-free on heap objects the snapshotter still references.

Impact Assessment

An attacker who can run JavaScript in the renderer and cause a heap snapshot to be taken gains a use-after-free in the V8 heap, corrupting or reading freed object memory in the renderer process. Preconditions are modest: control over an object’s @@toStringTag as an out-of-object double and the ability to trigger snapshotting, which is reachable through DevTools-driven or programmatic snapshot paths.

Files Changed

  • src/objects/lookup.cc
  • test/mjsunit/regress/regress-double-toStringTag.js

Audit Directions

  • Allocation under no-allocation scopes
    Audit every code path guarded by AllowAllocation::kNo or DisallowGarbageCollection for value-reads (doubles, boxed primitives) that can still allocate a HeapNumber or other object.
  • Partial field-kind guards
    Look for early-return checks that qualify on storage location (is_inobject) rather than the value kind (is_double), since the location does not determine whether boxing allocation happens.
  • Snapshot and profiler read paths
    Review LookupIterator, JSObject::FastPropertyAt, and constructor-name/tag lookups reached by the heap snapshotter and profiler for any remaining allocating property accesses.
From 9634c1ea1c47db5494f5486b6bfa25c11cf441bc Mon Sep 17 00:00:00 2001
From: Dominik Inführ <dinfuehr@chromium.org>
Date: Mon, 08 Jun 2026 15:38:33 +0200
Subject: [PATCH] [objects] Fix DCHECK when reading out-of-object double @@toStringTag

During heap snapshotting, allocation is disallowed. When V8 attempts
to read a custom @@toStringTag property to get the constructor name,
it must not allocate.

However, LookupIterator::FetchValue could still trigger an allocation
for doubles in out-of-object properties. This CL fixes this by
returning undefined in that case as well.

TAG=agy
CONV=811d12eb-568f-4c95-813c-bf202ead40c9

Bug: 520656237
Change-Id: I6a67ac9e656aebdc6d5a8c01acb6e5eb2ce54a19
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7908689
Commit-Queue: Dominik Inführ <dinfuehr@chromium.org>
Reviewed-by: Igor Sheludko <ishell@chromium.org>
Cr-Commit-Position: refs/heads/main@{#107912}
---

diff --git a/src/objects/lookup.cc b/src/objects/lookup.cc
index f50bc81..16a809d 100644
--- a/src/objects/lookup.cc
+++ b/src/objects/lookup.cc
@@ -1020,8 +1020,7 @@
     DirectHandle<JSObject> holder = GetHolder<JSObject>();
     FieldIndex field_index =
         FieldIndex::ForDetails(holder->map(), property_details_);
-    if (allow_allocation == AllowAllocation::kNo && field_index.is_inobject() &&
-        field_index.is_double()) {
+    if (allow_allocation == AllowAllocation::kNo && field_index.is_double()) {
       return isolate_->factory()->undefined_value();
     }
     return JSObject::FastPropertyAt(
diff --git a/test/mjsunit/regress/regress-double-toStringTag.js b/test/mjsunit/regress/regress-double-toStringTag.js
new file mode 100644
index 0000000..0e770ca
--- /dev/null
+++ b/test/mjsunit/regress/regress-double-toStringTag.js
@@ -0,0 +1,19 @@
+// 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: --allow-natives-syntax
+
+function make() {
+  var v = { maxByteLength: 1073741824 };
+  Object.defineProperty(v, "p1", { value: 0 });
+  Object.defineProperty(v, "p2", { value: 0 });
+  Object.defineProperty(v, Symbol.toStringTag, { value: 1073741824 });
+  return v;
+}
+
+// Keep it alive in a variable so it is visited during the snapshot.
+const keep = make();
+
+// Trigger heap snapshot
+%TakeHeapSnapshot("/dev/null");
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/test/mjsunit/regress/regress-double-toStringTag.js b/test/mjsunit/regress/regress-double-toStringTag.js
new file mode 100644
index 0000000..0e770ca
--- /dev/null
+++ b/test/mjsunit/regress/regress-double-toStringTag.js
@@ -0,0 +1,19 @@
+// 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: --allow-natives-syntax
+
+function make() {
+  var v = { maxByteLength: 1073741824 };
+  Object.defineProperty(v, "p1", { value: 0 });
+  Object.defineProperty(v, "p2", { value: 0 });
+  Object.defineProperty(v, Symbol.toStringTag, { value: 1073741824 });
+  return v;
+}
+
+// Keep it alive in a variable so it is visited during the snapshot.
+const keep = make();
+
+// Trigger heap snapshot
+%TakeHeapSnapshot("/dev/null");
Loading diff…

Original Bug Report

reported by wa...@gmail.com

V8 HeapProfiler snapshot crash from allocation in `FetchValue(kNo)` for out-of-object double fields

Summary

LookupIterator::FetchValue(AllowAllocation::kNo) does not fully honor its no-allocation contract for fast-property double fields. It suppresses boxing only when the double field is in-object. If the field is out-of-object, FetchValue(kNo) still calls JSObject::FastPropertyAt(...), which boxes the double into a newly allocated HeapNumber.

This is reachable through JSReceiver::GetConstructorName() when it reads @@toStringTag using GetDataProperty(..., AllowAllocation::kNo).

The security-relevant consumer is heap snapshot generation. V8HeapExplorer / heap snapshot iteration holds raw V8 heap pointers while computing constructor names for heap entries. In a same-source ASAN build with is_debug=false and dcheck_always_on=false, the official V8 inspector-test harness crashes in the HeapProfiler.takeHeapSnapshot protocol path when moving-GC stress is enabled.

This report does not claim arbitrary read/write, a pure webpage-only trigger, a typed ASAN heap-use-after-free report, or stable crash behavior in default stock Chrome release builds without GC stress flags.

Affected Code Path

Revision used for reproduction:

d0ed76cc734d2d59f041af7017cf066945bf402c

Relevant code:

// src/objects/js-objects.cc
GetConstructorHelper(...) {
  DisallowGarbageCollection no_gc;
  ...
  auto maybe_to_string_tag =
      JSReceiver::GetDataProperty(&it_to_string_tag, AllowAllocation::kNo);
  ...
}
// src/objects/lookup.cc
if (allow_allocation == AllowAllocation::kNo && field_index.is_inobject() &&
    field_index.is_double()) {
  return isolate_->factory()->undefined_value();
}
return JSObject::FastPropertyAt(...);

For out-of-object double fields, the field_index.is_inobject() condition is false, so the value is boxed and allocation occurs despite AllowAllocation::kNo.

The heap snapshot consumer reaches this through:

Inspector HeapProfiler.takeHeapSnapshot
V8HeapProfilerAgentImpl::takeHeapSnapshotNow
v8::HeapProfiler::TakeHeapSnapshot
HeapProfiler::TakeSnapshot
HeapSnapshotGenerator::GenerateSnapshot
V8HeapExplorer::AddEntry / IterateAndExtractReferences
V8HeapExplorer::GetConstructorName
JSReceiver::GetConstructorName
GetConstructorHelper
JSReceiver::GetDataProperty(..., AllowAllocation::kNo)
LookupIterator::FetchValue(kNo)
JSObject::FastPropertyAt
Object::WrapForRead -> NewHeapNumber

Main PoC

PoC file:

pocs/fetchvalue-inspector-test-heap-snapshot-many.js

It uses V8’s official inspector-test harness and sends HeapProfiler.takeHeapSnapshot through the protocol test layer. The script creates 50,000 live ordinary JS objects with an out-of-object double-valued Symbol.toStringTag:

var v = { maxByteLength: 1073741824 };
Object.defineProperty(v, "p1", { value: 0 });
Object.defineProperty(v, "p2", { value: 0 });
Object.defineProperty(v, Symbol.toStringTag, { value: 1073741824 });

1073741824 is intentionally larger than the 31-bit Smi maximum in this pointer-compression build, so the value is represented as a double field. The preceding properties push @@toStringTag into the out-of-object property store.

Reproduction

Build the official inspector harness:

ninja -C out/asan-nodcheck inspector-test
ninja -C out/asan inspector-test

The out/asan-nodcheck build used here had:

is_debug = false
symbol_level = 1
is_asan = true
v8_enable_sandbox = true
dcheck_always_on = false
v8_enable_slow_dchecks = false
v8_static_library = false
treat_warnings_as_errors = false

Run the release-semantics ASAN/nodcheck repro:

PKG=/path/to/unpacked/submission

ASAN_OPTIONS=detect_leaks=0:halt_on_error=1:symbolize=1 \
out/asan-nodcheck/inspector-test \
  --stress-compaction --stress-scavenge=1 --max-semi-space-size=1 --no-lazy \
  test/inspector/protocol-test.js \
  "$PKG/pocs/fetchvalue-inspector-test-heap-snapshot-many.js"

Observed result:

objects: 50000
AddressSanitizer:DEADLYSIGNAL
ERROR: AddressSanitizer: SEGV on unknown address ...
READ memory access
    #0 v8::internal::HeapObject::SizeFromMap(...)
    #1 v8::internal::PagedSpaceObjectIterator::Next()
    #2 v8::internal::HeapObjectIterator::NextObject()
    #4 v8::internal::V8HeapExplorer::IterateAndExtractReferences(...)
    #5 v8::internal::HeapSnapshotGenerator::GenerateSnapshot()
    #10 v8::HeapProfiler::TakeHeapSnapshot(...)
    #11 v8_inspector::V8HeapProfilerAgentImpl::takeHeapSnapshotNow(...)
    #12 v8_inspector::V8HeapProfilerAgentImpl::HeapSnapshotTask::Run(...)

Full log:

evidence/fetchvalue-inspector-test-nodcheck-many.out

Run the dcheck-on confirmation:

PKG=/path/to/unpacked/submission

ASAN_OPTIONS=detect_leaks=0:halt_on_error=1:symbolize=1 \
out/asan/inspector-test \
  test/inspector/protocol-test.js \
  "$PKG/pocs/fetchvalue-inspector-test-heap-snapshot-many.js"

Observed result:

Debug check failed: AllowHeapAllocation::IsAllowed().

Symbolizing the stack shows:

NewHeapNumber
Object::WrapForRead<kYoung>
JSObject::FastPropertyAt
LookupIterator::FetchValue(kNo)
JSReceiver::GetDataProperty(kNo)
GetConstructorHelper
JSReceiver::GetConstructorName
V8HeapExplorer::GetConstructorName
V8HeapExplorer::AddEntry
HeapSnapshotGenerator::GenerateSnapshot

Full log:

evidence/fetchvalue-inspector-test-dcheck-many.out

Additional Evidence and Scope Boundaries

Included auxiliary files:

pocs/fetchvalue-json-circular.js
evidence/fetchvalue-json-dcheck.out
evidence/fetchvalue-json-nodcheck.out

This confirms that a web API consumer (JSON.stringify circular-structure error formatting) reaches the same GetConstructorName / FetchValue(kNo) invariant violation in dcheck-on builds. In the nodcheck ASAN stress run it exits cleanly, consistent with this consumer being handle-safe.

pocs/fetchvalue-inspector-preview.js
evidence/fetchvalue-inspector-preview-nodcheck-many.out

This checks the Inspector Runtime object-preview path. The nodcheck ASAN stress run over 50,000 vulnerable objects exits cleanly, consistent with Inspector object preview using handle-safe v8::Local / DirectHandle paths. This is not the security-relevant crashing consumer.

Suggested Fix

Make LookupIterator::FetchValue(AllowAllocation::kNo) avoid boxing all double fields, not only in-object double fields. For example, remove the field_index.is_inobject() requirement from the existing guard, or add an explicit out-of-object double case that returns undefined_value() under AllowAllocation::kNo.

View on issue tracker