Overview

Low
Severity
7.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in V8
DescriptionInappropriate implementation in V8
ComponentV8
Sub-componentObjects
Bug ClassLogic Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
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.
Tracker513810921
Fix commit5b5f77018c92 (v8/v8) +35/-34
CWECWE-693
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedYuntao You (@GraVity0) of Bytedance Wuheng Lab
Reported2026-05-16 (per vendor advisory)
Fix landed2026-05-22 (public git)
Disclosed2026-06-30
Patch gap39 days — fix public in source before the advisory

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 to AllowAllocation{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-allocated HeapNumber.

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.

Key insight
The core mistake was that 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

  1. 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.
  2. Reach constructor naming For a traversed object, the generator calls JSReceiver::GetConstructor → GetConstructorHelper to derive a display name.
  3. Hit the allocating name path GetDebugName forwards to SharedFunctionInfo::DebugName (or a property lookup that reads an in-object double field), which allocates on the heap during traversal.
  4. 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

The flaw lets a heap-snapshot operation allocate at a point where allocation is forbidden, an inappropriate-implementation / 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

FunctionChangeNotes
GetConstructorHelper
src/objects/js-objects.cc
modified
JSGlobalObject
src/objects/js-objects.h
modified
JSGlobalProxy
src/objects/js-objects.h
modified

Files Changed

  • src/objects/js-function.cc
  • src/objects/js-function.h
  • src/objects/js-objects.cc
  • src/objects/js-objects.h
  • src/objects/lookup.cc
  • src/objects/lookup.h

Audit Directions

  • Allocation under no-alloc scopes
    Search other heap-snapshot / GC-time / DisallowGarbageCollection callers for helper calls (name lookups, DebugName, property reads) that may allocate transitively.
  • Unboxed-double boxing on read
    Review FetchValue/FastPropertyAt and similar readers for paths that box double fields into HeapNumber when the caller expected an allocation-free read.
  • Default-permissive allocation flags
    Audit APIs whose allocation-control parameter defaults to “allowed” (AllowAllocation::kYes) to ensure every allocation-forbidden caller explicitly passes kNo all the way down the call chain.
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 @@
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.