CVE-2026-9941
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ContextVksrc/libANGLE/renderer/vulkan/ContextVk.h |
modified |
Files Changed
src/libANGLE/renderer/vulkan/ContextVk.cppsrc/libANGLE/renderer/vulkan/ContextVk.hsrc/libANGLE/renderer/vulkan/VertexArrayVk.cpp
Patch
From 052c3437f454f97bfd3d7d959e25eef336413cb3 Mon Sep 17 00:00:00 2001
From: Charlie Lao <cclao@google.com>
Date: Thu, 14 May 2026 14:39:03 -0700
Subject: [PATCH] Vukan: Don't update default attributes for unused attributes
This change is follow up of https://crrev.com/c/7822999. It optimizes
how default vertex attributes (the constant values used when an
attribute is disabled) are handled in the Vulkan backend . The primary
goal is to avoid unnecessary updates for attributes that are not
actually used by the current program.
The attributes can come from two different sources: If vertex array
attribute is enabled, it should come from vertex array. Otherwise it
should come from context. And we want store these values in one array so
that we can send it down to driver without extra data copy. Right now we
store this mCurrentArray*** in VertexArrayVk, which causes a bug that
when a different vertex array is bound, the default attributes is not
being updated and may have a stale pointer that causes UAF, which
https://crrev.com/c/7822999 fixed. The other possible solution was to
store mCurrentArray*** in ContextVk, but after prototyping I noticed
that it is no simpler since now you have to deal with update vertex
array's attributes for every VAO bind, even though it made default
attribute easier. This CL focused on keeping mCurrentArray*** in
VertexArrayVk, but try to minimize the default attributes update
associated with previous fix.
Key Changes: Before this CL,
ContextVk::handleDirtyGraphicsDefaultAttribs will check if VAO has
changed, if yes, it calls updateDefaultAttrib for *all* attributes that
are disabled. That will allocate memory from ContextVk::
mStreamedVertexBuffers (per attribute DynamicBuffer). Since most of
attributes are unused, this is a bit waste. This CL try to avoid that by
detecting the exact default attribute that has stale buffer pointer and
set it to mEmptyBuffer. To make this possible, VertexArrayVk now
maintains mCurrentDefaultAttribsMask to track which attributes are
currently set up as default/constant attributes. When VAO binding
changes ( gl::state::DIRTY_BIT_VERTEX_ARRAY_BINDING), the new VAO's
mCurrentDefaultAttribsMask is split into two:
context->getActiveDefaultAttribsMask() is going to be updated by
ContextVk::handleDirtyGraphicsDefaultAttribs(). The remaining bits is
now explicitly set to mEmptyBuffer by calling
VertexArrayVk::syncDirtyDisabledAttribs().
The code previously used mDefaultAttribsGeneration to track when default
attribute buffers needed to be refreshed globally. This has been removed
with this explicit tracking of which attributes are using default
attributes.
Bug: chromium:502812366
Change-Id: Ic25da400a81c202bfc6e250a79b2d0b19ecc7986
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7851199
Reviewed-by: Yuxin Hu <yuxinhu@google.com>
Commit-Queue: Charlie Lao <cclao@google.com>
Reviewed-by: Shahbaz Youssefi <syoussefi@chromium.org>
---
diff --git a/src/libANGLE/renderer/vulkan/ContextVk.cpp b/src/libANGLE/renderer/vulkan/ContextVk.cpp
index 8b267c0..1657315 100644
--- a/src/libANGLE/renderer/vulkan/ContextVk.cpp
+++ b/src/libANGLE/renderer/vulkan/ContextVk.cpp
@@ -828,7 +828,6 @@
mFlipViewportForDrawFramebuffer(false),
mFlipViewportForReadFramebuffer(false),
mIsAnyHostVisibleBufferWritten(false),
- mDefaultAttribsGeneration(mDefaultAttribsGenerationFactory.generate()),
mImageWithTileMemory(nullptr),
mCurrentQueueSerialIndex(kInvalidQueueSerialIndex),
mInitialContextPriority(renderer->getDriverPriority(GetContextPriority(state))),
@@ -2140,25 +2139,11 @@
{
VertexArrayVk *vertexArrayVk = getVertexArray();
- gl::AttributesMask attribsMask;
- if (vertexArrayVk->getDefaultAttribsGeneration() != mDefaultAttribsGeneration)
- {
- vertexArrayVk->setDefaultAttribsGeneration(mDefaultAttribsGeneration);
- attribsMask = ~vertexArrayVk->getCurrentEnabledAttribsMask();
- }
- else
- {
- attribsMask = mDirtyDefaultAttribsMask;
- attribsMask &= ~vertexArrayVk->getCurrentEnabledAttribsMask();
- attribsMask &= mState.getProgramExecutable()->getAttributesMask();
- }
+ mDirtyDefaultAttribsMask &= ~vertexArrayVk->getCurrentEnabledAttribsMask();
+ mDirtyDefaultAttribsMask &= mState.getProgramExecutable()->getAttributesMask();
- for (size_t attribIndex : attribsMask)
- {
- ANGLE_TRY(vertexArrayVk->updateDefaultAttrib(this, attribIndex));
- }
-
- ANGLE_TRY(onVertexArrayChange(attribsMask));
+ ANGLE_TRY(vertexArrayVk->updateDefaultAttribs(this, mDirtyDefaultAttribsMask));
+ ANGLE_TRY(onVertexArrayChange(mDirtyDefaultAttribsMask));
mDirtyDefaultAttribsMask.reset();
return angle::Result::Continue;
@@ -5769,10 +5754,10 @@
break;
case gl::state::DIRTY_BIT_VERTEX_ARRAY_BINDING:
{
- if (vertexArrayVk->getDefaultAttribsGeneration() != mDefaultAttribsGeneration)
- {
- mGraphicsDirtyBits.set(DIRTY_BIT_DEFAULT_ATTRIBS);
- }
+ gl::AttributesMask staleDefaultAttribsMask =
+ vertexArrayVk->getCurrentDefaultAttribsMask() &
+ ~programExecutable->getAttributesMask();
+ vertexArrayVk->syncDirtyDisabledAttribs(this, staleDefaultAttribsMask);
invalidateDefaultAttributes(context->getActiveDefaultAttribsMask());
ANGLE_TRY(onVertexArrayChange(vertexArrayVk->getCurrentEnabledAttribsMask()));
ANGLE_TRY(onIndexBufferChange(vertexArrayVk->getCurrentElementArrayBuffer()));
@@ -7782,12 +7767,6 @@
return angle::Result::Continue;
}
-void ContextVk::invalidateDefaultAttribute(size_t attribIndex)
-{
- mDirtyDefaultAttribsMask.set(attribIndex);
- mGraphicsDirtyBits.set(DIRTY_BIT_DEFAULT_ATTRIBS);
-}
-
void ContextVk::invalidateDefaultAttributes(const gl::AttributesMask &dirtyMask)
{
if (dirtyMask.any())
diff --git a/src/libANGLE/renderer/vulkan/ContextVk.h b/src/libANGLE/renderer/vulkan/ContextVk.h
index 63a682b..b9f41a5 100644
--- a/src/libANGLE/renderer/vulkan/ContextVk.h
+++ b/src/libANGLE/renderer/vulkan/ContextVk.h
@@ -91,8 +91,6 @@
class ContextVk : public ContextImpl, public vk::Context, public MultisampleTextureInitializer
{
public:
- using VertexArrayGeneration = UniqueSerial;
-
ContextVk(const gl::State &state, gl::ErrorSet *errorSet, vk::Renderer *renderer);
~ContextVk() override;
@@ -429,7 +427,6 @@
angle::Result onVertexArrayChange(const gl::AttributesMask dirtyAttribBits);
- void invalidateDefaultAttribute(size_t attribIndex);
void invalidateDefaultAttributes(const gl::AttributesMask &dirtyMask);
angle::Result onFramebufferChange(FramebufferVk *framebufferVk, gl::Command command);
void onDrawFramebufferRenderPassDescChange(FramebufferVk *framebufferVk,
@@ -802,8 +799,6 @@
if (newBufferOut)
{
mHasInFlightStreamedVertexBuffers.set(attribIndex);
- mDefaultAttribsGeneration = mDefaultAttribsGenerationFactory.generate();
- mGraphicsDirtyBits.set(DIRTY_BIT_DEFAULT_ATTRIBS);
}
return angle::Result::Continue;
}
@@ -1577,10 +1572,6 @@
// "Current Value" aka default vertex attribute state.
gl::AttributesMask mDirtyDefaultAttribsMask;
- // Tracks if default vertex attribute buffers have been invalidated.
- UniqueSerialFactory mDefaultAttribsGenerationFactory;
- VertexArrayGeneration mDefaultAttribsGeneration;
-
// DynamicBuffers for streaming vertex data from client memory pointer as well as for default
// attributes. mHasInFlightStreamedVertexBuffers indicates if the dynamic buffer has any
// in-flight buffer or not that we need to release at submission time.
diff --git a/src/libANGLE/renderer/vulkan/VertexArrayVk.cpp b/src/libANGLE/renderer/vulkan/VertexArrayVk.cpp
index 723062c..5a1162a 100644
--- a/src/libANGLE/renderer/vulkan/VertexArrayVk.cpp
+++ b/src/libANGLE/renderer/vulkan/VertexArrayVk.cpp
@@ -948,6 +948,9 @@
vk::Renderer *renderer = contextVk->getRenderer();
contextVk->getPerfCounters().vertexArraySyncStateCalls++;
+ gl::AttributesMask prevEnabledAttribsMask = mCurrentEnabledAttribsMask;
+ mCurrentEnabledAttribsMask = mState.getEnabledAttributesMask();
+
const std::vector<gl::VertexAttribute> &attribs = mState.getVertexAttributes();
const std::vector<gl::VertexBinding> &bindings = mState.getVertexBindings();
@@ -1016,9 +1019,9 @@
for (size_t attribIndex : enabledNonStreamAttribDirtyBits)
{
// This will also update mNeedsConversionAttribMask
- ANGLE_TRY(syncDirtyEnabledNonStreamingAttrib(contextVk, attribs[attribIndex],
- bindings[attribs[attribIndex].bindingIndex],
- attribIndex, (*attribBits)[attribIndex]));
+ syncDirtyEnabledNonStreamingAttrib(contextVk, attribs[attribIndex],
+ bindings[attribs[attribIndex].bindingIndex], attribIndex,
+ (*attribBits)[attribIndex]);
}
// Sync all enabled and streaming attributes that are dirty
@@ -1027,9 +1030,9 @@
for (size_t attribIndex : enabledStreamAttribDirtyBits)
{
// This will also update mNeedsConversionAttribMask
- ANGLE_TRY(syncDirtyEnabledStreamingAttrib(contextVk, attribs[attribIndex],
- bindings[attribs[attribIndex].bindingIndex],
Original Bug Report
Potential Heap-Use-After-Free in ANGLE Vulkan VertexArrayVk
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 go/chrome-ai-generated-security-bugs-faq for more information.
Overview: A potential heap-use-after-free exists in ANGLE’s Vulkan backend due to dangling raw pointers in VertexArrayVk. When a DynamicBuffer resizes, old BufferHelper objects are deleted, but stale pointers remain cached in mCurrentArrayBuffers. Subsequent draw calls can dereference these pointers, providing an arbitrary memory increment primitive and potential driver-level exploitation.
Affected files:
third_party/angle/src/libANGLE/renderer/vulkan/VertexArrayVk.cppthird_party/angle/src/libANGLE/renderer/vulkan/vk_helpers.cppthird_party/angle/src/libANGLE/renderer/vulkan/ContextVk.cppthird_party/angle/src/libANGLE/renderer/vulkan/VertexArrayVk.h
Estimated timestamp from git blame: 2025-09-22
Summary
A potential heap-use-after-free (UAF) vulnerability has been identified in the ANGLE Vulkan backend. The issue originates from how default vertex attributes are cached. When a program uses an attribute that is disabled on the bound Vertex Array Object (VAO), ANGLE allocates a BufferHelper for the default value and stores a raw pointer to it in VertexArrayVk::mCurrentArrayBuffers. If the underlying DynamicBuffer is later forced to resize, the old BufferHelper objects are deleted from the heap during a command flush. However, the VertexArrayVk cache is not updated. A subsequent draw call can dereference this dangling pointer, allowing an attacker who grooms the heap to forge a BufferHelper object and achieve arbitrary code execution in the unsandboxed GPU process.
Technical Details
- Allocation: During
ContextVk::setupDraw, if a program uses a disabled attributeA,VertexArrayVk::updateDefaultAttribis called. This allocates memory viaDynamicBuffer::allocate, returning a rawBufferHelper*which is cached inVertexArrayVk::mCurrentArrayBuffers[A]. - The Free: If the attacker forces the
DynamicBufferfor attributeAto require a larger block size (e.g., via a large instanced draw call), the oldBufferHelperis moved to an in-flight list. Upon a Vulkan command flush (ContextVk::flushAndSubmitCommands),DynamicBuffer::updateQueueSerialAndReleaseInFlightBuffersdetects the size mismatch and clears the list. This destroys thestd::unique_ptrholding theBufferHelper, callingoperator deleteon the heap object. The raw pointer inmCurrentArrayBuffers[A]is now dangling. - The Use: The attacker issues a new draw call using a different program that does not use attribute
A, but uses an attributeBwhereB > A. BecauseAis not active,updateDefaultAttribis skipped, leaving the dangling pointer in place. However,ContextVkcallsmRenderPassCommands->buffersVertexAttribRead(..., maxAttrib). Since the new program’smaxAttribisB + 1, the loop iterates over indexAand dereferences the stale pointer, callingbuffer->recordReadBarrier(...).
Impact and Exploitability
By grooming the heap, an attacker can replace the freed BufferHelper (approx 488 bytes) with controlled data. When recordReadBarrier is called on the forged object, it reads a forged mCurrentWriteEvent handle.
- Arbitrary Increment: The forged handle is passed to
eventCollector->emplace_back, invokingRefCountedEvent’s copy constructor, which callsmHandle->addRef(). This performs an atomic 32-bit increment (mRefCount++) at the attacker-supplied memory address. - Driver-Level Execution: Later, when the event collector is cleared, the forged event’s destructor calls
vkDestroyEvent(device, mHandle->event, nullptr). Passing an attacker-controlled handle to the Vulkan driver can allow the attacker to free arbitrary driver memory or hijack execution, leading to full Remote Code Execution (RCE) in the GPU process. Note that the GPU process is unsandboxed on Android.
mCurrentArrayBuffers is a standard std::array of raw pointers, so it is not protected by MiraclePtr/BRP.
Potential Attacker Steps (Suggested)
Note: Our tooling agent does not have the ability to run code, these are suggested steps based on static analysis.
- Create a WebGL2 context and a VAO.
- Compile Program 1 (uses attribute 0) and Program 2 (uses attribute 1).
- Disable attribute 0 (
glDisableVertexAttribArray(0)). - Call
glUseProgram(Program 1)andglDrawArrays(...)to cache a defaultBufferHelperpointer for attribute 0. - Force a resize of the
DynamicBufferfor attribute 0 (e.g., enable it, set a divisor, and draw a massive number of instances). - Trigger a flush (e.g.,
glReadPixels) to free the oldBufferHelperobject. - Groom the heap with controlled data to forge the
BufferHelperstructure at the freed location. - Call
glUseProgram(Program 2)andglDrawArrays(...)to trigger the UAF and execute the exploit primitives.
Suggested Fix
Replace the raw pointers in gl::AttribArray<vk::BufferHelper *> mCurrentArrayBuffers with a safer mechanism. Options include:
- Using
BackupRefPtr(e.g.,base::raw_ptr<vk::BufferHelper>) to immediately mitigate the exploitability by turning the UAF into a safe crash. - Having
VertexArrayVkobserve the destruction ofBufferHelperobjects or strictly clearingmCurrentArrayBuffersfor indices that are not active in the current program duringsetupDraw.
Evaluated with Chrome root at commit: c0eb5541aebfa4ea08806eaf6e94bcc69f87ab2f
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.