Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in V8
DescriptionUse after free in V8
ComponentV8
Bug ClassUAF
Action tierpatch-now — corruption primitive, web-reachable in default config (confirmed) — upheld by independent review by AI
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.
Independent review by AIConfirmed
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker511279942
Fix commite814d52038d5 (v8/v8) +8/-49
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-08 (per vendor advisory)
Fix landed2026-05-13 (public git)
Disclosed2026-06-08
Patch gap26 days — fix public in source before the advisory

Background

`JSDispatchHandle` / `JSDispatchTable`
a handle into V8’s dispatch table that maps a function’s entry to executable Code; installing a handle requires writing the 32-bit handle into a field of the host HeapObject.
`HeapObject::AllocateAndInstallJSDispatchHandle`
the helper that allocates a new dispatch-table entry (via NewJSDispatchHandle) and stores the resulting handle into the host object’s dispatch-handle field.
`offsetof` vs. raw `location` pointer
the field to write can be identified either by a fixed byte offset within the host (stable across GC) or by a precomputed raw pointer/&field address (invalidated if GC relocates the host).
GC-moving allocation
isolate->factory()->NewJSDispatchHandle(...) can trigger garbage collection, which may move the host HeapObject, so any address computed before that call may no longer point into the live object.

Root Cause Analysis

The removed overload of HeapObject::AllocateAndInstallJSDispatchHandle (and the mirror JSDispatchHandleMember::AllocateAndInstall) received or computed a raw destination pointer — JSDispatchHandle* location, e.g. &cell->dispatch_handle_ or &storage_ — before calling NewJSDispatchHandle, which can trigger GC. If GC moved the host object during allocation, the previously computed location/&storage_ became a dangling pointer into stale memory, and the subsequent base::AsAtomic32::Release_Store(location, handle) wrote the new handle to freed/relocated memory. The violated invariant is that a write destination inside a movable HeapObject must be recomputed from the (possibly relocated) host after any GC-capable allocation, not held as a raw address across it.

The fix deletes the raw-pointer overload and JSDispatchHandleMember::AllocateAndInstall, keeping only the overload that takes an offset: it allocates first and then derives the destination via host->field_address(offset), so the address is computed from the live host after allocation. Call sites were updated accordingly — feedback-vector.cc now passes offsetof(FeedbackCell, dispatch_handle_) instead of &cell->dispatch_handle_, and js-function-inl.h now calls HeapObject::AllocateAndInstallJSDispatchHandle(function, offsetof(JSFunction, dispatch_handle_), ...) instead of function->dispatch_handle_.AllocateAndInstall(...).

Key insight
The core mistake was computing and holding a raw write-destination pointer into a movable HeapObject across a GC-triggering allocation; the fix passes a stable offsetof field offset and recomputes the address via host->field_address(offset) after allocation, so the store always targets the relocated live object.

Attack Path

  1. Reach dispatch-handle installation The attacker causes closures/functions to be created so that AllocateAndInstallJSDispatchHandle runs, e.g. via feedback-cell closure allocation (feedback-vector.cc) or JSFunction dispatch-handle setup (js-function-inl.h).
  2. Provoke GC during allocation The NewJSDispatchHandle allocation inside the routine triggers a garbage collection that relocates the host object (FeedbackCell/JSFunction), invalidating the previously computed raw location/&storage_.
  3. Stale write via dangling pointer The code then executes Release_Store(location, handle) on the pre-GC address, writing the dispatch handle into freed/relocated memory — the use-after-free the advisory describes.
  4. Escalate via corrupted dispatch state A controlled write of a dispatch handle into stale memory, or leaving a live object with an uninstalled/corrupted dispatch handle, could be leveraged toward type confusion or memory corruption (inferred); the patch shows only the dangling write.

Impact Assessment

An attacker running JavaScript in the renderer that induces the vulnerable dispatch-handle installation with well-timed GC gains a use-after-free write of a JSDispatchHandle into relocated/freed memory, per the advisory’s classification of a use-after-free in V8. The precondition is triggering GC-driven relocation of the host object during NewJSDispatchHandle. Any escalation to a controlled read/write primitive or code execution is (inferred) and not demonstrated by the patch.

Changed Functions

FunctionChangeNotes
JSDispatchHandleMember
src/objects/tagged-field.h
modified

Files Changed

  • src/objects/feedback-vector.cc
  • src/objects/heap-object.h
  • src/objects/js-function-inl.h
  • src/objects/objects-inl.h
  • src/objects/tagged-field.h

