CVE-2026-17729
Overview
Files Changed
src/objects/lookup.cctest/mjsunit/regress/regress-double-toStringTag.js
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.