Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in ANGLE
DescriptionUse after free in ANGLE
ComponentANGLE
Bug ClassUAF
Tracker498872764
Fix commitc0d3d631ba99 (angle/angle) +45/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Files Changed

  • src/libANGLE/renderer/vulkan/FramebufferVk.cpp
  • src/tests/gl_tests/TextureTest.cpp
From c0d3d631ba995e9b604c7755ed1bd57b1036f85e Mon Sep 17 00:00:00 2001
From: Shahbaz Youssefi <syoussefi@chromium.org>
Date: Wed, 08 Apr 2026 10:46:15 -0400
Subject: [PATCH] Vulkan: Fix clean up of FSR image

Bug: chromium:498872764
Change-Id: Id708656effead53182ca2820b5827fc0cef6b055
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7736774
Reviewed-by: Charlie Lao <cclao@google.com>
Commit-Queue: Shahbaz Youssefi <syoussefi@chromium.org>
---

diff --git a/src/libANGLE/renderer/vulkan/FramebufferVk.cpp b/src/libANGLE/renderer/vulkan/FramebufferVk.cpp
index 9b8968f..4577593 100644
--- a/src/libANGLE/renderer/vulkan/FramebufferVk.cpp
+++ b/src/libANGLE/renderer/vulkan/FramebufferVk.cpp
@@ -472,6 +472,8 @@
 
     if (mFragmentShadingRateImage.valid())
     {
+        contextVk->finalizeImageLayout(&mFragmentShadingRateImage, {});
+
         vk::Renderer *renderer = contextVk->getRenderer();
         mFragmentShadingRateImageView.release(renderer, mFragmentShadingRateImage.getResourceUse());
         mFragmentShadingRateImage.releaseImage(renderer);
diff --git a/src/tests/gl_tests/TextureTest.cpp b/src/tests/gl_tests/TextureTest.cpp
index 11cb22f..3699645 100644
--- a/src/tests/gl_tests/TextureTest.cpp
+++ b/src/tests/gl_tests/TextureTest.cpp
@@ -14982,6 +14982,49 @@
     EXPECT_PIXEL_COLOR_EQ(0, 0, angle::GLColor::blue);
 }
 
+// Test destruction of foveated framebuffer while render pass is open.  Regression test for a bug
+// where the foveation image owned by the framebuffer wasn't properly handled upon framebuffer
+// destruction.
+TEST_P(Texture2DTestES3Foveation, DestroyFramebufferWhileRenderPassIsOpen)
+{
+    ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_QCOM_framebuffer_foveated"));
+
+    // Switch to foveated framebuffer
+    GLTexture color;
+    glBindTexture(GL_TEXTURE_2D, color);
+    glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 64, 64);
+
+    GLFramebuffer fbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, color, 0);
+    ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+    // Just need 1 focal point
+    GLuint providedFeatures = 0;
+    glFramebufferFoveationConfigQCOM(fbo, 1, 1, GL_FOVEATION_ENABLE_BIT_QCOM, &providedFeatures);
+    ASSERT_NE(providedFeatures & GL_FOVEATION_ENABLE_BIT_QCOM, 0u);
+    // Set foveation parameters
+    glFramebufferFoveationParametersQCOM(fbo, 0, 0, 0.0f, 0.0f, 8.0f, 8.0f, 0.0f);
+    EXPECT_GL_NO_ERROR();
+
+    ANGLE_GL_PROGRAM(drawGreen, essl1_shaders::vs::Simple(), essl1_shaders::fs::Green());
+    glUseProgram(drawGreen);
+
+    // Start a render pass with the foveated framebuffer
+    drawQuad(drawGreen, essl1_shaders::PositionAttrib(), 0.5f);
+    EXPECT_GL_NO_ERROR();
+
+    // Switch to another framebuffer
+    glBindFramebuffer(GL_FRAMEBUFFER, 0);
+
+    // Delete the original framebuffer while the render pass is still open.
+    fbo.reset();
+
+    // Start a new render pass on the default framebuffer.
+    drawQuad(drawGreen, essl1_shaders::PositionAttrib(), 0.5f);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
+}
+
 // QCOM framebuffer foveated rendering with MSAA framebuffer
 TEST_P(Texture2DTestES3Foveation, DrawWithMsaaFramebuffer)
 {
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/gl_tests/TextureTest.cpp b/src/tests/gl_tests/TextureTest.cpp
index 11cb22f..3699645 100644
--- a/src/tests/gl_tests/TextureTest.cpp
+++ b/src/tests/gl_tests/TextureTest.cpp
@@ -14982,6 +14982,49 @@
     EXPECT_PIXEL_COLOR_EQ(0, 0, angle::GLColor::blue);
 }
 
