CVE-2026-11650
Overview
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
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.