CVE-2026-11040
Overview
Files Changed
src/libANGLE/renderer/vulkan/ContextVk.cppsrc/libANGLE/renderer/vulkan/FramebufferVk.cppsrc/libANGLE/renderer/vulkan/RenderbufferVk.cppsrc/libANGLE/renderer/vulkan/ShareGroupVk.cppsrc/libANGLE/renderer/vulkan/ShareGroupVk.hsrc/libANGLE/renderer/vulkan/TextureVk.cppsrc/libANGLE/renderer/vulkan/vk_helpers.cpp
Patch
From 7757353a17f7b8fee0e766a3069455ecb735599d Mon Sep 17 00:00:00 2001 From: Charlie Lao <[email protected]> Date: Wed, 01 Apr 2026 14:57:33 -0700 Subject: [PATCH] Vulkan: Remove image from mImagesWithTileMemory when fallback When we fallback from tile memory, make sure we remove it from mImagesWithTileMemory. Otherwise, when image is released, it walks mImagesWithTileMemory and check mUsesTileMemory and is false, it will keep it in the list even though image is released. There is no actual bug exist yet, due to almost all cases when we fallback, we will always end up with submitCommands which will make mImagesWithTileMemory empty. But this change will make it more robust. Bug: b/498371085 Change-Id: I6f620e46ba2cddba06004539e640cc8626c76e91 Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7723223 Reviewed-by: Shahbaz Youssefi <[email protected]> Reviewed-by: Yuxin Hu <[email protected]> Commit-Queue: Charlie Lao <[email protected]> --- diff --git a/src/libANGLE/renderer/vulkan/ContextVk.cpp b/src/libANGLE/renderer/vulkan/ContextVk.cpp index 93a9617..8fad6f7 100644 --- a/src/libANGLE/renderer/vulkan/ContextVk.cpp +++ b/src/libANGLE/renderer/vulkan/ContextVk.cpp @@ -9034,7 +9034,6 @@ bool ContextVk::isImageWithTileMemoryFinalized(const vk::ImageHelper *image) const { - ASSERT(image->useTileMemory()); return std::find(mImagesWithTileMemory.begin(), mImagesWithTileMemory.end(), image) == mImagesWithTileMemory.end(); } @@ -9042,32 +9041,30 @@ angle::Result ContextVk::finalizeImagesWithTileMemory() { ASSERT(!mImagesWithTileMemory.empty()); + std::vector<vk::ImageHelper *> imagesToClearForSimulation; // Check all images with tile memory to see if they have valid content or not. tile memory are // transient, we must reallocate to keep data valid across command buffer boundary. - for (auto iter = mImagesWithTileMemory.begin(); iter != mImagesWithTileMemory.end();) + while (!mImagesWithTileMemory.empty()) { - vk::ImageHelper *image = *iter; + vk::ImageHelper *image = mImagesWithTileMemory.back(); + ASSERT(image->useTileMemory()); + mImagesWithTileMemory.pop_back(); + // Other context may have submitted command buffer and causes it fallback already, so check // again. - if (image->isVkImageContentDefined() && image->useTileMemory()) + if (image->isVkImageContentDefined()) { ANGLE_TRY(image->fallbackFromTileMemory(this)); ASSERT(!image->useTileMemory()); - iter = mImagesWithTileMemory.erase(iter); } - else + else if (!getFeatures().supportsTileMemoryHeap.enabled) { - ++iter; + imagesToClearForSimulation.push_back(image); } } - if (getFeatures().supportsTileMemoryHeap.enabled) - { - // We dont explicitly unbind tileMemory here. They occur implicitly at endCommandBiuffer - // time - } - else + if (!imagesToClearForSimulation.empty()) { ASSERT(getFeatures().simulateTileMemoryForTesting.enabled); @@ -9077,7 +9074,7 @@ params.layer = 0; params.clearValue = {}; params.clearArea = gl::Box(0, 0, 0, 0, 0, 1); - for (vk::ImageHelper *image : mImagesWithTileMemory) + for (vk::ImageHelper *image : imagesToClearForSimulation) { // Other context may have triggered fallback already, so check // again. @@ -9100,10 +9097,13 @@ image->invalidateEntireLevelStencilContent(this, gl::LevelIndex(0)); } } + + imagesToClearForSimulation.clear(); + // clearTextureNoFlush will end up add it back to mImagesWithTileMemory. + mImagesWithTileMemory.clear(); } - mImagesWithTileMemory.clear(); - + ASSERT(mImagesWithTileMemory.empty()); return angle::Result::Continue; } diff --git a/src/libANGLE/renderer/vulkan/FramebufferVk.cpp b/src/libANGLE/renderer/vulkan/FramebufferVk.cpp index 4577593..274530b 100644 --- a/src/libANGLE/renderer/vulkan/FramebufferVk.cpp +++ b/src/libANGLE/renderer/vulkan/FramebufferVk.cpp @@ -1289,14 +1289,12 @@ if (!readImage.canTransferFrom()) { ASSERT(readImage.useTileMemory()); - readImage.finalizeImageLayoutInShareContexts(renderer, contextVk, {}); ANGLE_TRY(readImage.fallbackFromTileMemory(contextVk)); } if (!drawImage.canTransferTo()) { ASSERT(drawImage.useTileMemory()); - drawImage.finalizeImageLayoutInShareContexts(renderer, contextVk, {}); ANGLE_TRY(drawImage.fallbackFromTileMemory(contextVk)); } } diff --git a/src/libANGLE/renderer/vulkan/RenderbufferVk.cpp b/src/libANGLE/renderer/vulkan/RenderbufferVk.cpp index 8140f9f..920dde1 100644 --- a/src/libANGLE/renderer/vulkan/RenderbufferVk.cpp +++ b/src/libANGLE/renderer/vulkan/RenderbufferVk.cpp @@ -332,7 +332,7 @@ { if (mImage) { - mImage->finalizeImageLayoutInShareContexts(renderer, contextVk, mImageSiblingSerial); + mImage->finalizeImageLayoutInShareContexts(contextVk, mImageSiblingSerial); } mImage = nullptr; mImageObserverBinding.bind(nullptr); diff --git a/src/libANGLE/renderer/vulkan/ShareGroupVk.cpp b/src/libANGLE/renderer/vulkan/ShareGroupVk.cpp index 1ae69af..418b968 100644 --- a/src/libANGLE/renderer/vulkan/ShareGroupVk.cpp +++ b/src/libANGLE/renderer/vulkan/ShareGroupVk.cpp @@ -393,4 +393,15 @@ } } } + +void ShareGroupVk::imageWillFallbackFromTileMemory(vk::ImageHelper *image) +{ + ASSERT(image->useTileMemory()); + for (auto context : mState.getContexts()) + { + ContextVk *contextVk = vk::GetImpl(context.second); + contextVk->finalizeImageLayout(image, {}); + contextVk->removeImageWithTileMemory(image); + } +} } // namespace rx diff --git a/src/libANGLE/renderer/vulkan/ShareGroupVk.h b/src/libANGLE/renderer/vulkan/ShareGroupVk.h index 7692628..5954559 100644 --- a/src/libANGLE/renderer/vulkan/ShareGroupVk.h +++ b/src/libANGLE/renderer/vulkan/ShareGroupVk.h @@ -106,6 +106,8 @@ void onFrameBoundary(); uint32_t getCurrentFrameCount() const { return mCurrentFrameCount; } + void imageWillFallbackFromTileMemory(vk::ImageHelper *image); + private: angle::Result updateContextsPriority(ContextVk *contextVk, egl::ContextPriority newPriority); diff --git a/src/libANGLE/renderer/vulkan/TextureVk.cpp b/src/libANGLE/renderer/vulkan/TextureVk.cpp index 3de8b87..948fc8e 100644 --- a/src/libANGLE/renderer/vulkan/TextureVk.cpp +++ b/src/libANGLE/renderer/vulkan/TextureVk.cpp @@ -4380,7 +4380,7 @@ } else { - mImage->finalizeImageLayoutInShareContexts(renderer, contextVk, mImageSiblingSerial); + mImage->finalizeImageLayoutInShareContexts(contextVk, mImageSiblingSerial); mImageObserverBinding.bind(nullptr); mImage = nullptr; } diff --git a/src/libANGLE/renderer/vulkan/vk_helpers.cpp b/src/libANGLE/renderer/vulkan/vk_helpers.cpp index 2d012de..a7abed6 100644 --- a/src/libANGLE/renderer/vulkan/vk_helpers.cpp +++ b/src/libANGLE/renderer/vulkan/vk_helpers.cpp @@ -6122,26 +6122,29 @@ ContextVk *contextVk, UniqueSerial imageSiblingSerial) { - finalizeImageLayoutInShareContexts(renderer, contextVk, imageSiblingSerial); + finalizeImageLayoutInShareContexts(contextVk, imageSiblingSerial); contextVk->addToPendingImageGarbage(mUse, mAllocationSize); releaseImage(renderer); } -void ImageHelper::finalizeImageLayoutInShareContexts(Renderer *renderer, - ContextVk *contextVk, +void ImageHelper::finalizeImageLayoutInShareContexts(ContextVk *contextVk, UniqueSerial imageSiblingSerial) { if (contextVk && mImageSerial.valid()) {
Regression Test / PoC
diff --git a/src/tests/gl_tests/VulkanPerformanceCounterTest.cpp b/src/tests/gl_tests/VulkanPerformanceCounterTest.cpp
index 2ccf441..03223a3 100644
--- a/src/tests/gl_tests/VulkanPerformanceCounterTest.cpp
+++ b/src/tests/gl_tests/VulkanPerformanceCounterTest.cpp
@@ -9636,6 +9636,60 @@
EXPECT_PIXEL_RECT_EQ(0, 0, getWindowWidth(), getWindowHeight(), GLColor::green);
}
+// Regression test for UAF in finalizeImagesWithTileMemory after readPixels triggered
+// fallbackFromTileMemory.
+TEST_P(VulkanPerformanceCounterTest_TileMemory, DepthBufferPBOReadThenDelete)
+{
+ ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled(kPerfMonitorExtensionName));
+ ANGLE_SKIP_TEST_IF(!isFeatureEnabled(Feature::SimulateTileMemoryForTesting) &&
+ !isFeatureEnabled(Feature::SupportsTileMemoryHeap));
+ ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_NV_read_depth_stencil"));
+
+ setupPrograms();
+
+ GLenum depthStencilFormat = GL_DEPTH24_STENCIL8;
+ constexpr GLsizei kWidth = 256;
+ constexpr GLsizei kHeight = 256;
+
+ uint64_t tileMemoryImageCountBefore = getPerfCounters().tileMemoryImages;
+
+ GLTexture colorTexture;
+ GLRenderbuffer depthStencil;
+ setupColorTextureAndDepthBuffer(colorTexture, depthStencil, depthStencilFormat, kWidth,
+ kHeight);
+ GLFramebuffer fbo;
+ setupFBO(colorTexture, depthStencil, depthStencil, fbo, kWidth, kHeight);
+
+ GLfloat depthValue = 0.0f;
+ // Clear color/depth/stencil buffers
+ glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
+ glClearDepthf(depthValue * 0.5f + 0.5f);
+ glClearStencil(0x55);
+ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
+ glEnable(GL_DEPTH_TEST);
+ drawQuadToVerifyDepthValue(drawGreen, drawRed, depthValue);
+ EXPECT_EQ(1u, getPerfCounters().tileMemoryImages - tileMemoryImageCountBefore);
+
+ // PBO read
+ GLBuffer pbo;
+ glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo);
+ glBufferData(GL_PIXEL_PACK_BUFFER, kWidth * kHeight * sizeof(uint32_t), nullptr,
+ GL_DYNAMIC_READ);
+ glReadPixels(0, 0, kWidth, kHeight, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, 0);
+ glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
+ EXPECT_GL_NO_ERROR();
+
+ // Delete depthBuffer
+ depthStencil.reset();
+
+ std::array<GLenum, 2> attachments = {GL_DEPTH_ATTACHMENT, GL_STENCIL_ATTACHMENT};
+ glInvalidateFramebuffer(GL_FRAMEBUFFER, attachments.size(), attachments.data());
+
+ // For completeness, verify rendering results.
+ EXPECT_PIXEL_RECT_EQ(0, 0, kWidth, kHeight, GLColor::green);
+ EXPECT_GL_NO_ERROR();
+}
+
class VulkanPerformanceCounterTest_Dither : public VulkanPerformanceCounterTest
{};
Original Bug Report
Potential Use-After-Free in ANGLE Vulkan ContextVk::mImagesWithTileMemory
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A potential Use-After-Free vulnerability exists in ANGLE’s Vulkan backend on devices supporting tile memory (e.g., Qualcomm Adreno). An image falling back from tile memory clears its tracking flag without being removed from the context’s tracking list, causing it to be skipped during destruction cleanup. Subsequent command submissions dereference the freed pointer, which could allow a compromised renderer to execute arbitrary code in the GPU process.
Affected files:
third_party/angle/src/libANGLE/renderer/vulkan/vk_helpers.cppthird_party/angle/src/libANGLE/renderer/vulkan/ContextVk.cppthird_party/angle/src/libANGLE/renderer/vulkan/ContextVk.hthird_party/angle/src/libANGLE/renderer/vulkan/RenderbufferVk.cpp
Estimated timestamp from git blame: 2026-01-08
Vulnerability Details
In ANGLE’s Vulkan backend, ContextVk maintains a list of images currently utilizing transient tile memory via std::vector<vk::ImageHelper *> mImagesWithTileMemory.
The vulnerability stems from a state desynchronization when an image is forced out of tile memory before its destruction:
- When
glReadPixelsis called on an active tile memory attachment, Vulkan transfer limitations force ANGLE to fall back to regular memory. vk::ImageHelper::readPixelsImplcallsfallbackFromTileMemory(vk_helpers.cpp), which copies the image state and explicitly setsmUseTileMemory = falseon the originalImageHelperobject.- Crucially,
fallbackFromTileMemorydoes not remove theImageHelperfromContextVk::mImagesWithTileMemory. - When the attacker subsequently deletes the renderbuffer,
RenderbufferVk::onDestroyinitiates image cleanup. - The cleanup routine calls
ImageHelper::finalizeImageLayoutInShareContexts. This function dictates list removal using the conditionif (mUseTileMemory) { ... removeImageWithTileMemory(this); }. - Because
mUseTileMemorywas previously cleared, the removal loop is bypassed. TheImageHelperobject is then freed viaSafeDelete, leaving a dangling raw pointer insideContextVk::mImagesWithTileMemory.
When a context containing the dangling pointer flushes its command buffer (e.g., via glFlush), ContextVk::finalizeImagesWithTileMemory iterates over mImagesWithTileMemory, dereferencing the freed pointer and invoking methods on it.
Potential Impact
This is a potential Renderer-to-GPU sandbox escape. ImageHelper inherits from angle::Subject, which contains an inline FastVector of observer pointers. By grooming the heap, an attacker can reclaim the freed ImageHelper memory and populate the observer list with a fake ObserverBindingBase pointer.
When ContextVk::finalizeImagesWithTileMemory calls fallbackFromTileMemory on the attacker-controlled object, it eventually triggers onStateChange(). This iterates over the attacker’s fake observers and executes a virtual function call (binding->getObserver()->onSubjectStateChange()), granting Remote Code Execution (RCE) in the GPU process. MiraclePtr (BRP) does not mitigate this, as the vector stores raw pointers.
Suggested Reproduction Steps
Note: These are theoretical steps, as our AI tooling does not currently possess the ability to run and verify proof-of-concept code.
- Prerequisite: Run Chrome on a device with a Vulkan driver supporting
VK_QCOM_tile_memory_heap. - In WebGL, create a
GL_DEPTH_COMPONENT24renderbuffer, attach it to an FBO, and issue a draw call to place the attachment into the context’smImagesWithTileMemorylist. - Bind a PBO and call
glReadPixelson the depth attachment. This triggersfallbackFromTileMemory, settingmUseTileMemory = false. - Call
glDeleteRenderbufferson the renderbuffer. TheImageHelperis deleted, but the pointer remains in the context’s tracking list. - Allocate similarly sized WebGL objects to reclaim the freed memory chunk, injecting a fake
angle::Subjectstate and vtable pointers. - Call
glFlush()to triggerContextVk::submitCommands, which loops over the dangling pointer and executes the hijacked virtual call.
Proposed Fix
There are two primary ways to fix this:
- Update Tracking in Fallback: Ensure that
ImageHelper::fallbackFromTileMemoryexplicitly callsremoveImageWithTileMemoryfor all contexts in the share group before (or concurrently with) settingmUseTileMemorytofalse. - Unconditional Removal: Modify
ImageHelper::finalizeImageLayoutInShareContextsto unconditionally check if the image is in themImagesWithTileMemorylist, rather than gating the removal loop behind theif (mUseTileMemory)check.
Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33
Results from 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.