Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in V8
DescriptionUse after free in V8
ComponentV8
Bug ClassUAF
Tracker520656237
Fix commit9634c1ea1c47 (v8/v8) +20/-2
CISA KEVNot listed
Creditedwang1r && lhfff
Disclosed2026-07-29

Files Changed

  • src/objects/lookup.cc
  • test/mjsunit/regress/regress-double-toStringTag.js
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