Audit Directions

  • Address-before-allocation
    Flag any code that computes &field, field_address, or a raw slot pointer into a movable HeapObject before a call that can allocate/GC, then writes through it afterward; prefer offset-based recomputation.
  • Handle/slot installers
    Audit other install-and-store helpers (dispatch handles, external pointers, trusted-pointer slots) for the “compute location, allocate, store” ordering that a moving GC invalidates.
  • Raw-pointer API parameters
    Review APIs that accept a raw destination pointer (like the removed JSDispatchHandle* location) instead of a host+offset, since callers can inadvertently pass an address that does not survive GC.
From e814d52038d5aafc1e733197a05d77777f4ebdeb Mon Sep 17 00:00:00 2001
From: Michael Lippautz <[email protected]>
Date: Tue, 12 May 2026 12:42:37 +0000
Subject: [PATCH] Fix raw pointer usage across GC point in dispatch handle allocation

CONV=4fae63e0-b73f-42ed-9a34-8922115bbe52
TAG=AGY

Bug: 511279942
Change-Id: I12c19e62d2816fc9f8b27f3c12cf61b30b52c4c6
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7840603
Reviewed-by: Leszek Swirski <[email protected]>
Commit-Queue: Michael Lippautz <[email protected]>
Cr-Commit-Position: refs/heads/main@{#107290}
---

diff --git a/src/objects/feedback-vector.cc b/src/objects/feedback-vector.cc
index aca27ed..c29053d 100644
--- a/src/objects/feedback-vector.cc
+++ b/src/objects/feedback-vector.cc
@@ -16,6 +16,7 @@
 #include "src/ic/handler-configuration-inl.h"
 #include "src/ic/ic-inl.h"
 #include "src/objects/data-handler-inl.h"
+#include "src/objects/feedback-cell.h"
 #include "src/objects/feedback-vector-inl.h"
 #include "src/objects/hash-table-inl.h"
 #include "src/objects/map-inl.h"
@@ -236,7 +237,8 @@
         shared->feedback_metadata()->GetCreateClosureParameterCount(i);
     auto initial_code = BUILTIN_CODE(isolate, CompileLazy);
     HeapObject::AllocateAndInstallJSDispatchHandle(
-        cell, &cell->dispatch_handle_, isolate, parameter_count, initial_code);
+        cell, offsetof(FeedbackCell, dispatch_handle_), isolate,
+        parameter_count, initial_code);
     cells.push_back(cell);
   }
 
diff --git a/src/objects/heap-object.h b/src/objects/heap-object.h
index c131fd0..9cd7588 100644
--- a/src/objects/heap-object.h
+++ b/src/objects/heap-object.h
@@ -288,12 +288,6 @@
       uint16_t parameter_count, DirectHandle<Code> code,
       WriteBarrierMode mode = WriteBarrierMode::UPDATE_WRITE_BARRIER);
 
-  template <typename ObjectType>
-  static inline JSDispatchHandle AllocateAndInstallJSDispatchHandle(
-      DirectHandle<ObjectType> host, JSDispatchHandle* location,
-      Isolate* isolate, uint16_t parameter_count, DirectHandle<Code> code,
-      WriteBarrierMode mode = WriteBarrierMode::UPDATE_WRITE_BARRIER);
-
   // Returns the field at offset in obj, as a read/write Object reference.
   // Does no checking, and is safe to use during GC, while maps are invalid.
   // Does not invoke write barrier, so should only be assigned to
diff --git a/src/objects/js-function-inl.h b/src/objects/js-function-inl.h
index 8528fde..bfa2b23 100644
--- a/src/objects/js-function-inl.h
+++ b/src/objects/js-function-inl.h
@@ -205,8 +205,9 @@
     uint16_t parameter_count, DirectHandle<Code> code, WriteBarrierMode mode) {
   DCHECK_EQ(function->raw_feedback_cell()->dispatch_handle(),
             kNullJSDispatchHandle);
-  return function->dispatch_handle_.AllocateAndInstall(
-      function, isolate, parameter_count, code, mode);
+  return HeapObject::AllocateAndInstallJSDispatchHandle(
+      function, offsetof(JSFunction, dispatch_handle_), isolate,
+      parameter_count, code, mode);
 }
 
 void JSFunction::clear_dispatch_handle() { dispatch_handle_.Relaxed_Clear(); }
diff --git a/src/objects/objects-inl.h b/src/objects/objects-inl.h
index 56cbc53..96f063f 100644
--- a/src/objects/objects-inl.h
+++ b/src/objects/objects-inl.h
@@ -937,43 +937,11 @@
   auto location =
       reinterpret_cast<JSDispatchHandle*>(host->field_address(offset));
   base::AsAtomic32::Release_Store(location, handle);
