CVE-2026-11650
Overview
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 hostHeapObject. - `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/
&fieldaddress (invalidated if GC relocates the host). - GC-moving allocation
isolate->factory()->NewJSDispatchHandle(...)can trigger garbage collection, which may move the hostHeapObject, 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(...).
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
- Reach dispatch-handle installation
The attacker causes closures/functions to be created so that
AllocateAndInstallJSDispatchHandleruns, e.g. via feedback-cell closure allocation (feedback-vector.cc) orJSFunctiondispatch-handle setup (js-function-inl.h). - Provoke GC during allocation
The
NewJSDispatchHandleallocation inside the routine triggers a garbage collection that relocates the host object (FeedbackCell/JSFunction), invalidating the previously computed rawlocation/&storage_. - 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. - 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
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
| Function | Change | Notes |
|---|---|---|
JSDispatchHandleMembersrc/objects/tagged-field.h |
modified |
Files Changed
src/objects/feedback-vector.ccsrc/objects/heap-object.hsrc/objects/js-function-inl.hsrc/objects/objects-inl.hsrc/objects/tagged-field.h
Audit Directions
- Address-before-allocationFlag any code that computes
&field,field_address, or a raw slot pointer into a movableHeapObjectbefore a call that can allocate/GC, then writes through it afterward; prefer offset-based recomputation. - Handle/slot installersAudit 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 parametersReview 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.
Patch
From e814d52038d5aafc1e733197a05d77777f4ebdeb Mon Sep 17 00:00:00 2001
From: Michael Lippautz <mlippautz@chromium.org>
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 <leszeks@chromium.org>
Commit-Queue: Michael Lippautz <mlippautz@chromium.org>
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};
};
Original Bug Report
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.hv8/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.
- Exhaust the Dispatch Table: The attacker creates a massive number of unique JS functions to exhaust the
JSDispatchTablecapacity, ensuring the next handle allocation forces a Major GC. - Groom the Heap: The attacker allocates and frees objects to create high fragmentation on a specific
OldSpacepage. This ensures the page will be selected for evacuation. - Trigger Allocation: The attacker triggers
ClosureFeedbackCellArray::New(e.g., via heavily nested evals). - Evacuation & Stale Write: The
FeedbackCellis allocated on the groomed page. The dispatch handle allocation triggers the GC, evacuating the groomed page. The function returns and writes a 4-byteuint32_tinto the newly freed page. - Free List Hijacking: By predicting the heap layout, the attacker ensures the 4-byte write corrupts a
FreeSpacenode’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.