CVE-2026-9914
Overview
Background
- ANGLE native YUV textures
- The
GL_ANGLE_yuv_internal_formatextension exposes multi-planar YUV internal formats such asGL_G8_B8R8_2PLANE_420_UNORM_ANGLEandGL_G8_B8_R8_3PLANE_420_UNORM_ANGLE, allocated viaglTexStorage2DEXT. - Format capability table
src/libANGLE/formatutils.cppbuilds a map describing each internal format’s capabilities viaAddYUVFormat(...); the per-format columns include Texture supported, Filterable, Texture attachment (color-attachable / renderable), Renderbuffer, and Blend.- `RequireExt<&Extensions::yuvInternalFormatANGLE>` vs `NeverSupported`
- Capability predicates: the former advertises the capability whenever the YUV extension is enabled;
NeverSupporteddisables it unconditionally. - Framebuffer completeness
- A texture may be bound as a color attachment only if its format is color-attachable; otherwise
glCheckFramebufferStatusreturnsGL_FRAMEBUFFER_INCOMPLETE_ATTACHMENTand draws/readbacks fail withGL_INVALID_FRAMEBUFFER_OPERATION.
Root Cause Analysis
847b036be553), which an audit found was a preparatory/helper change, not the fix. This page now points to the commit whose diff removes the bug. No root-cause analysis is published for this CVE.The advisory describes a three-flaw chain (a missing gl::Texture::isYUV() override, a passthrough-decoder internalformat validation gap, and a Vulkan-backend getExternalFormat() != 0 misidentification). The mapped diff does none of those. The only non-test change is in src/libANGLE/formatutils.cpp: for both YUV internal formats, AddYUVFormat previously set the Texture attachment column to RequireExt<&Extensions::yuvInternalFormatANGLE> — i.e. a YUV texture was advertised as color-attachable whenever the extension was enabled. The patch changes that column to NeverSupported for both the 2-plane and 3-plane formats (the Texture supported and Filterable columns are left as RequireExt). This is the real root cause fixed here: the capability table incorrectly declared multi-planar YUV formats renderable, so a YUV texture could pass framebuffer-completeness and be driven through color-attachment operations (glCopyTexImage2D, glReadPixels, draws) that then hand VK_IMAGE_ASPECT_COLOR_BIT operations on a multi-planar image to the driver — undefined behavior and potential OOB per the advisory.
By marking YUV formats as never color-attachable, the fix makes any FBO with a YUV color attachment report GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT, so the dangerous copy/draw/readback paths are rejected at the GL layer before reaching the backend. The added Texture2DTestES3YUV tests confirm this: both the “allocated up front” and “allocated late” cases expect GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT, and expect draws and glReadPixels to raise GL_INVALID_FRAMEBUFFER_OPERATION. Discrepancy to flag: the advisory’s isYUV() override, passthrough internalformat hardening, and Vulkan-backend YUV-property check are not in this diff; this commit closes the renderability path specifically. The advisory’s “insufficient validation of untrusted input” label is directionally right (a format was permitted into operations it should not reach), but the substantiated mechanism is a capability-table logic error, not the isYUV() chain described.
NeverSupported for attachment removes the entry point.Attack Path
- Compromise/raw IPC
From a renderer, enable
GL_ANGLE_yuv_internal_formatand allocate a native YUV texture viaglTexStorage2DEXTwith e.g.GL_G8_B8R8_2PLANE_420_UNORM_ANGLE(manual command-buffer IPC if needed; inferred). - Attach to FBO
Bind the YUV texture to color attachment 0 of an FBO. Pre-patch, the capability table marked the format attachable, so the framebuffer was reported
GL_FRAMEBUFFER_COMPLETE. - Issue color-aspect op
Trigger an operation prohibited for multi-planar YUV, e.g.
glCopyTexImage2D, a draw, orglReadPixels, which the complete-framebuffer state now permits. - Reach backend
The operation descends to the Vulkan backend, which issues a color-aspect command (e.g.
vkCmdCopyImageToBufferwithVK_IMAGE_ASPECT_COLOR_BIT) against a multi-planar image — a Vulkan VUID violation. - Undefined behavior / OOB The driver executes the malformed multi-planar command, yielding undefined behavior and potential out-of-bounds GPU memory access (inferred; driver-side, not in diff).
Impact Assessment
S:C, consistent with crossing into the GPU/driver). The diff substantiates closing one reachability path: YUV textures can no longer be color attachments, so the copy/draw/readback route to malformed multi-planar driver commands is blocked. The downstream memory-safety consequence (VUID violation → undefined behavior → OOB access in the driver) is described by the advisory and is (inferred) rather than shown by this diff. Because this diff does not implement the advisory’s isYUV()/passthrough/Vulkan-check fixes, other paths to the same backend behavior (not through a renderable attachment) are not addressed here; a reviewer should not assume the full chain is closed by this commit alone.Files Changed
src/libANGLE/formatutils.cppsrc/tests/gl_tests/TextureTest.cpp
Audit Directions
- Advisory flaws not fixed hereIndependently verify the advisory’s other claims: whether
gl::Texture::isYUV()is overridden, whetherGLES2DecoderPassthroughImpl::DoTexStorage2DEXTvalidatesinternalformat, and whether the Vulkan backend keys YUV staging offgetActualFormat().isYUVrather thangetExternalFormat() != 0. - Capability-table correctnessAudit other
AddYUVFormat/AddRGBA*/special-format rows for capabilities (attachment, renderbuffer, blend, filterable) that are over-permissive for the hardware/format semantics. - Multi-planar aspect handlingReview Vulkan-backend copy/readback paths that use
VK_IMAGE_ASPECT_COLOR_BITto ensure they reject multi-planar images regardless of how the image reached them.
Patch
diff --git a/src/libANGLE/formatutils.cpp b/src/libANGLE/formatutils.cpp
index 1ec1e20..507c369 100644
--- a/src/libANGLE/formatutils.cpp
+++ b/src/libANGLE/formatutils.cpp
@@ -1465,8 +1465,8 @@
// Non-standard YUV formats
// | Internal format | sized | Cr | Y | Cb | A | S | Format | Type | Comp | SRGB | Texture supported | Filterable | Texture attachment | Renderbuffer | Blend
- AddYUVFormat(&map, GL_G8_B8R8_2PLANE_420_UNORM_ANGLE, true, 8, 8, 8, 0, 0, GL_G8_B8R8_2PLANE_420_UNORM_ANGLE, GL_UNSIGNED_BYTE, GL_UNSIGNED_NORMALIZED, false, RequireExt<&Extensions::yuvInternalFormatANGLE>, RequireExt<&Extensions::yuvInternalFormatANGLE>, RequireExt<&Extensions::yuvInternalFormatANGLE>, NeverSupported, NeverSupported);
- AddYUVFormat(&map, GL_G8_B8_R8_3PLANE_420_UNORM_ANGLE, true, 8, 8, 8, 0, 0, GL_G8_B8_R8_3PLANE_420_UNORM_ANGLE, GL_UNSIGNED_BYTE, GL_UNSIGNED_NORMALIZED, false, RequireExt<&Extensions::yuvInternalFormatANGLE>, RequireExt<&Extensions::yuvInternalFormatANGLE>, RequireExt<&Extensions::yuvInternalFormatANGLE>, NeverSupported, NeverSupported);
+ AddYUVFormat(&map, GL_G8_B8R8_2PLANE_420_UNORM_ANGLE, true, 8, 8, 8, 0, 0, GL_G8_B8R8_2PLANE_420_UNORM_ANGLE, GL_UNSIGNED_BYTE, GL_UNSIGNED_NORMALIZED, false, RequireExt<&Extensions::yuvInternalFormatANGLE>, RequireExt<&Extensions::yuvInternalFormatANGLE>, NeverSupported, NeverSupported, NeverSupported);
+ AddYUVFormat(&map, GL_G8_B8_R8_3PLANE_420_UNORM_ANGLE, true, 8, 8, 8, 0, 0, GL_G8_B8_R8_3PLANE_420_UNORM_ANGLE, GL_UNSIGNED_BYTE, GL_UNSIGNED_NORMALIZED, false, RequireExt<&Extensions::yuvInternalFormatANGLE>, RequireExt<&Extensions::yuvInternalFormatANGLE>, NeverSupported, NeverSupported, NeverSupported);
// clang-format on
diff --git a/src/tests/gl_tests/TextureTest.cpp b/src/tests/gl_tests/TextureTest.cpp
index 3d8efe5..5484dbb 100644
--- a/src/tests/gl_tests/TextureTest.cpp
+++ b/src/tests/gl_tests/TextureTest.cpp
@@ -4942,6 +4942,87 @@
ASSERT_GL_NO_ERROR();
}
+
+// Test that rendering to a YUV texture allocated with GL_ANGLE_yuv_internal_format fails.
+// Framebuffer completeness check should fail, and draws/readbacks should fail.
+TEST_P(Texture2DTestES3YUV, YuvInternalFormatNotColorRenderable)
+{
+ ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_ANGLE_yuv_internal_format"));
+
+ // Create YUV texture
+ GLTexture yuvTexture;
+ glBindTexture(GL_TEXTURE_2D, yuvTexture);
+ glTexStorage2D(GL_TEXTURE_2D, 1, GL_G8_B8R8_2PLANE_420_UNORM_ANGLE, 4, 4);
+ ASSERT_GL_NO_ERROR();
+
+ // Create a Framebuffer Object and bind the YUV texture to color attachment 0
+ GLFramebuffer fbo;
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, yuvTexture, 0);
+ ASSERT_GL_NO_ERROR();
+
+ // Verify that the framebuffer is incomplete
+ GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
+ EXPECT_EQ(status, static_cast<GLenum>(GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT));
+
+ // Verify that drawing to this framebuffer fails
+ ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), essl1_shaders::fs::Red());
+ glUseProgram(program);
+
+ // Drawing should generate GL_INVALID_FRAMEBUFFER_OPERATION
+ drawQuad(program, essl1_shaders::PositionAttrib(), 0.5f);
+ EXPECT_GL_ERROR(GL_INVALID_FRAMEBUFFER_OPERATION);
+
+ // Verify that glReadPixels from this framebuffer fails
+ GLubyte pixel[4];
+ glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel);
+ EXPECT_GL_ERROR(GL_INVALID_FRAMEBUFFER_OPERATION);
+}
+
+// Test that rendering to a YUV texture allocated with GL_ANGLE_yuv_internal_format late, after
+// attached to the framebuffer's color attachment, fails. Framebuffer completeness check should
+// fail, and draws/readbacks should fail.
+TEST_P(Texture2DTestES3YUV, YuvInternalFormatAllocatedLateNotColorRenderable)
+{
+ ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_ANGLE_yuv_internal_format"));
+
+ // Create but do not allocate YUV texture
+ GLTexture yuvTexture;
+ glBindTexture(GL_TEXTURE_2D, yuvTexture);
+ ASSERT_GL_NO_ERROR();
+
+ // Create a Framebuffer Object and bind the YUV texture to color attachment 0
+ GLFramebuffer fbo;
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, yuvTexture, 0);
+ ASSERT_GL_NO_ERROR();
+
+ // Verify that the framebuffer is incomplete
+ GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
+ EXPECT_EQ(status, static_cast<GLenum>(GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT));
+
+ // Allocate storage for the YUV texture
+ glTexStorage2D(GL_TEXTURE_2D, 1, GL_G8_B8R8_2PLANE_420_UNORM_ANGLE, 4, 4);
+ EXPECT_GL_NO_ERROR();
+
+ // Verify that the framebuffer is still incomplete
+ status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
+ EXPECT_EQ(status, static_cast<GLenum>(GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT));
+
+ // Verify that drawing to this framebuffer fails
+ ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), essl1_shaders::fs::Red());
+ glUseProgram(program);
+
+ // Drawing should generate GL_INVALID_FRAMEBUFFER_OPERATION
+ drawQuad(program, essl1_shaders::PositionAttrib(), 0.5f);
+ EXPECT_GL_ERROR(GL_INVALID_FRAMEBUFFER_OPERATION);
+
+ // Verify that glReadPixels from this framebuffer fails
+ GLubyte pixel[4];
+ glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel);
+ EXPECT_GL_ERROR(GL_INVALID_FRAMEBUFFER_OPERATION);
+}
+
// Tests CopySubImage for float formats
TEST_P(Texture2DTest, CopySubImageFloat_R_R)
{
Regression Test / PoC
diff --git a/src/tests/gl_tests/TextureTest.cpp b/src/tests/gl_tests/TextureTest.cpp
index 3d8efe5..5484dbb 100644
--- a/src/tests/gl_tests/TextureTest.cpp
+++ b/src/tests/gl_tests/TextureTest.cpp
@@ -4942,6 +4942,87 @@
ASSERT_GL_NO_ERROR();
}
+
+// Test that rendering to a YUV texture allocated with GL_ANGLE_yuv_internal_format fails.
+// Framebuffer completeness check should fail, and draws/readbacks should fail.
+TEST_P(Texture2DTestES3YUV, YuvInternalFormatNotColorRenderable)
+{
+ ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_ANGLE_yuv_internal_format"));
+
+ // Create YUV texture
+ GLTexture yuvTexture;
+ glBindTexture(GL_TEXTURE_2D, yuvTexture);
+ glTexStorage2D(GL_TEXTURE_2D, 1, GL_G8_B8R8_2PLANE_420_UNORM_ANGLE, 4, 4);
+ ASSERT_GL_NO_ERROR();
+
+ // Create a Framebuffer Object and bind the YUV texture to color attachment 0
+ GLFramebuffer fbo;
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, yuvTexture, 0);
+ ASSERT_GL_NO_ERROR();
+
+ // Verify that the framebuffer is incomplete
+ GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
+ EXPECT_EQ(status, static_cast<GLenum>(GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT));
+
+ // Verify that drawing to this framebuffer fails
+ ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), essl1_shaders::fs::Red());
+ glUseProgram(program);
+
+ // Drawing should generate GL_INVALID_FRAMEBUFFER_OPERATION
+ drawQuad(program, essl1_shaders::PositionAttrib(), 0.5f);
+ EXPECT_GL_ERROR(GL_INVALID_FRAMEBUFFER_OPERATION);
+
+ // Verify that glReadPixels from this framebuffer fails
+ GLubyte pixel[4];
+ glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel);
+ EXPECT_GL_ERROR(GL_INVALID_FRAMEBUFFER_OPERATION);
+}
+
+// Test that rendering to a YUV texture allocated with GL_ANGLE_yuv_internal_format late, after
+// attached to the framebuffer's color attachment, fails. Framebuffer completeness check should
+// fail, and draws/readbacks should fail.
+TEST_P(Texture2DTestES3YUV, YuvInternalFormatAllocatedLateNotColorRenderable)
+{
+ ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_ANGLE_yuv_internal_format"));
+
+ // Create but do not allocate YUV texture
+ GLTexture yuvTexture;
+ glBindTexture(GL_TEXTURE_2D, yuvTexture);
+ ASSERT_GL_NO_ERROR();
+
+ // Create a Framebuffer Object and bind the YUV texture to color attachment 0
+ GLFramebuffer fbo;
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, yuvTexture, 0);
+ ASSERT_GL_NO_ERROR();
+
+ // Verify that the framebuffer is incomplete
+ GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
+ EXPECT_EQ(status, static_cast<GLenum>(GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT));
+
+ // Allocate storage for the YUV texture
+ glTexStorage2D(GL_TEXTURE_2D, 1, GL_G8_B8R8_2PLANE_420_UNORM_ANGLE, 4, 4);
+ EXPECT_GL_NO_ERROR();
+
+ // Verify that the framebuffer is still incomplete
+ status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
+ EXPECT_EQ(status, static_cast<GLenum>(GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT));
+
+ // Verify that drawing to this framebuffer fails
+ ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), essl1_shaders::fs::Red());
+ glUseProgram(program);
+
+ // Drawing should generate GL_INVALID_FRAMEBUFFER_OPERATION
+ drawQuad(program, essl1_shaders::PositionAttrib(), 0.5f);
+ EXPECT_GL_ERROR(GL_INVALID_FRAMEBUFFER_OPERATION);
+
+ // Verify that glReadPixels from this framebuffer fails
+ GLubyte pixel[4];
+ glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel);
+ EXPECT_GL_ERROR(GL_INVALID_FRAMEBUFFER_OPERATION);
+}
+
// Tests CopySubImage for float formats
TEST_P(Texture2DTest, CopySubImageFloat_R_R)
{
Original Bug Report
Validation Bypass and VUID Violation via Native YUV Textures 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 security team.
Overview: A potential vulnerability exists where native YUV textures bypass validation due to a missing isYUV() override in gl::Texture. A compromised renderer can pass an arbitrary YUV format to the passthrough decoder, causing ANGLE to send malformed multi-planar commands to the Vulkan backend. This results in undefined driver behavior and potential GPU process memory corruption.
Affected files:
third_party/angle/src/libANGLE/Texture.hthird_party/angle/src/libANGLE/renderer/vulkan/vk_helpers.hthird_party/angle/src/libANGLE/Texture.cppthird_party/angle/src/libANGLE/renderer/vulkan/vk_helpers.cppthird_party/angle/src/libANGLE/Image.cppthird_party/angle/src/libANGLE/validationES.cppthird_party/angle/src/libANGLE/Framebuffer.cpp
Estimated timestamp from git blame: 2023-10-17
Description
A potential vulnerability exists in how ANGLE handles native YUV textures (e.g., GL_G8_B8R8_2PLANE_420_UNORM_ANGLE), allowing a compromised renderer to bypass validation and trigger undefined behavior in the GPU process via Vulkan API violations. This relies on three chained flaws in the GPU process:
- Passthrough Decoder Validation Bypass: The
GLES2DecoderPassthroughImplblindly forwards theinternalformatargument of commands likeTexStorage2DEXTdirectly to ANGLE without validating it against Chrome’svalidators_.texture_internal_format_storage. A compromised renderer can manually construct an IPC to submit arbitrary internal formats. - Missing
gl::Texture::isYUV()Override:gl::Textureinherits fromegl::ImageSiblingbut does not override the virtualisYUV()method. The defaultegl::ImageSibling::isYUV()implementation returnsfalseunless the texture is an EGLImage target (mTargetOf). Native YUV textures created viaglTexStorage2DEXThave a nullmTargetOf, causingisYUV()to incorrectly returnfalse. This causes ANGLE validation functions (e.g.,ValidateCopyTexImageParametersBase) to treat the texture as a standard color format and bypassGL_INVALID_OPERATIONerrors for YUV-restricted operations. - Vulkan Backend Format Misidentification: In the Vulkan backend (e.g.,
ImageHelper::readPixelsImpl), ANGLE checks if a texture requires YUV-to-RGBA staging by evaluatinggetExternalFormat() != 0. Because native Vulkan YUV formats have an external format of0, this check fails, and the safe staging path is skipped.
When these flaws are combined, ANGLE attempts direct operations (e.g., vkCmdCopyImageToBuffer) on the multi-planar YUV image using VK_IMAGE_ASPECT_COLOR_BIT. This violates Vulkan specification requirements (VUID-vkCmdCopyImageToBuffer-srcImage-07971), resulting in undefined behavior and out-of-bounds memory accesses in the underlying graphics driver.
Potential Reproduction Steps
Note: Our tooling agent does not have the ability to run code. These are the suggested steps an attacker would follow.
- Obtain arbitrary code execution within a sandboxed renderer process.
- Write a raw
glRequestExtensionCHROMIUMIPC to the command buffer to enable theGL_ANGLE_yuv_internal_formatextension for the context. - Construct a raw
TexStorage2DEXTIPC, manually setting theinternalformatargument toGL_G8_B8R8_2PLANE_420_UNORM_ANGLE(enum value0x96B1). - Create a Framebuffer Object (FBO) and bind the native YUV texture to color attachment 0. ANGLE will report the framebuffer as
GL_FRAMEBUFFER_COMPLETE. - Trigger a command strictly prohibited for YUV framebuffers, such as
glCopyTexImage2DorglReadPixels. - Because validation is bypassed, the invalid operation reaches the Vulkan backend, passing a malformed command to the driver and triggering a potential sandbox escape.
Suggested Fix
- Override
isYUV()ingl::Texture: Implementgl::Texture::isYUV()to accurately report its state based on its internal format (e.g., checking if the base level description format has the YUV property). - Update Vulkan Backend Checks: Update the backend (e.g.,
ImageHelper::readPixelsImpl) to rely on the actual format’s YUV property (getActualFormat().isYUV) instead of strictly checking if the external format is non-zero. - Harden Passthrough Decoder: Implement validation in
GLES2DecoderPassthroughImpl::DoTexStorage2DEXTand related commands to rejectinternalformatvalues that are not explicitly permitted by Chrome’s feature info.
Evaluated with Chrome root at commit: f200f57a19490707ff8bc7aa5de3cbc443a3afad
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.