CVE-2026-14409
Overview
Background
- Heap snapshot generator
- the V8 subsystem that walks the live heap (e.g. for DevTools) to produce a snapshot, and which must not allocate or trigger garbage collection while it iterates the heap.
- `GetConstructorHelper` / `JSReceiver::GetConstructor`
- internal routines that derive a human-readable constructor/object name for a receiver, used by the snapshot generator to label objects.
- `AllocationPolicy` / `AllowAllocation`
- an enum controlling whether a lookup helper is permitted to allocate; the patch renames the
AllocationPolicy{kAllocationAllowed,kAllocationDisallowed}enum toAllowAllocation{kYes,kNo}and threads it through more call sites. - Unboxed double field
- an in-object property stored as a raw
double; reading it as a tagged value normally requires boxing it into a freshly-allocatedHeapNumber.
Root Cause Analysis
GetConstructorHelper computed a better object name by calling JSFunction::GetDebugName(isolate, constructor), which forwarded to SharedFunctionInfo::DebugName without any allocation restriction, so in some cases the name computation could allocate on the heap. Because the heap snapshot generator invokes GetConstructor() during heap traversal, that allocation could occur at a point where allocation/GC must not happen, violating the snapshot generator’s no-allocation invariant.
The patch adds a DisallowGarbageCollection no_gc scope at the top of GetConstructorHelper and plumbs an explicit allocation flag through the whole name-lookup chain: GetDebugName gains an AllowAllocation allow_allocation parameter, JSReceiver::GetDataProperty / LookupIterator::FetchValue / GetDataValue / GetStringPropertyValue take AllowAllocation instead of AllocationPolicy, and the two problematic call sites pass AllowAllocation::kNo. In FetchValue, when allow_allocation == AllowAllocation::kNo and the field is an in-object double, the code returns undefined instead of allocating a HeapNumber, so the whole path is now guaranteed allocation-free.
The fix works by making the disallowed-allocation intent explicit and enforced (via no_gc) all the way down to the field-read that would otherwise box a double.
GetConstructorHelper, called during allocation-forbidden heap-snapshot traversal, reached a GetDebugName/property-lookup path that could allocate (boxing an unboxed double); the fix threads AllowAllocation::kNo through that path and adds a DisallowGarbageCollection scope so the name lookup never allocates.Attack Path
- Trigger a heap snapshot Cause V8 to run the heap snapshot generator (e.g. via DevTools/profiling), which walks the heap under an implicit no-allocation requirement.
- Reach constructor naming
For a traversed object, the generator calls
JSReceiver::GetConstructor→GetConstructorHelperto derive a display name. - Hit the allocating name path
GetDebugNameforwards toSharedFunctionInfo::DebugName(or a property lookup that reads an in-object double field), which allocates on the heap during traversal. - Allocation during traversal The unexpected allocation/GC occurs while the snapshot generator is iterating the heap, breaking its invariant and producing the advisory’s “Inappropriate implementation in V8”. (inferred)
Impact Assessment
LogicError condition inside the V8/renderer process as the advisory describes it; the precondition is exercising the heap snapshot generator’s constructor-naming path on an object whose name lookup would allocate. The advisory does not report in-the-wild exploitation, and any consequence beyond the broken no-allocation invariant (e.g. memory corruption from GC during traversal) is (inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
GetConstructorHelpersrc/objects/js-objects.cc |
modified | |
JSGlobalObjectsrc/objects/js-objects.h |
modified | |
JSGlobalProxysrc/objects/js-objects.h |
modified |
Files Changed
src/objects/js-function.ccsrc/objects/js-function.hsrc/objects/js-objects.ccsrc/objects/js-objects.hsrc/objects/lookup.ccsrc/objects/lookup.h
Audit Directions
- Allocation under no-alloc scopesSearch other heap-snapshot / GC-time /
DisallowGarbageCollectioncallers for helper calls (name lookups,DebugName, property reads) that may allocate transitively. - Unboxed-double boxing on readReview
FetchValue/FastPropertyAtand similar readers for paths that box double fields intoHeapNumberwhen the caller expected an allocation-free read. - Default-permissive allocation flagsAudit APIs whose allocation-control parameter defaults to “allowed” (
AllowAllocation::kYes) to ensure every allocation-forbidden caller explicitly passeskNoall the way down the call chain.
Patch
From 5b5f77018c9296911170b4f886570c1735e97919 Mon Sep 17 00:00:00 2001
From: Dominik Inführ <dinfuehr@chromium.org>
Date: Tue, 19 May 2026 15:24:07 +0200
Subject: [PATCH] [objects] Prevent allocations in JSReceiver::GetConstructor()
The heap snapshot generator uses GetConstructor() to provide a
better object name. However, this method allocates in some cases.
This CL removes this allocation by passing the already existing
AllocationPolicy to the GetDebugName() methods as well.
Bug: 513810921
Change-Id: I3f7abd2c7bfb5cf61d25390a77d7718d6409f1b5
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7858355
Commit-Queue: Dominik Inführ <dinfuehr@chromium.org>
Reviewed-by: Igor Sheludko <ishell@chromium.org>
Cr-Commit-Position: refs/heads/main@{#107510}
---
diff --git a/src/objects/js-function.cc b/src/objects/js-function.cc
index 119c7f3..d6a945f 100644
--- a/src/objects/js-function.cc
+++ b/src/objects/js-function.cc
@@ -1366,7 +1366,8 @@
} // namespace
DirectHandle<String> JSFunction::GetDebugName(
- Isolate* isolate, DirectHandle<JSFunction> function) {
+ Isolate* isolate, DirectHandle<JSFunction> function,
+ AllowAllocation allow_allocation) {
// Below we use the same fast-path that we already established for
// Function.prototype.bind(), where we avoid a slow "name" property
// lookup if the DescriptorArray for the |function| still has the
@@ -1386,7 +1387,7 @@
if (IsString(*name)) return Cast<String>(name);
}
return SharedFunctionInfo::DebugName(
- isolate, direct_handle(function->shared(), isolate));
+ isolate, direct_handle(function->shared(), isolate), allow_allocation);
}
bool JSFunction::SetName(Isolate* isolate, DirectHandle<JSFunction> function,
diff --git a/src/objects/js-function.h b/src/objects/js-function.h
index 18d98f3..89e6b75 100644
--- a/src/objects/js-function.h
+++ b/src/objects/js-function.h
@@ -455,8 +455,9 @@
// The function's name if it is configured, otherwise shared function info
// debug name.
- static DirectHandle<String> GetDebugName(Isolate* isolate,
- DirectHandle<JSFunction> function);
+ static DirectHandle<String> GetDebugName(
+ Isolate* isolate, DirectHandle<JSFunction> function,
+ AllowAllocation allow_allocation = AllowAllocation::kYes);
// The function's string representation implemented according to
// ES6 section 19.2.3.5 Function.prototype.toString ( ).
diff --git a/src/objects/js-objects.cc b/src/objects/js-objects.cc
index 77bc101..f218fd6 100644
--- a/src/objects/js-objects.cc
+++ b/src/objects/js-objects.cc
@@ -168,7 +168,7 @@
}
Handle<Object> JSReceiver::GetDataProperty(LookupIterator* it,
- AllocationPolicy allocation_policy) {
+ AllowAllocation allow_allocation) {
for (;; it->Next()) {
switch (it->state()) {
case LookupIterator::INTERCEPTOR:
@@ -209,7 +209,7 @@
case LookupIterator::TYPED_ARRAY_INDEX_NOT_FOUND:
return it->isolate()->factory()->undefined_value();
case LookupIterator::DATA:
- return it->GetDataValue(allocation_policy);
+ return it->GetDataValue(allow_allocation);
case LookupIterator::NOT_FOUND:
return it->isolate()->factory()->undefined_value();
case LookupIterator::MODULE_NAMESPACE: {
@@ -601,6 +601,7 @@
namespace {
std::pair<MaybeDirectHandle<JSFunction>, DirectHandle<String>>
GetConstructorHelper(Isolate* isolate, DirectHandle<JSReceiver> receiver) {
+ DisallowGarbageCollection no_gc;
// If the object was instantiated simply with base == new.target, the
// constructor on the map provides the most accurate name.
// Don't provide the info for prototypes, since their constructors are
@@ -613,7 +614,7 @@
DirectHandle<JSFunction> constructor =
Cast<JSFunction>(maybe_constructor);
DirectHandle<String> name =
- JSFunction::GetDebugName(isolate, constructor);
+ JSFunction::GetDebugName(isolate, constructor, AllowAllocation::kNo);
if (name->length() != 0 &&
!name->Equals(ReadOnlyRoots(isolate).Object_string())) {
return std::make_pair(indirect_handle(constructor, isolate), name);
@@ -636,8 +637,8 @@
LookupIterator it_to_string_tag(
isolate, receiver, isolate->factory()->to_string_tag_symbol(), current,
LookupIterator::OWN_SKIP_INTERCEPTOR);
- auto maybe_to_string_tag = JSReceiver::GetDataProperty(
- &it_to_string_tag, AllocationPolicy::kAllocationDisallowed);
+ auto maybe_to_string_tag =
+ JSReceiver::GetDataProperty(&it_to_string_tag, AllowAllocation::kNo);
if (IsString(*maybe_to_string_tag)) {
return std::make_pair(MaybeHandle<JSFunction>(),
Cast<String>(maybe_to_string_tag));
@@ -673,12 +674,13 @@
LookupIterator it_constructor(
isolate, receiver, isolate->factory()->constructor_string(), current,
LookupIterator::OWN_SKIP_INTERCEPTOR);
- auto maybe_constructor = JSReceiver::GetDataProperty(
- &it_constructor, AllocationPolicy::kAllocationDisallowed);
+ auto maybe_constructor =
+ JSReceiver::GetDataProperty(&it_constructor, AllowAllocation::kNo);
if (IsJSFunction(*maybe_constructor)) {
auto constructor = Cast<JSFunction>(maybe_constructor);
auto name = SharedFunctionInfo::DebugName(
- isolate, direct_handle(constructor->shared(), isolate));
+ isolate, direct_handle(constructor->shared(), isolate),
+ AllowAllocation::kNo);
if (name->length() != 0 &&
!name->Equals(ReadOnlyRoots(isolate).Object_string())) {
diff --git a/src/objects/js-objects.h b/src/objects/js-objects.h
index 2585096..9ef4c49 100644
--- a/src/objects/js-objects.h
+++ b/src/objects/js-objects.h
@@ -23,10 +23,6 @@
namespace v8::internal {
-// Enum for functions that offer a second mode that does not cause allocations.
-// Used in conjunction with LookupIterator and unboxed double fields.
-enum class AllocationPolicy { kAllocationAllowed, kAllocationDisallowed };
-
enum InstanceType : uint16_t;
class JSGlobalObject;
class JSGlobalProxy;
@@ -335,8 +331,8 @@
DirectHandle<JSReceiver> object,
DirectHandle<Name> name);
V8_EXPORT_PRIVATE static Handle<Object> GetDataProperty(
- LookupIterator* it, AllocationPolicy allocation_policy =
- AllocationPolicy::kAllocationAllowed);
+ LookupIterator* it,
+ AllowAllocation allow_allocation = AllowAllocation::kYes);
// Retrieves a permanent object identity hash code. The undefined value might
// be returned in case no hash was created yet.
diff --git a/src/objects/lookup.cc b/src/objects/lookup.cc
index 681b70c..bf5ba6a 100644
--- a/src/objects/lookup.cc
+++ b/src/objects/lookup.cc
@@ -996,7 +996,7 @@
}
DirectHandle<Object> LookupIterator::FetchValue(
- AllocationPolicy allocation_policy) const {
+ AllowAllocation allow_allocation) const {
Tagged<Object> result;
DCHECK_NE(state_, STRING_LOOKUP_START_OBJECT);
DCHECK(!IsWasmObject(*holder_));
@@ -1020,8 +1020,8 @@
DirectHandle<JSObject> holder = GetHolder<JSObject>();
FieldIndex field_index =
FieldIndex::ForDetails(holder->map(), property_details_);
- if (allocation_policy == AllocationPolicy::kAllocationDisallowed &&
- field_index.is_inobject() && field_index.is_double()) {
+ if (allow_allocation == AllowAllocation::kNo && field_index.is_inobject() &&
+ field_index.is_double()) {
return isolate_->factory()->undefined_value();
}
return JSObject::FastPropertyAt(
@@ -1132,7 +1132,7 @@
}
Handle<Object> LookupIterator::GetStringPropertyValue(
- AllocationPolicy allocation_policy) const {
+ AllowAllocation allow_allocation) const {
DCHECK_EQ(state_, STRING_LOOKUP_START_OBJECT);
if (IsElement()) {
DirectHandle<String> string = Cast<String>(lookup_start_object_);
@@ -1149,9 +1149,9 @@
}
Handle<Object> LookupIterator::GetDataValue(
- AllocationPolicy allocation_policy) const {
+ AllowAllocation allow_allocation) const {
DCHECK_EQ(DATA, state_);
- return indirect_handle(FetchValue(allocation_policy), isolate_);
+ return indirect_handle(FetchValue(allow_allocation), isolate_);
}
DirectHandle<Object> LookupIterator::GetDataValue(SeqCstAccessTag tag) const {
diff --git a/src/objects/lookup.h b/src/objects/lookup.h
index f1807b2..d5666a3 100644
--- a/src/objects/lookup.h
+++ b/src/objects/lookup.h
@@ -262,10 +262,9 @@