CVE-2026-14401
Overview
Files Changed
src/libANGLE/State.cppsrc/libANGLE/State.hsrc/libANGLE/angletypes.cppsrc/libANGLE/angletypes.hsrc/libANGLE/capture/FrameCapture.cpp
Patch
From 17fa38a7c983ac388399921c38955dbed9e55c5c Mon Sep 17 00:00:00 2001
From: Ken Russell <kbr@chromium.org>
Date: Fri, 15 May 2026 15:53:11 -0700
Subject: [PATCH] Remove BlendState struct in favor of BlendStateExt.
Eliminate duplicate state tracking to fix state desynchronization
bugs. Refactor D3D9 backend to look only at index 0 of BlendStateExt.
Remove BlendState usage from FrameCapture, inlining some GL defaults
where they previously came from a temporary BlendState instance.
Add test case from bug report. Verified on Pixel 9 that the new test
fails prior to the code changes and passes with them.
Co-authored with jetski-cli.
Fixed: chromium:513048822
Change-Id: Ibe9ed7e574c5519c19d215f22d082cc7e5d1c251
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7851594
Reviewed-by: Shahbaz Youssefi <syoussefi@chromium.org>
Commit-Queue: Kenneth Russell <kbr@chromium.org>
Reviewed-by: Geoff Lang <geofflang@chromium.org>
---
diff --git a/src/libANGLE/State.cpp b/src/libANGLE/State.cpp
index ccdbbfb..5478c12 100644
--- a/src/libANGLE/State.cpp
+++ b/src/libANGLE/State.cpp
@@ -543,11 +543,6 @@
return;
}
- mBlendState.colorMaskRed = red;
- mBlendState.colorMaskGreen = green;
- mBlendState.colorMaskBlue = blue;
- mBlendState.colorMaskAlpha = alpha;
-
mBlendStateExt.setColorMask(red, green, blue, alpha);
mDirtyBits.set(state::DIRTY_BIT_COLOR_MASK);
}
@@ -701,10 +696,8 @@
return;
}
- if (mSetBlendIndexedInvoked || mBlendState.blend != enabled)
+ if (mSetBlendIndexedInvoked || mBlendStateExt.getEnabledMask().test(0) != enabled)
{
- mBlendState.blend = enabled;
-
mSetBlendIndexedInvoked = false;
mBlendStateExt.setEnabled(enabled);
mDirtyBits.set(state::DIRTY_BIT_BLEND_ENABLED);
@@ -743,18 +736,15 @@
GLenum sourceAlpha,
GLenum destAlpha)
{
- if (!mSetBlendFactorsIndexedInvoked && mBlendState.sourceBlendRGB == sourceRGB &&
- mBlendState.destBlendRGB == destRGB && mBlendState.sourceBlendAlpha == sourceAlpha &&
- mBlendState.destBlendAlpha == destAlpha)
+ if (!mSetBlendFactorsIndexedInvoked &&
+ mBlendStateExt.getSrcColorIndexed(0) == FromGLenum<BlendFactorType>(sourceRGB) &&
+ mBlendStateExt.getDstColorIndexed(0) == FromGLenum<BlendFactorType>(destRGB) &&
+ mBlendStateExt.getSrcAlphaIndexed(0) == FromGLenum<BlendFactorType>(sourceAlpha) &&
+ mBlendStateExt.getDstAlphaIndexed(0) == FromGLenum<BlendFactorType>(destAlpha))
{
return;
}
- mBlendState.sourceBlendRGB = sourceRGB;
- mBlendState.destBlendRGB = destRGB;
- mBlendState.sourceBlendAlpha = sourceAlpha;
- mBlendState.destBlendAlpha = destAlpha;
-
if (mNoSimultaneousConstantColorAndAlphaBlendFunc)
{
if (hasConstantColor(sourceRGB, destRGB))
@@ -827,12 +817,10 @@
void PrivateState::setBlendEquation(GLenum rgbEquation, GLenum alphaEquation)
{
- if (mSetBlendEquationsIndexedInvoked || mBlendState.blendEquationRGB != rgbEquation ||
- mBlendState.blendEquationAlpha != alphaEquation)
+ if (mSetBlendEquationsIndexedInvoked ||
+ mBlendStateExt.getEquationColorIndexed(0) != FromGLenum<BlendEquationType>(rgbEquation) ||
+ mBlendStateExt.getEquationAlphaIndexed(0) != FromGLenum<BlendEquationType>(alphaEquation))
{
- mBlendState.blendEquationRGB = rgbEquation;
- mBlendState.blendEquationAlpha = alphaEquation;
-
mSetBlendEquationsIndexedInvoked = false;
mBlendStateExt.setEquations(rgbEquation, alphaEquation);
mDirtyBits.set(state::DIRTY_BIT_BLEND_EQUATIONS);
diff --git a/src/libANGLE/State.h b/src/libANGLE/State.h
index 67bc68d..2676141 100644
--- a/src/libANGLE/State.h
+++ b/src/libANGLE/State.h
@@ -254,7 +254,6 @@
// State chunk getters
const RasterizerState &getRasterizerState() const { return mRasterizer; }
- const BlendState &getBlendState() const { return mBlendState; }
const BlendStateExt &getBlendStateExt() const { return mBlendStateExt; }
const DepthStencilState &getDepthStencilState() const { return mDepthStencil; }
@@ -699,7 +698,6 @@
bool mNoUnclampedBlendColor;
- BlendState mBlendState; // Buffer zero blend state legacy struct
BlendStateExt mBlendStateExt;
ColorF mBlendColor;
bool mSampleAlphaToCoverage;
@@ -1358,7 +1356,6 @@
// Convenience functions that forward to context-private state.
const RasterizerState &getRasterizerState() const { return mPrivateState.getRasterizerState(); }
- const BlendState &getBlendState() const { return mPrivateState.getBlendState(); }
const BlendStateExt &getBlendStateExt() const { return mPrivateState.getBlendStateExt(); }
const DepthStencilState &getDepthStencilState() const
{
diff --git a/src/libANGLE/angletypes.cpp b/src/libANGLE/angletypes.cpp
index cedecca..d13da7b 100644
--- a/src/libANGLE/angletypes.cpp
+++ b/src/libANGLE/angletypes.cpp
@@ -106,38 +106,6 @@
return !(a == b);
}
-BlendState::BlendState()
-{
- memset(this, 0, sizeof(BlendState));
-
- blend = false;
- sourceBlendRGB = GL_ONE;
- sourceBlendAlpha = GL_ONE;
- destBlendRGB = GL_ZERO;
- destBlendAlpha = GL_ZERO;
- blendEquationRGB = GL_FUNC_ADD;
- blendEquationAlpha = GL_FUNC_ADD;
- colorMaskRed = true;
- colorMaskGreen = true;
- colorMaskBlue = true;
- colorMaskAlpha = true;
-}
-
-BlendState::BlendState(const BlendState &other)
-{
- memcpy(this, &other, sizeof(BlendState));
-}
-
-bool operator==(const BlendState &a, const BlendState &b)
-{
- return memcmp(&a, &b, sizeof(BlendState)) == 0;
-}
-
-bool operator!=(const BlendState &a, const BlendState &b)
-{
- return !(a == b);
-}
-
DepthStencilState::DepthStencilState()
{
memset(this, 0, sizeof(DepthStencilState));
diff --git a/src/libANGLE/angletypes.h b/src/libANGLE/angletypes.h
index 5439354..f9aa01c 100644
--- a/src/libANGLE/angletypes.h
+++ b/src/libANGLE/angletypes.h
@@ -317,29 +317,6 @@
bool operator==(const RasterizerState &a, const RasterizerState &b);
bool operator!=(const RasterizerState &a, const RasterizerState &b);
-struct BlendState final
-{
- // This will zero-initialize the struct, including padding.
- BlendState();
- BlendState(const BlendState &other);
-
- bool blend;
- GLenum sourceBlendRGB;
- GLenum destBlendRGB;
- GLenum sourceBlendAlpha;
- GLenum destBlendAlpha;
- GLenum blendEquationRGB;
- GLenum blendEquationAlpha;
-
- bool colorMaskRed;
- bool colorMaskGreen;
- bool colorMaskBlue;
- bool colorMaskAlpha;
-};
-
-bool operator==(const BlendState &a, const BlendState &b);
-bool operator!=(const BlendState &a, const BlendState &b);
-
struct DepthStencilState final
{
// This will zero-initialize the struct, including padding.
diff --git a/src/libANGLE/capture/FrameCapture.cpp b/src/libANGLE/capture/FrameCapture.cpp
index d96a7e3..123a062 100644
--- a/src/libANGLE/capture/FrameCapture.cpp
+++ b/src/libANGLE/capture/FrameCapture.cpp
Regression Test / PoC
diff --git a/src/tests/gl_tests/ImageTest.cpp b/src/tests/gl_tests/ImageTest.cpp
index ae7d27c..c969107 100644
--- a/src/tests/gl_tests/ImageTest.cpp
+++ b/src/tests/gl_tests/ImageTest.cpp
@@ -5085,6 +5085,72 @@
destroyAndroidHardwareBuffer(source);
}
+// Test that indexed setters from GL_OES_draw_buffers_indexed work with YUV rendering.
+// Regression test for a bug in ValidateDrawStates that only checked the non-indexed blend state.
+TEST_P(ImageTestES3, RenderToYUVAHBIndexedBlendValidationBypass)
+{
+ EGLWindow *window = getEGLWindow();
+
+ ANGLE_SKIP_TEST_IF(!hasOESExt() || !hasBaseExt() || !has2DTextureExt() || !hasYUVTargetExt());
+ ANGLE_SKIP_TEST_IF(!hasAndroidImageNativeBufferExt() || !hasAndroidHardwareBufferSupport());
+ ANGLE_SKIP_TEST_IF(!hasAhbLockPlanesSupport());
+ ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_OES_draw_buffers_indexed"));
+
+ // 3 planes of data, initialize to all zeroes
+ GLubyte dataY[4] = {0, 0, 0, 0};
+ GLubyte dataCb[1] = {0};
+ GLubyte dataCr[1] = {0};
+
+ AHardwareBuffer *source;
+ EGLImageKHR image;
+ createEGLImageAndroidHardwareBufferSource(
+ 2, 2, 1, AHARDWAREBUFFER_FORMAT_Y8Cb8Cr8_420, kDefaultAHBUsage, kDefaultAttribs,
+ {{dataY, 1}, {dataCb, 1}, {dataCr, 1}}, &source, &image);
+
+ GLTexture target;
+ createEGLImageTargetTextureExternal(image, target);
+
+ GLFramebuffer fbo;
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_EXTERNAL_OES, target,
+ 0);
+ ASSERT_GL_NO_ERROR();
+ EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+ glUseProgram(mRenderYUVProgram);
+ glUniform4f(mRenderYUVUniformLocation, 0.5f, 0.5f, 0.5f, 1.0f);
+
+ // Test non-indexed blend enable
+ glEnable(GL_BLEND);
+ drawQuad(mRenderYUVProgram, "position", 0.0f);
+ EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+ glDisable(GL_BLEND);
+
+ // Test non-indexed partial color mask
+ glColorMask(GL_FALSE, GL_TRUE, GL_FALSE, GL_TRUE);
+ drawQuad(mRenderYUVProgram, "position", 0.0f);
+ EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+ glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
+
+ // Test indexed blend enable
+ glEnableiOES(GL_BLEND, 0);
+ EXPECT_GL_NO_ERROR();
+ drawQuad(mRenderYUVProgram, "position", 0.0f);
+ EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+ glDisableiOES(GL_BLEND, 0);
+
+ // Test indexed partial color mask
+ glColorMaskiOES(0, GL_FALSE, GL_TRUE, GL_FALSE, GL_TRUE);
+ EXPECT_GL_NO_ERROR();
+ drawQuad(mRenderYUVProgram, "position", 0.0f);
+ EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+ glColorMaskiOES(0, GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
+
+ glFinish();
+ eglDestroyImageKHR(window->getDisplay(), image);
+ destroyAndroidHardwareBuffer(source);
+}
+
// Test rendering to a YUV AHB using EXT_yuv_target with a normal depth attachment
TEST_P(ImageTestES3, RenderToYUVAHBWithDepth)
{
Original Bug Report
ANGLE: Potential EXT_yuv_target draw validation bypass via indexed state setters
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 logic error in ANGLE’s frontend validation allows rendering to YUV framebuffers with blending or partial color masks enabled, violating the EXT_yuv_target specification. This occurs because the validation layer checks a legacy state object that is not synchronized with modern indexed state setters. Submitting this invalid state to a graphics driver can result in undefined behavior or potential sandbox escape on Android.
Affected files:
third_party/angle/src/libANGLE/validationES.cppthird_party/angle/src/libANGLE/State.cppthird_party/angle/src/libANGLE/renderer/vulkan/vk_format_utils.cpp
Estimated timestamp from git blame: 2020-11-03
Summary
ANGLE’s validation for the EXT_yuv_target extension contains a potential logic error that allows applications to bypass mandatory restrictions when rendering to YUV framebuffers. Specifically, the extension requires that blending be disabled and that all color mask components (Red, Green, Blue, Alpha) be set to true. ANGLE attempts to enforce this during draw calls, but it incorrectly relies on a legacy, non-indexed state object (mBlendState) that is not updated by indexed state-setting functions (e.g., glEnableiOES, glColorMaskiOES).
Root Cause Analysis
In third_party/angle/src/libANGLE/validationES.cpp, the function ValidateDrawStates performs the following check for YUV framebuffers:
// validationES.cpp:4134
bool framebufferIsYUV = framebuffer->hasYUVAttachment();
if (ANGLE_UNLIKELY(framebufferIsYUV))
{
const BlendState &blendState = state.getBlendState(); // Retrieves legacy mBlendState
if (!blendState.colorMaskRed || ...)
return kInvalidColorMaskForYUV;
if (blendState.blend)
return kInvalidBlendStateForYUV;
}
However, the PrivateState class in libANGLE/State.cpp maintains two sets of blend state: mBlendState (legacy, for index 0) and mBlendStateExt (modern, indexed). While non-indexed calls like glEnable(GL_BLEND) update both, the indexed versions used in modern OpenGL ES only update the extended state:
// State.cpp:714
void PrivateState::setBlendIndexed(bool enabled, GLuint index)
{
// ... PLS checks ...
mSetBlendIndexedInvoked = true;
mBlendStateExt.setEnabledIndexed(index, enabled); // Legacy mBlendState is NOT updated
mDirtyBits.set(state::DIRTY_BIT_BLEND_ENABLED);
}
A similar desynchronization exists for setColorMaskIndexed. Consequently, if an application uses glEnableiOES(GL_BLEND, 0) on a YUV framebuffer, the validation check re-reads the stale mBlendState.blend (which defaults to false) and incorrectly passes.
Security Impact
By bypassing this validation, a compromised renderer can pass forbidden state to the underlying graphics driver. In the Vulkan backend, ANGLE uses the correct indexed state (mBlendStateExt) to build the pipeline, leading it to create a VkPipeline with blendEnable = VK_TRUE for a YUV/external attachment. This is a violation of Vulkan Valid Usage requirements (e.g., VUID-VkPipelineColorBlendAttachmentState-blendEnable-00601).
On Android, the GPU process is typically unsandboxed. Submitting invalid state that violates Vulkan invariants can lead to driver-dependent memory corruption or undefined behavior, providing a potential vector for sandbox escape.
Potential Steps to Reproduce
- Initialize an ANGLE context on a device supporting
GL_EXT_yuv_targetandGL_OES_draw_buffers_indexed(common on Android Vulkan). - Bind a YUV external texture to a framebuffer’s color attachment.
- Call
glEnableiOES(GL_BLEND, 0)to enable blending via the indexed setter. - Issue a draw call (e.g.,
glDrawArrays). - Observe that the draw call proceeds without the expected
GL_INVALID_OPERATIONerror.
Suggested Fix
The validation logic in ValidateDrawStates should be updated to check the modern mBlendStateExt object instead of the legacy mBlendState. Alternatively, setBlendIndexed and setColorMaskIndexed should ensure that mBlendState is kept in sync when the index is 0.
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.