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
Tracker513257423
Fix commit1b18da44dff8 (angle/angle) +155/-18
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Changed Functions

FunctionChangeNotes
AdvancedBlendTestES32
src/tests/gl_tests/AdvancedBlendTest.cpp
modified

Files Changed

  • src/libANGLE/ErrorStrings.h
  • src/libANGLE/renderer/vulkan/ContextVk.cpp
  • src/libANGLE/renderer/vulkan/ContextVk.h
  • src/libANGLE/renderer/vulkan/FramebufferVk.cpp
  • src/libANGLE/validationES.cpp
  • src/tests/gl_tests/AdvancedBlendTest.cpp
From 1b18da44dff8704b82f36d15424c7ac0583ce0ac Mon Sep 17 00:00:00 2001
From: Shahbaz Youssefi <syoussefi@chromium.org>
Date: Tue, 09 Jun 2026 13:58:24 -0400
Subject: [PATCH] Vulkan: Fix advanced blend validation + backend skip

Advanced blend is only valid when used with draw buffer 0.  When
enabled, non-0 draw buffers shouldn't be enabled.

In the Vulkan backend, blend is skipped for disabled draw buffers
because Vulkan's validation is less forgiving to mixing blend and
advanced blend for disabled attachments (See
VUID-VkPipelineColorBlendAttachmentState-advancedBlendIndependentBlend-01407)

Bug: chromium:513257423
Change-Id: I9671015fef16bcb7b170b182905d481c37c2371d
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7914802
Reviewed-by: Amirali Abdolrashidi <abdolrashidi@google.com>
Commit-Queue: Shahbaz Youssefi <syoussefi@chromium.org>
---

diff --git a/src/libANGLE/ErrorStrings.h b/src/libANGLE/ErrorStrings.h
index 6a51ec0..78dba20 100644
--- a/src/libANGLE/ErrorStrings.h
+++ b/src/libANGLE/ErrorStrings.h
@@ -17,7 +17,7 @@
 // clang-format off
 inline constexpr const char *k3DDepthStencil = "Format cannot be GL_DEPTH_COMPONENT or GL_DEPTH_STENCIL if target is GL_TEXTURE_3D.";
 inline constexpr const char *kANGLECopyTextureMissingRequiredExtension = "Copy*TextureCHROMIUM from EXTERNAL_OES to integer format requires OES_EGL_image_external_essl3.";
-inline constexpr const char *kAdvancedBlendEquationWithMRT = "Advanced blend equation can only be used when only one draw buffer is not NONE.";
+inline constexpr const char *kAdvancedBlendEquationWithMRT = "Advanced blend equations are used with a draw buffer other than draw buffer zero or with multiple draw buffers.";
 inline constexpr const char *kAtomicCounterResourceName = "Active atomic counter resources are not assigned name strings.";
 inline constexpr const char *kAttributeListNotNull = "Attribute list must be NULL or GL_NONE.";
 inline constexpr const char *kAttributeNameNull = "Attribute name is null.";
diff --git a/src/libANGLE/renderer/vulkan/ContextVk.cpp b/src/libANGLE/renderer/vulkan/ContextVk.cpp
index 59664ca..187c4db 100644
--- a/src/libANGLE/renderer/vulkan/ContextVk.cpp
+++ b/src/libANGLE/renderer/vulkan/ContextVk.cpp
@@ -4817,6 +4817,18 @@
     }
 }
 
+void ContextVk::updateBlendEnabled()
+{
+    const gl::DrawBufferMask framebufferMask = mState.getDrawFramebuffer()->getDrawBufferMask();
+    const gl::DrawBufferMask enabledBlend    = mState.getBlendStateExt().getEnabledMask();
+
+    // Filter out blend for disabled attachments.  If advanced blend is enabled, Vulkan
+    // forbids blend from being specified on the other attachments (same as GL, but GL
+    // ignores blend on disabled attachments).
+    mGraphicsPipelineDesc->updateBlendEnabled(&mGraphicsPipelineTransition,
+                                              enabledBlend & framebufferMask);
+}
+
 void ContextVk::updateBlendFuncsAndEquations()
 {
     const gl::BlendStateExt &blendStateExt = mState.getBlendStateExt();
@@ -5476,8 +5488,7 @@
                 updateDepthRange(glState.getNearPlane(), glState.getFarPlane());
                 break;
             case gl::state::DIRTY_BIT_BLEND_ENABLED:
-                mGraphicsPipelineDesc->updateBlendEnabled(
-                    &mGraphicsPipelineTransition, glState.getBlendStateExt().getEnabledMask());
+                updateBlendEnabled();
                 updateDither();
                 updateAdvancedBlendEquations(programExecutable);
                 break;
@@ -5741,6 +5752,7 @@
                                glState.getFarPlane());
                 updateColorMasks();
                 updateMissingAttachments();
+                updateBlendEnabled();
                 updateRasterizationSamples(drawFramebufferVk->getSamples());
                 updateRasterizerDiscardEnabled(
                     mState.isQueryActive(gl::QueryType::PrimitivesGenerated));
@@ -6516,6 +6528,7 @@
 
     // Attachments might have changed.
     updateMissingAttachments();
+    updateBlendEnabled();
 
     if (mState.getProgramExecutable())
     {
diff --git a/src/libANGLE/renderer/vulkan/ContextVk.h b/src/libANGLE/renderer/vulkan/ContextVk.h
index a5f8c44..24d0a5540 100644
--- a/src/libANGLE/renderer/vulkan/ContextVk.h
+++ b/src/libANGLE/renderer/vulkan/ContextVk.h
@@ -491,6 +491,7 @@
                                        gl::SamplerFormat format,
                                        gl::Texture **textureOut);
     void updateColorMasks();
+    void updateBlendEnabled();
     void updateBlendFuncsAndEquations();
 
     void handleError(VkResult errorCode,
diff --git a/src/libANGLE/renderer/vulkan/FramebufferVk.cpp b/src/libANGLE/renderer/vulkan/FramebufferVk.cpp
index 952ce67..c45ceabe 100644
--- a/src/libANGLE/renderer/vulkan/FramebufferVk.cpp
+++ b/src/libANGLE/renderer/vulkan/FramebufferVk.cpp
@@ -2758,6 +2758,7 @@
     if (shouldUpdateColorMaskAndBlend)
     {
         contextVk->updateColorMasks();
+        contextVk->updateBlendEnabled();
         contextVk->updateBlendFuncsAndEquations();
     }
 
diff --git a/src/libANGLE/validationES.cpp b/src/libANGLE/validationES.cpp
index 8f02859..d46f55f 100644
--- a/src/libANGLE/validationES.cpp
+++ b/src/libANGLE/validationES.cpp
@@ -4081,28 +4081,27 @@
         }
     }
 
-    // Advanced blend equation can only be enabled for a single render target.
+    // Advanced blend equation can only be enabled for color attachment zero and with only that
+    // attachment enabled.
     const BlendStateExt &blendStateExt = state.getBlendStateExt();
-    if (ANGLE_UNLIKELY(blendStateExt.getUsesAdvancedBlendEquationMask().any()))
+    const gl::DrawBufferMask drawBuffersWithAdvancedBlend =
+        blendStateExt.getUsesAdvancedBlendEquationMask() & blendStateExt.getEnabledMask();
+    if (ANGLE_UNLIKELY(drawBuffersWithAdvancedBlend.any()))
     {
+        // Note: Framebuffer::getDrawBufferMask() excludes non-existing draw buffers.  But if
+        // glDrawBuffers sets them to non-NONE, draw with advanced blend is still expected to fail.
+        // So we can't do a quick check with getDrawBufferMask() alone.
         const size_t drawBufferCount            = framebuffer->getDrawbufferStateCount();
-        uint32_t advancedBlendRenderTargetCount = 0;
-
-        for (size_t drawBufferIndex : blendStateExt.getUsesAdvancedBlendEquationMask())
+        const bool advancedBlendOnDrawBufferZero =
+            drawBuffersWithAdvancedBlend[0] && framebuffer->getDrawBufferState(0) != GL_NONE;
+        for (size_t drawBufferIndex = 1; drawBufferIndex < drawBufferCount; ++drawBufferIndex)
         {
-            if (drawBufferIndex < drawBufferCount &&
-                framebuffer->getDrawBufferState(drawBufferIndex) != GL_NONE &&
-                blendStateExt.getEnabledMask().test(drawBufferIndex) &&
-                blendStateExt.getUsesAdvancedBlendEquationMask().test(drawBufferIndex))
+            if (framebuffer->getDrawBufferState(drawBufferIndex) != GL_NONE &&
+                (advancedBlendOnDrawBufferZero || drawBuffersWithAdvancedBlend[drawBufferIndex]))
             {
-                ++advancedBlendRenderTargetCount;
+                return kAdvancedBlendEquationWithMRT;
             }
         }
-
-        if (advancedBlendRenderTargetCount > 1)
-        {
-            return kAdvancedBlendEquationWithMRT;
-        }
     }
 
     // Dual-source blending functions limit the number of supported draw buffers.
diff --git a/src/tests/gl_tests/AdvancedBlendTest.cpp b/src/tests/gl_tests/AdvancedBlendTest.cpp
index 15a6c49..bc79347 100644
--- a/src/tests/gl_tests/AdvancedBlendTest.cpp
+++ b/src/tests/gl_tests/AdvancedBlendTest.cpp
@@ -34,6 +34,129 @@
 class AdvancedBlendTestES32 : public AdvancedBlendTest
 {};
 
+// Test that advanced blend cannot be used when a non-zero draw buffer is enabled.
+TEST_P(AdvancedBlendTest, NonZeroDrawBufferDisallowed)
+{
+    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_KHR_blend_equation_advanced"));
+
+    GLFramebuffer fbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+
+    std::array<GLRenderbuffer, 4> rbo;
+    for (uint32_t i = 0; i < 4; ++i)
+    {
+        glBindRenderbuffer(GL_RENDERBUFFER, rbo[i]);
+        glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 1, 1);
+        glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + i, GL_RENDERBUFFER,
+                                  rbo[i]);
+    }
+    EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+    ASSERT_GL_NO_ERROR();
+
+    GLenum enabled[4] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2,
+                         GL_COLOR_ATTACHMENT3};
+    glDrawBuffers(4, enabled);
+
+    constexpr char kVS[] = R"(#version 300 es
+precision highp float;
+void main()
+{
+    switch (gl_VertexID)
+    {
+        case 0:  gl_Position = vec4(-1, -1, 0, 1); break;
+        case 1:  gl_Position = vec4( 3, -1, 0, 1); break;
+        default: gl_Position = vec4(-1, 3, 0, 1); break;
+    }
+})";
+
+    constexpr char kFS[] = R"(#version 300 es
+#extension GL_KHR_blend_equation_advanced : require
+layout (blend_support_multiply) out;
+precision mediump float;
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/gl_tests/AdvancedBlendTest.cpp b/src/tests/gl_tests/AdvancedBlendTest.cpp
index 15a6c49..bc79347 100644
--- a/src/tests/gl_tests/AdvancedBlendTest.cpp
+++ b/src/tests/gl_tests/AdvancedBlendTest.cpp
@@ -34,6 +34,129 @@
 class AdvancedBlendTestES32 : public AdvancedBlendTest
 {};
 
+// Test that advanced blend cannot be used when a non-zero draw buffer is enabled.
+TEST_P(AdvancedBlendTest, NonZeroDrawBufferDisallowed)
+{
+    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_KHR_blend_equation_advanced"));
+
+    GLFramebuffer fbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+
+    std::array<GLRenderbuffer, 4> rbo;
+    for (uint32_t i = 0; i < 4; ++i)
+    {
+        glBindRenderbuffer(GL_RENDERBUFFER, rbo[i]);
+        glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 1, 1);
+        glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + i, GL_RENDERBUFFER,
+                                  rbo[i]);
+    }
+    EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+    ASSERT_GL_NO_ERROR();
+
+    GLenum enabled[4] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2,
+                         GL_COLOR_ATTACHMENT3};
+    glDrawBuffers(4, enabled);
+
+    constexpr char kVS[] = R"(#version 300 es
+precision highp float;
+void main()
+{
+    switch (gl_VertexID)
+    {
+        case 0:  gl_Position = vec4(-1, -1, 0, 1); break;
+        case 1:  gl_Position = vec4( 3, -1, 0, 1); break;
+        default: gl_Position = vec4(-1, 3, 0, 1); break;
+    }
+})";
+
+    constexpr char kFS[] = R"(#version 300 es
+#extension GL_KHR_blend_equation_advanced : require
+layout (blend_support_multiply) out;
+precision mediump float;
+out vec4 fragColor;
+void main()
+{
+    fragColor = vec4(1., 0., 0., 1.);
+})";
+    ANGLE_GL_PROGRAM(program, kVS, kFS);
+    glUseProgram(program);
+
+    glEnable(GL_BLEND);
+    glBlendEquation(GL_MULTIPLY_KHR);
+
+    // Invalid draw because all 4 draw buffers are enabled.
+    ASSERT_GL_NO_ERROR();
+    glDrawArrays(GL_TRIANGLES, 0, 3);
+    EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+
+    enabled[1] = GL_NONE;
+    enabled[2] = GL_NONE;
+    glDrawBuffers(4, enabled);
+
+    // Still invalid because draw buffer #3 is enabled.
+    ASSERT_GL_NO_ERROR();
+    glDrawArrays(GL_TRIANGLES, 0, 3);
+    EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+
+    enabled[0] = GL_NONE;
+    enabled[1] = GL_COLOR_ATTACHMENT1;
+    enabled[3] = GL_NONE;
+    glDrawBuffers(4, enabled);
+
+    // Even though a single draw buffer is enabled, it's not index 0 so it's still invalid
+    ASSERT_GL_NO_ERROR();
+    glDrawArrays(GL_TRIANGLES, 0, 3);
+    EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+
+    enabled[0] = GL_COLOR_ATTACHMENT0;
+    enabled[1] = GL_NONE;
+    glDrawBuffers(4, enabled);
+
+    // When only color attachment 0 is enabled, the draw is valid.
+    glClearColor(1, 1, 1, 1);
+    glClear(GL_COLOR_BUFFER_BIT);
+    glDrawArrays(GL_TRIANGLES, 0, 3);
+    ASSERT_GL_NO_ERROR();
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);
+
+    // It's also ok to render to multiple attachments if blend is not enabled on the attachment that
+    // has advanced blend..
+    if (EnsureGLExtensionEnabled("GL_OES_draw_buffers_indexed"))
+    {
+        // Enable two attachments
+        enabled[1] = GL_COLOR_ATTACHMENT1;
+        glDrawBuffers(4, enabled);
+
+        // Enable blend only on attachment 0
+        glDisable(GL_BLEND);
+        glEnableiOES(GL_BLEND, 0);
+        glDisableiOES(GL_BLEND, 1);
+
+        // Set advanced blend on the attachment that has blend disabled.  Set a non-advanced blend
+        // on the one that has blend enabled.
+        glBlendEquationiOES(0, GL_FUNC_ADD);
+        glBlendFunciOES(0, GL_ONE, GL_ONE);
+        glBlendEquationiOES(1, GL_MULTIPLY_KHR);
+
+        glClearColor(0, 1, 0, 1);
+        glClear(GL_COLOR_BUFFER_BIT);
+        glDrawArrays(GL_TRIANGLES, 0, 3);
+        ASSERT_GL_NO_ERROR();
+        EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::yellow);
+
+        // If advanced blend is enabled on a NONE attachment, that's also ok.
+        enabled[1] = GL_NONE;
+        glDrawBuffers(4, enabled);
+        glEnableiOES(GL_BLEND, 1);
+
+        glClearColor(0, 0, 1, 1);
+        glClear(GL_COLOR_BUFFER_BIT);
+        glDrawArrays(GL_TRIANGLES, 0, 3);
+        ASSERT_GL_NO_ERROR();
+        EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::magenta);
+    }
+}
+
 void AdvancedBlendTest::callBlendBarrier(APIExtensionVersion usedExtension)
 {
     ASSERT(usedExtension == APIExtensionVersion::Core || usedExtension == APIExtensionVersion::KHR);
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential GPU memory corruption via incomplete advanced blend validation in ANGLE

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 validation gap in ANGLE allows draw calls with mixed advanced and standard blend operations, violating Vulkan Valid Usage requirements. This malformed state can trigger undefined behavior or memory corruption in Vulkan drivers within the GPU process. On Android, where the GPU process is typically unsandboxed, this could facilitate a sandbox escape.

Affected files:

  • third_party/angle/src/libANGLE/validationES.cpp
  • third_party/angle/src/libANGLE/renderer/vulkan/vk_cache_utils.cpp
  • third_party/angle/src/libANGLE/renderer/vulkan/vk_renderer.cpp
  • third_party/angle/src/libANGLE/renderer/vulkan/vk_caps_utils.cpp

Estimated timestamp from git blame: 2022-02-23

Summary

A potential vulnerability exists in ANGLE’s OpenGL ES validation layer where it fails to enforce strict Multi-Render Target (MRT) restrictions when using advanced blend equations (from GL_KHR_blend_equation_advanced). This allows a compromised renderer to issue draw calls that violate the underlying Vulkan API’s Valid Usage IDs (VUIDs), potentially leading to memory corruption in the GPU process.

Root Cause Analysis

The issue stems from incomplete validation in ValidateDrawStates within third_party/angle/src/libANGLE/validationES.cpp. The OpenGL ES 3.2 specification (§17.3.6.2) and the GL_KHR_blend_equation_advanced extension require that if any enabled draw buffer uses an advanced blend equation, all other enabled draw buffers must have blending disabled.

However, ANGLE’s current implementation only verifies that no more than one draw buffer uses an advanced blend equation:

// third_party/angle/src/libANGLE/validationES.cpp:4168
for (size_t drawBufferIndex : blendStateExt.getUsesAdvancedBlendEquationMask())
{
    if (drawBufferIndex < drawBufferCount && ...)
    {
        ++advancedBlendRenderTargetCount;
    }
}
if (advancedBlendRenderTargetCount > 1)
    return kAdvancedBlendEquationWithMRT;

This logic fails to check if other enabled draw buffers (which might use standard equations like GL_FUNC_ADD) have blending enabled. Consequently, the Vulkan backend generates a VkPipelineColorBlendStateCreateInfo containing mixed blendEnable and colorBlendOp states.

On hardware where advancedBlendIndependentBlend is VK_FALSE (common on many mobile and NVIDIA GPUs), using an advanced blend op with other attachments having blendEnable = VK_TRUE is a violation of Vulkan Valid Usage. Additionally, advanced blend operations strictly require specific source and destination factors (VK_BLEND_FACTOR_SRC_ALPHA and VK_BLEND_FACTOR_DST_ALPHA), but ANGLE does not prevent an attacker from setting invalid factors via glBlendFunc for these attachments.

Potential Impact

Passing these invalid states to vkCreateGraphicsPipelines can cause the driver’s pipeline compiler to enter an undefined state, potentially resulting in heap overflows or other memory corruption within the GPU process. On platforms like Android where the GPU process often runs without a full sandbox, this could lead to an elevation of privilege or sandbox escape.

Suggested Reproducer Steps

  1. In a compromised renderer, initialize an ANGLE GLES context with GL_KHR_blend_equation_advanced and GL_OES_draw_buffers_indexed support.
  2. Create an FBO with two color attachments and set glDrawBuffers(2, ...).
  3. Enable blending for both: glEnablei(GL_BLEND, 0) and glEnablei(GL_BLEND, 1).
  4. Set attachment 0 to use an advanced equation: glBlendEquationi(0, GL_MULTIPLY).
  5. Set attachment 1 to use a standard equation: glBlendEquationi(1, GL_FUNC_ADD).
  6. (Optionally) Set invalid factors for the advanced attachment: glBlendFunci(0, GL_ONE, GL_ONE).
  7. Execute a draw call (glDrawArrays).

Proposed Fix

Update ValidateDrawStates in validationES.cpp to verify that if blendStateExt.getUsesAdvancedBlendEquationMask().any() is true, then for all drawBufferIndex < drawBufferCount where the draw buffer is not GL_NONE, blendStateExt.getEnabledMask().test(drawBufferIndex) must be false, EXCEPT for the single index using the advanced blend equation.

Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e


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