-  CONDITIONAL_JS_DISPATCH_HANDLE_WRITE_BARRIER(host, handle, mode);
-
-  return handle;
-}
-
-// static
-template <typename ObjectType>
-JSDispatchHandle HeapObject::AllocateAndInstallJSDispatchHandle(
-    DirectHandle<ObjectType> host, JSDispatchHandle* location, Isolate* isolate,
-    uint16_t parameter_count, DirectHandle<Code> code, WriteBarrierMode mode) {
-  JSDispatchTable::Space* space = isolate->GetJSDispatchTableSpaceFor(location);
-  JSDispatchHandle handle =
-      isolate->factory()->NewJSDispatchHandle(parameter_count, code, space);
-
-  // Use a Release_Store to ensure that the store of the pointer into the table
-  // is not reordered after the store of the handle. Otherwise, other threads
-  // may access an uninitialized table entry and crash.
-  base::AsAtomic32::Release_Store(location, handle);
   CONDITIONAL_JS_DISPATCH_HANDLE_WRITE_BARRIER(*host, handle, mode);
 
   return handle;
 }
 
-template <typename ObjectType>
-JSDispatchHandle JSDispatchHandleMember::AllocateAndInstall(
-    DirectHandle<ObjectType> host, Isolate* isolate, uint16_t parameter_count,
-    DirectHandle<Code> code, WriteBarrierMode mode) {
-  JSDispatchTable::Space* space =
-      isolate->GetJSDispatchTableSpaceFor(reinterpret_cast<Address>(&storage_));
-  JSDispatchHandle handle =
-      isolate->factory()->NewJSDispatchHandle(parameter_count, code, space);
-  storage_.store(static_cast<uint32_t>(handle.value()),
-                 std::memory_order_release);
-  CONDITIONAL_JS_DISPATCH_HANDLE_WRITE_BARRIER(*host, handle, mode);
-  return handle;
-}
-
 ObjectSlot HeapObject::RawField(int byte_offset) const {
   return ObjectSlot(field_address(byte_offset));
 }
diff --git a/src/objects/tagged-field.h b/src/objects/tagged-field.h
index 045f9cf..cf01b8c 100644
--- a/src/objects/tagged-field.h
+++ b/src/objects/tagged-field.h
@@ -103,6 +103,8 @@
 // JSDispatchHandleMember stores a 32-bit JSDispatchHandle as a member of a
 // HeapObject subclass. Explicit padding must be added to the containing
 // class on builds with kTaggedSize == 8.
+//
+// Use HeapObject::AllocateAndInstallJSDispatchHandle() to install the handle.
 class JSDispatchHandleMember {
  public:
   constexpr JSDispatchHandleMember() = default;
@@ -121,14 +123,6 @@
   inline void Relaxed_Store(HeapObject* host, JSDispatchHandle handle,
                             WriteBarrierMode mode = UPDATE_WRITE_BARRIER);
 
-  // Allocates a new dispatch-table entry, release-stores it into this member
-  // and emits the write barrier anchored on |host|. Mirrors
-  // HeapObject::AllocateAndInstallJSDispatchHandle.
-  template <typename ObjectType>
-  inline JSDispatchHandle AllocateAndInstall(
-      DirectHandle<ObjectType> host, Isolate* isolate, uint16_t parameter_count,
-      DirectHandle<Code> code, WriteBarrierMode mode = UPDATE_WRITE_BARRIER);
-
  private:
   std::atomic<uint32_t> storage_{0};
 };
Loading diff…

Original Bug Report

reported by [email protected]

Stale interior pointer write in HeapObject::AllocateAndInstallJSDispatchHandle

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A raw C++ pointer to an object’s interior member is passed to a function that can trigger a Major Mark-Compact GC. In standard builds where Conservative Stack Scanning is disabled, this raw pointer is not tracked by the GC, leading to a stale pointer write when the object is evacuated.

Affected files:

  • v8/src/objects/objects-inl.h
  • v8/src/objects/feedback-vector.cc

Estimated timestamp from git blame: 2026-03-23

Summary

In HeapObject::AllocateAndInstallJSDispatchHandle, a raw interior pointer (JSDispatchHandle* location) is used across a potential garbage collection point. If the JSDispatchTable is exhausted, allocating a new handle triggers a Major Mark-Compact GC. In standard Chrome builds, Conservative Stack Scanning (CSS) is disabled by default (v8_enable_direct_handle=false). Because the C++ stack is not scanned, the location pointer is not updated if the host object is evacuated during the GC. Upon returning, the function blindly writes a 4-byte dispatch handle to the stale, evacuated address, leading to memory corruption.

