Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in ANGLE
DescriptionInsufficient validation of untrusted input in ANGLE
ComponentANGLE
Bug ClassLogic Error
Tracker513920834
Fix commitb29ddb229672 (angle/angle) +35/-4
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Files Changed

  • src/libANGLE/Framebuffer.cpp
  • src/libANGLE/renderer/d3d/FramebufferD3D.cpp
  • src/tests/gl_tests/WebGLCompatibilityTest.cpp
From b29ddb229672bf3890bd570878efd73c3060bc00 Mon Sep 17 00:00:00 2001
From: Geoff Lang <geofflang@chromium.org>
Date: Mon, 25 May 2026 12:30:39 -0400
Subject: [PATCH] Validate unique framebuffer attachments for hardened contexts.

This behaviour was already done for WebGL and some backends.

Fixed: chromium:513920834
Change-Id: Ifedf64a9b9be2265fa0f42af4f05586dd45611cd
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7870307
Reviewed-by: Shahbaz Youssefi <syoussefi@chromium.org>
Commit-Queue: Shahbaz Youssefi <syoussefi@chromium.org>
---

diff --git a/src/libANGLE/Framebuffer.cpp b/src/libANGLE/Framebuffer.cpp
index 03d8d09..9168b54 100644
--- a/src/libANGLE/Framebuffer.cpp
+++ b/src/libANGLE/Framebuffer.cpp
@@ -1604,7 +1604,7 @@
     // The WebGL conformance tests implicitly define that all framebuffer
     // attachments must be unique. For example, the same level of a texture can
     // not be attached to two different color attachments.
-    if (state.getExtensions().webglCompatibilityANGLE)
+    if (context->isWebGL() || context->isHardenedContext())
     {
         if (!mState.colorAttachmentsAreUniqueImages())
         {
diff --git a/src/libANGLE/renderer/d3d/FramebufferD3D.cpp b/src/libANGLE/renderer/d3d/FramebufferD3D.cpp
index 83622df..a00a277 100644
--- a/src/libANGLE/renderer/d3d/FramebufferD3D.cpp
+++ b/src/libANGLE/renderer/d3d/FramebufferD3D.cpp
@@ -263,9 +263,9 @@
     }
 
     // D3D11 does not allow for overlapping RenderTargetViews.
-    // If WebGL compatibility is enabled, this has already been checked at a higher level.
-    ASSERT(!context->isWebGL() || mState.colorAttachmentsAreUniqueImages());
-    if (!context->isWebGL())
+    // If WebGL compatibility (or hardened context) is enabled, this has already been checked at a
+    // higher level.
+    if (!context->isWebGL() && !context->isHardenedContext())
     {
         if (!mState.colorAttachmentsAreUniqueImages())
         {
@@ -274,6 +274,7 @@
                 gl::err::kFramebufferIncompleteUnsupportedNonUniqueAttachments);
         }
     }
+    ASSERT(mState.colorAttachmentsAreUniqueImages());
 
     // D3D requires all render targets to have the same dimensions.
     if (!mState.attachmentsHaveSameDimensions())
diff --git a/src/tests/gl_tests/WebGLCompatibilityTest.cpp b/src/tests/gl_tests/WebGLCompatibilityTest.cpp
index 4c253a5..653e8ea 100644
--- a/src/tests/gl_tests/WebGLCompatibilityTest.cpp
+++ b/src/tests/gl_tests/WebGLCompatibilityTest.cpp
@@ -4557,6 +4557,36 @@
     drawBuffersFeedbackLoop(program, {{GL_COLOR_ATTACHMENT0, GL_NONE}}, GL_INVALID_OPERATION);
 }
 
+// WebGL requires that all framebuffer attachments are unique
+TEST_P(WebGL2CompatibilityTest, UniqueFramebufferAttachments)
+{
+    GLTexture tex0;
+    FillTexture2D(tex0, 8, 8, GLColor::red, 0, GL_RGBA, GL_RGBA, GL_UNSIGNED_BYTE);
+    ASSERT_GL_NO_ERROR();
+
+    GLFramebuffer fbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex0, 0);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, tex0, 0);
+    EXPECT_GLENUM_EQ(GL_FRAMEBUFFER_UNSUPPORTED, glCheckFramebufferStatus(GL_FRAMEBUFFER));
+}
+
+// Hardened contexts also require that all framebuffer color attachments are unique
+TEST_P(HardenedContextTest, UniqueFramebufferAttachments)
+{
+    ANGLE_SKIP_TEST_IF(getClientMajorVersion() < 3);
+
+    GLTexture tex0;
+    FillTexture2D(tex0, 8, 8, GLColor::red, 0, GL_RGBA, GL_RGBA, GL_UNSIGNED_BYTE);
+    ASSERT_GL_NO_ERROR();
+
+    GLFramebuffer fbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex0, 0);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, tex0, 0);
+    EXPECT_GLENUM_EQ(GL_FRAMEBUFFER_UNSUPPORTED, glCheckFramebufferStatus(GL_FRAMEBUFFER));
+}
+
 // This tests that texture base level for immutable textures is clamped to the valid range, unlike
 // for non-immutable textures, for purposes of validation. Related to WebGL test
 // conformance2/textures/misc/immutable-tex-render-feedback.html
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/gl_tests/WebGLCompatibilityTest.cpp b/src/tests/gl_tests/WebGLCompatibilityTest.cpp
index 4c253a5..653e8ea 100644
--- a/src/tests/gl_tests/WebGLCompatibilityTest.cpp
+++ b/src/tests/gl_tests/WebGLCompatibilityTest.cpp
@@ -4557,6 +4557,36 @@
     drawBuffersFeedbackLoop(program, {{GL_COLOR_ATTACHMENT0, GL_NONE}}, GL_INVALID_OPERATION);
 }
 
+// WebGL requires that all framebuffer attachments are unique
+TEST_P(WebGL2CompatibilityTest, UniqueFramebufferAttachments)
+{
+    GLTexture tex0;
+    FillTexture2D(tex0, 8, 8, GLColor::red, 0, GL_RGBA, GL_RGBA, GL_UNSIGNED_BYTE);
+    ASSERT_GL_NO_ERROR();
+
+    GLFramebuffer fbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex0, 0);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, tex0, 0);
+    EXPECT_GLENUM_EQ(GL_FRAMEBUFFER_UNSUPPORTED, glCheckFramebufferStatus(GL_FRAMEBUFFER));
+}
+
+// Hardened contexts also require that all framebuffer color attachments are unique
+TEST_P(HardenedContextTest, UniqueFramebufferAttachments)
+{
+    ANGLE_SKIP_TEST_IF(getClientMajorVersion() < 3);
+
+    GLTexture tex0;
+    FillTexture2D(tex0, 8, 8, GLColor::red, 0, GL_RGBA, GL_RGBA, GL_UNSIGNED_BYTE);
+    ASSERT_GL_NO_ERROR();
+
+    GLFramebuffer fbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex0, 0);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, tex0, 0);
+    EXPECT_GLENUM_EQ(GL_FRAMEBUFFER_UNSUPPORTED, glCheckFramebufferStatus(GL_FRAMEBUFFER));
+}
+
 // This tests that texture base level for immutable textures is clamped to the valid range, unlike
 // for non-immutable textures, for purposes of validation. Related to WebGL test
 // conformance2/textures/misc/immutable-tex-render-feedback.html
Loading diff…

Original Bug Report

reported by vm...@google.com

ANGLE Vulkan: Potential driver undefined behavior due to non-unique color attachments

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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: ANGLE’s Vulkan backend fails to ensure color attachments are unique images in non-WebGL contexts, violating Vulkan Valid Usage requirements. This can lead to driver-level undefined behavior in the GPU process when multiple attachments alias the same memory. This is especially critical on Android where the GPU process is unsandboxed.

Affected files:

  • third_party/angle/src/libANGLE/renderer/vulkan/FramebufferVk.cpp
  • third_party/angle/src/libANGLE/Framebuffer.cpp
  • third_party/angle/src/libANGLE/renderer/vulkan/vk_cache_utils.cpp
  • third_party/angle/src/libANGLE/renderer/vulkan/TextureVk.cpp
  • third_party/angle/src/libANGLE/renderer/vulkan/vk_helpers.cpp

Estimated timestamp from git blame: Unknown (Google3 checkout)

Summary

A potential security vulnerability exists in ANGLE’s Vulkan backend where it fails to enforce that color attachments in a framebuffer are unique images for non-WebGL contexts (e.g., OpenGL ES 3.0). This omission leads to a violation of Vulkan Valid Usage (VUID-VkRenderPassCreateInfo-pAttachments-00836), which forbids multiple attachments from aliasing the same memory in a single subpass unless the VK_ATTACHMENT_DESCRIPTION_MAY_ALIAS_BIT is set. ANGLE currently never sets this bit and does not prevent the aliasing, resulting in driver-level undefined behavior.

Root Cause Analysis

In ANGLE’s frontend (third_party/angle/src/libANGLE/Framebuffer.cpp), the check for unique color attachment images is guarded by the webglCompatibilityANGLE extension:

if (state.getExtensions().webglCompatibilityANGLE)
{
    if (!mState.colorAttachmentsAreUniqueImages())
    {
        return FramebufferStatus::Incomplete(GL_FRAMEBUFFER_UNSUPPORTED, ...);
    }
}

When a renderer requests a non-WebGL GLES context (which is allowed via IPC), this check is bypassed. While some backends like D3D11 duplicate this check in their own checkStatus implementation, the Vulkan backend (third_party/angle/src/libANGLE/renderer/vulkan/FramebufferVk.cpp) does not.

Furthermore, when creating the Vulkan render pass in third_party/angle/src/libANGLE/renderer/vulkan/vk_cache_utils.cpp, the VkAttachmentDescription2 structures are initialized without the VK_ATTACHMENT_DESCRIPTION_MAY_ALIAS_BIT flag, which is required if multiple attachments alias the same image memory.

Potential Trigger Path

Note: These steps are suggested based on code analysis; a functional proof-of-concept has not yet been executed.

  1. A compromised renderer process requests a non-WebGL context (e.g., CONTEXT_TYPE_OPENGLES3) from the GPU process.
  2. The renderer creates a single texture and attaches it to both GL_COLOR_ATTACHMENT0 and GL_COLOR_ATTACHMENT1 on a new Framebuffer Object (FBO).
  3. The renderer calls glCheckFramebufferStatus, which returns GL_FRAMEBUFFER_COMPLETE due to the frontend bypass and missing backend check.
  4. The renderer enables both attachments and issues a draw command (e.g., glDrawArrays).
  5. ANGLE’s Vulkan backend creates a VkFramebuffer containing duplicate VkImageView handles and a VkRenderPass where the attachments alias the same memory without the MAY_ALIAS bit.
  6. The graphics driver executes the command, violating Vulkan valid usage and potentially entering an exploitable undefined state.

Security Impact

Triggering driver-level undefined behavior represents a path for memory corruption within the GPU process. On platforms like Android, where the GPU process is unsandboxed, this could potentially be leveraged by an attacker to escape the renderer sandbox and gain full control over the GPU process.

Suggested Fix

The uniqueness check should be mirrored in the Vulkan backend’s status check to ensure color attachments are unique images for all context types. This can be achieved by adding a check to FramebufferVk::checkStatus in third_party/angle/src/libANGLE/renderer/vulkan/FramebufferVk.cpp similar to the one in the D3D backend.

Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

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