+// Test destruction of foveated framebuffer while render pass is open.  Regression test for a bug
+// where the foveation image owned by the framebuffer wasn't properly handled upon framebuffer
+// destruction.
+TEST_P(Texture2DTestES3Foveation, DestroyFramebufferWhileRenderPassIsOpen)
+{
+    ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_QCOM_framebuffer_foveated"));
+
+    // Switch to foveated framebuffer
+    GLTexture color;
+    glBindTexture(GL_TEXTURE_2D, color);
+    glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 64, 64);
+
+    GLFramebuffer fbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, color, 0);
+    ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+    // Just need 1 focal point
+    GLuint providedFeatures = 0;
+    glFramebufferFoveationConfigQCOM(fbo, 1, 1, GL_FOVEATION_ENABLE_BIT_QCOM, &providedFeatures);
+    ASSERT_NE(providedFeatures & GL_FOVEATION_ENABLE_BIT_QCOM, 0u);
+    // Set foveation parameters
+    glFramebufferFoveationParametersQCOM(fbo, 0, 0, 0.0f, 0.0f, 8.0f, 8.0f, 0.0f);
+    EXPECT_GL_NO_ERROR();
+
+    ANGLE_GL_PROGRAM(drawGreen, essl1_shaders::vs::Simple(), essl1_shaders::fs::Green());
+    glUseProgram(drawGreen);
+
+    // Start a render pass with the foveated framebuffer
+    drawQuad(drawGreen, essl1_shaders::PositionAttrib(), 0.5f);
+    EXPECT_GL_NO_ERROR();
+
+    // Switch to another framebuffer
+    glBindFramebuffer(GL_FRAMEBUFFER, 0);
+
+    // Delete the original framebuffer while the render pass is still open.
+    fbo.reset();
+
+    // Start a new render pass on the default framebuffer.
+    drawQuad(drawGreen, essl1_shaders::PositionAttrib(), 0.5f);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
+}
+
 // QCOM framebuffer foveated rendering with MSAA framebuffer
 TEST_P(Texture2DTestES3Foveation, DrawWithMsaaFramebuffer)
 {
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential UAF in ANGLE Vulkan via dangling FragmentShadingRateImage pointer

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 security team.

Overview: A potential Use-After-Free (UAF) vulnerability exists in ANGLE’s Vulkan backend due to deferred render pass closure. A bare pointer to an inline ImageHelper is cached during a render pass and dereferenced later, leading to a UAF if the parent Framebuffer is synchronously deleted. Exploitation provides a highly reliable arbitrary 32-bit decrement primitive in the GPU process.

Affected files:

  • third_party/angle/src/libANGLE/renderer/vulkan/FramebufferVk.cpp
  • third_party/angle/src/libANGLE/renderer/vulkan/vk_helpers.cpp
  • third_party/angle/src/libANGLE/renderer/vulkan/FramebufferVk.h
  • third_party/angle/src/libANGLE/renderer/vulkan/vk_helpers.h

Estimated timestamp from git blame: 2025-09-08

Background

In ANGLE’s Vulkan backend, FramebufferVk contains an inline embedded vk::ImageHelper member named mFragmentShadingRateImage. When a render pass starts using a framebuffer with a fragment shading rate attachment, the address of this inline member (&mFragmentShadingRateImage) is stored as a bare ImageHelper* in RenderPassCommandBufferHelper::mFragmentShadingRateAtachment.mImage.

The Vulnerability

A Use-After-Free condition occurs because gl::Framebuffer objects can be deleted synchronously while their associated render passes are deferred and left active in the background for performance reasons.

Here are the potential steps an attacker might follow to exploit this:

  1. Foveation Initialization: A compromised renderer creates a texture with GL_TEXTURE_FOVEATED_FEATURE_BITS_QCOM (reachable via the passthrough command buffer on supported hardware like Adreno) and attaches it to FBO X.
  2. Pointer Caching: The attacker binds FBO X and issues a draw call. FramebufferVk::startNewRenderPass is called, which passes the address of the inline mFragmentShadingRateImage to RenderPassCommandBufferHelper::fragmentShadingRateImageRead. This stores the bare pointer in mImage.
  3. Deferred Render Pass Closure: The attacker binds a different framebuffer (FBO Y). ANGLE processes the binding change but, for performance optimization, does not immediately flush or close the existing render pass. The RenderPassCommandBufferHelper remains alive in the background.
  4. Synchronous Deletion: The attacker deletes FBO X via glDeleteFramebuffers. Because FBO X is unbound, the deletion executes synchronously. The underlying FramebufferVk object is freed via SafeDelete(mImpl), and because mFragmentShadingRateImage is inline, its backing memory is freed as well.
  5. Heap Spraying: The attacker sprays the heap to reclaim the freed FramebufferVk chunk. They specifically craft the mLastNonShaderReadOnlyEvent member of the sprayed ImageHelper. This member is a RefCountedEvent containing a bare pointer RefCounted<EventAndStage>* mHandle. The attacker sets mHandle to an arbitrary target memory address.
  6. UAF Trigger: The attacker issues a draw call on FBO Y, forcing the deferred render pass to flush. RenderPassCommandBufferHelper::endRenderPass retrieves the dangling mImage pointer and passes it to updateImageLayoutAndBarrier.
  7. Arbitrary Decrement Primitive: The layout transition logic executes mLastNonShaderReadOnlyEvent.release(context), which calls mHandle->getAndReleaseRef() == 1. Because mRefCount is the first member of the RefCounted<T> struct at offset +0, this executes a 32-bit decrement (mRefCount--) exactly at the attacker’s chosen mHandle address. This provides a powerful, highly reliable primitive to corrupt lengths/sizes and achieve RCE in the GPU process.

(Note: These are suggested steps based on source code analysis. Our tooling agent does not yet have the ability to run code to produce a working Proof of Concept).

Suggested Fix

The vulnerability fundamentally stems from caching a bare pointer to an object with a strictly shorter lifetime. Potential fixes include:

  • Refactoring mFragmentShadingRateImage out of the inline FramebufferVk memory and managing it via reference counting or std::shared_ptr so it survives as long as the RenderPassCommandBufferHelper needs it.
  • Having FramebufferVk::destroy explicitly walk the active command buffers and clear out any cached attachments originating from itself.
  • Adopting raw_ptr<T> (MiraclePtr) for RenderPassAttachment::mImage, which would mitigate the UAF into a controlled crash.

Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33


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