Vulnerability Details

The vulnerability occurs in v8/src/objects/objects-inl.h:

template <typename ObjectType>
JSDispatchHandle HeapObject::AllocateAndInstallJSDispatchHandle(
    DirectHandle<ObjectType> host, JSDispatchHandle* location, Isolate* isolate,
    uint16_t parameter_count, DirectHandle<Code> code, WriteBarrierMode mode) {
  JSDispatchTable::Space* space = isolate->GetJSDispatchTableSpaceFor(location);
  
  // BUG: NewJSDispatchHandle can trigger a Major GC (Mark-Compact)
  JSDispatchHandle handle =
      isolate->factory()->NewJSDispatchHandle(parameter_count, code, space);

  // BUG: If 'host' was evacuated during the GC, 'location' is now a stale pointer
  base::AsAtomic32::Release_Store(location, handle);
  CONDITIONAL_JS_DISPATCH_HANDLE_WRITE_BARRIER(*host, handle, mode);

  return handle;
}

The primary caller is ClosureFeedbackCellArray::New in v8/src/objects/feedback-vector.cc:

    DirectHandle<FeedbackCell> cell = isolate->factory()->NewNoClosuresCell();
    uint16_t parameter_count =
        shared->feedback_metadata()->GetCreateClosureParameterCount(i);
    auto initial_code = BUILTIN_CODE(isolate, CompileLazy);
    HeapObject::AllocateAndInstallJSDispatchHandle(
        cell, &cell->dispatch_handle_, isolate, parameter_count, initial_code);

When &cell->dispatch_handle_ is evaluated, it dereferences the indirect handle cell and calculates the raw memory address of the dispatch_handle_ member within the FeedbackCell object on the Old Space heap. This raw pointer is passed into location.

Inside AllocateAndInstallJSDispatchHandle, the call to NewJSDispatchHandle attempts to allocate a table entry. If the JSDispatchTable is full, it enters a slow path (RetryCustomAllocateOrFail with AllocationType::kOld). This explicitly triggers GarbageCollector::MARK_COMPACTOR (a Major GC).

If the FeedbackCell was located on a highly fragmented page, the Mark-Compact GC will select that page as an evacuation candidate and move the FeedbackCell to a new address. The cell handle is safely updated by the GC. However, because Conservative Stack Scanning is disabled in standard builds, the GC ignores the raw location pointer on the C++ stack.

When NewJSDispatchHandle returns, location still points to the old, freed page. The subsequent Release_Store(location, handle) writes the 4-byte handle index into an evacuated memory location.

Suggested Exploitation Steps

Note: These are potential steps as our tooling agent cannot execute code to provide a working Proof of Concept.

  1. Exhaust the Dispatch Table: The attacker creates a massive number of unique JS functions to exhaust the JSDispatchTable capacity, ensuring the next handle allocation forces a Major GC.
  2. Groom the Heap: The attacker allocates and frees objects to create high fragmentation on a specific OldSpace page. This ensures the page will be selected for evacuation.
  3. Trigger Allocation: The attacker triggers ClosureFeedbackCellArray::New (e.g., via heavily nested evals).
  4. Evacuation & Stale Write: The FeedbackCell is allocated on the groomed page. The dispatch handle allocation triggers the GC, evacuating the groomed page. The function returns and writes a 4-byte uint32_t into the newly freed page.
  5. Free List Hijacking: By predicting the heap layout, the attacker ensures the 4-byte write corrupts a FreeSpace node’s “next” pointer. Subsequent allocations follow the corrupted free list, leading to overlapping objects (type confusion/arbitrary read-write) and arbitrary code execution within the renderer.

Suggested Fix

Do not pass the raw interior pointer across the GC point. Re-derive the location pointer from the host handle after NewJSDispatchHandle has returned.

template <typename ObjectType>
JSDispatchHandle HeapObject::AllocateAndInstallJSDispatchHandle(
    DirectHandle<ObjectType> host, size_t offset, Isolate* isolate,
    uint16_t parameter_count, DirectHandle<Code> code, WriteBarrierMode mode) {
  
  // ... allocate handle ...

  // Recompute the raw pointer AFTER the allocation in case a GC occurred
  auto location =
      reinterpret_cast<JSDispatchHandle*>(host->field_address(offset));
  base::AsAtomic32::Release_Store(location, handle);

All callers (like feedback-vector.cc) should pass the field offset rather than the raw pointer. A field offset overload of this function already exists in objects-inl.h.

Evaluated with Chrome root at commit: eca8648a4e1cdfdda68c495a6003059fed641955


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker