Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactHeap buffer overflow in ANGLE
DescriptionHeap buffer overflow in ANGLE
ComponentANGLE
Bug ClassOOB
Tracker500063836
Fix commit210ffede00f2 (angle/angle) +155/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-27

Files Changed

  • src/libANGLE/ErrorStrings.h
  • src/libANGLE/renderer/vulkan/vk_helpers.cpp
  • src/libANGLE/validationES3.cpp
  • src/tests/angle_end2end_tests_expectations.txt
  • src/tests/gl_tests/TextureTest.cpp
From 210ffede00f208573d16dbc3d4b4dfb2670c438d Mon Sep 17 00:00:00 2001
From: Ken Russell <[email protected]>
Date: Fri, 24 Apr 2026 20:28:10 -0700
Subject: [PATCH] Scale UV plane offsets for YUV textures.

The U and V channels are always half-resolution. Scale the offsets
appropriately when copying data into those planes of the texture.

Add validation rejecting glTexSubImage2D uploads to
ANGLE_yuv_internal_format textures when any of the xoffset, yoffset,
width or height are odd. The results are ill-defined.

Add new unit tests performing sub-image uploads at non-zero offsets to
these texture types. 3-plane textures are not rendering correctly (the
result of sampling from them was never tested before) so the test is
skipped in that configuration.

Fixed: chromium:500063836
Bug: chromium:506193593
Change-Id: Ie89e6f501ffe7b30f6a87bbb64e7ac4a5d945422
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7793855
Auto-Submit: Kenneth Russell <[email protected]>
Commit-Queue: Kenneth Russell <[email protected]>
Reviewed-by: Shahbaz Youssefi <[email protected]>
Reviewed-by: Geoff Lang <[email protected]>
---

diff --git a/src/libANGLE/ErrorStrings.h b/src/libANGLE/ErrorStrings.h
index 1ed3394..589a3da 100644
--- a/src/libANGLE/ErrorStrings.h
+++ b/src/libANGLE/ErrorStrings.h
@@ -676,6 +676,7 @@
 inline constexpr const char *kWebGLNameLengthLimitExceeded = "Name is longer than %d characters.";
 inline constexpr const char *kYUVOutputMissmatch = "Program and framebuffer YUV output state does not match.";
 inline constexpr const char *kYUVTargetExtensionRequired = "GL_EXT_YUV_target not enabled.";
+inline constexpr const char *kYuvTexSubImage2DOddOffsetOrDimension = "TexSubImage2D with YUV formats requires even offsets and dimensions.";
 inline constexpr const char *kZeroBoundToTarget = "Zero is bound to target.";
 inline constexpr const char *kUnrecognizedShaderStageBit = "Unrecognized shader stage bit.";
 inline constexpr const char *kProgramNotSeparable = "Program object was not linked with its PROGRAM_SEPARABLE status set.";
diff --git a/src/libANGLE/renderer/vulkan/vk_helpers.cpp b/src/libANGLE/renderer/vulkan/vk_helpers.cpp
index 56cfbda..42588e7 100644
--- a/src/libANGLE/renderer/vulkan/vk_helpers.cpp
+++ b/src/libANGLE/renderer/vulkan/vk_helpers.cpp
@@ -8507,15 +8507,25 @@
         ASSERT(index.getLevelIndex() == 0);
         ASSERT(index.getLayerCount() == 1);
 
+        int hSub = 1, vSub = 1;
+        gl::GetSubSampleFactor(formatInfo.internalFormat, &hSub, &vSub);
+
         for (uint32_t plane = 0; plane < yuvInfo.planeCount; plane++)
         {
+            gl::Offset planeOffset = offset;
+            if (plane > 0)
+            {
+                planeOffset.x = offset.x / hSub;
+                planeOffset.y = offset.y / vSub;
+            }
+
             VkBufferImageCopy copy           = {};
             copy.bufferOffset                = stagingOffset + yuvInfo.planeOffset[plane];
             copy.bufferRowLength             = 0;
             copy.bufferImageHeight           = 0;
             copy.imageSubresource.mipLevel   = 0;
             copy.imageSubresource.layerCount = 1;
-            gl_vk::GetOffset(offset, &copy.imageOffset);
+            gl_vk::GetOffset(planeOffset, &copy.imageOffset);
             gl_vk::GetExtent(yuvInfo.planeExtent[plane], &copy.imageExtent);
             copy.imageSubresource.baseArrayLayer = 0;
             copy.imageSubresource.aspectMask     = kPlaneAspectFlags[plane];
diff --git a/src/libANGLE/validationES3.cpp b/src/libANGLE/validationES3.cpp
index c331225..ce0ac2f 100644
--- a/src/libANGLE/validationES3.cpp
+++ b/src/libANGLE/validationES3.cpp
@@ -716,6 +716,15 @@
             return false;
         }
 
+        if (gl::IsYuvFormat(actualInternalFormat))
+        {
+            if ((xoffset % 2) != 0 || (yoffset % 2) != 0 || (width % 2) != 0 || (height % 2) != 0)
+            {
+                ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, kYuvTexSubImage2DOddOffsetOrDimension);
+                return false;
+            }
+        }
+
         if (width > 0 && height > 0 && depth > 0 && pixels == nullptr &&
             context->getState().getTargetBuffer(BufferBinding::PixelUnpack) == nullptr)
         {
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index cc0f892..aaf5758 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -90,6 +90,7 @@
 42265137 VULKAN : UniformTest.UnusedStructInlineUniform/* = SKIP
 42265137 VULKAN : UniformTest.UnusedStructInlineUniformWithSampler/* = SKIP
 42265405 VULKAN : Texture2DTestES3.TextureRGBUpdateWithPBO/ES3_Vulkan* = SKIP
+506193593 VULKAN : Texture2DTestES3.YUVTexSubImage2DNonZeroOffset_3Plane/* = SKIP
 42265226 VULKAN : GLSLTest_ES3.GLVertexIDIntegerTextureDrawElementsU8Line/* = SKIP
 42265226 VULKAN : GLSLTest_ES3.GLVertexIDIntegerTextureDrawElementsU8LineIds/* = SKIP
 235877059 VULKAN : MultisampleTestES3.ResolveToFBO/ES3_Vulkan_EmulatedPrerotation180 = SKIP
diff --git a/src/tests/gl_tests/TextureTest.cpp b/src/tests/gl_tests/TextureTest.cpp
index 5484dbb..c562a86 100644
--- a/src/tests/gl_tests/TextureTest.cpp
+++ b/src/tests/gl_tests/TextureTest.cpp
@@ -397,6 +397,7 @@
 {
   protected:
     Texture2DTestES3() : Texture2DTest() {}
+    void runYUVTexSubImage2DNonZeroOffsetTest(GLenum format);
 
     const char *getVertexShaderSource() override
     {
@@ -4916,6 +4917,138 @@
     ASSERT_GL_NO_ERROR();
 }
 
+void Texture2DTestES3::runYUVTexSubImage2DNonZeroOffsetTest(GLenum format)
+{
+    ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_ANGLE_yuv_internal_format"));
+
+    constexpr size_t kWidth  = 4;
+    constexpr size_t kHeight = 4;
+
+    struct YUVColor
+    {
+        GLubyte y;
+        GLubyte cb;
+        GLubyte cr;
+    };
+
+    // Color A: Y=0, Cb=128, Cr=128 -> RGB=(0, 0, 0)
+    constexpr YUVColor kColorA = {0, 128, 128};
+    const GLColor kExpectedRgbA(0, 0, 0, 255);
+
+    // Color B: Y=255, Cb=128, Cr=128 -> RGB=(255, 255, 255)
+    constexpr YUVColor kColorB = {255, 128, 128};
+    const GLColor kExpectedRgbB(255, 255, 255, 255);
+
+    std::vector<GLubyte> initData(24);
+    std::fill(initData.begin(), initData.begin() + 16, kColorA.y);  // Y
+    if (format == GL_G8_B8R8_2PLANE_420_UNORM_ANGLE)
+    {
+        for (size_t i = 16; i < 24; i += 2)
+        {
+            initData[i]     = kColorA.cb;
+            initData[i + 1] = kColorA.cr;
+        }
+    }
+    else
+    {
+        std::fill(initData.begin() + 16, initData.begin() + 20, kColorA.cb);
+        std::fill(initData.begin() + 20, initData.end(), kColorA.cr);
+    }
+
+    GLTexture yuvTexture;
+    glBindTexture(GL_TEXTURE_2D, yuvTexture);
+    glTexStorage2D(GL_TEXTURE_2D, 1, format, kWidth, kHeight);
+    ASSERT_GL_NO_ERROR();
+
+    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kWidth, kHeight, format, GL_UNSIGNED_BYTE,
+                    initData.data());
+    ASSERT_GL_NO_ERROR();
+
+    // Update quadrant (2,2) with Color B
+    std::vector<GLubyte> updateData(6);
+    std::fill(updateData.begin(), updateData.begin() + 4, kColorB.y);  // Y
+    updateData[4] = kColorB.cb;
+    updateData[5] = kColorB.cr;
+
+    glTexSubImage2D(GL_TEXTURE_2D, 0, 2, 2, 2, 2, format, GL_UNSIGNED_BYTE, updateData.data());
+    ASSERT_GL_NO_ERROR();
+
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+    ASSERT_GL_NO_ERROR();
+
+    // Draw a quad with the target texture
+    glUseProgram(mProgram);
+    glBindTexture(GL_TEXTURE_2D, yuvTexture);
+    glUniform1i(mTexture2DUniformLocation, 0);
+
+    drawQuad(mProgram, "position", 0.5f);
+    ASSERT_GL_NO_ERROR();
+
+    int w = getWindowWidth();
+    int h = getWindowHeight();
+
+    // Bottom-left (Color A)
+    EXPECT_PIXEL_COLOR_NEAR(w / 4, h / 4, kExpectedRgbA, 1);
+    // Top-right (Color B)
+    EXPECT_PIXEL_COLOR_NEAR(3 * w / 4, 3 * h / 4, kExpectedRgbB, 1);
+    // Bottom-right (Color A)
+    EXPECT_PIXEL_COLOR_NEAR(3 * w / 4, h / 4, kExpectedRgbA, 1);
+    // Top-left (Color A)
+    EXPECT_PIXEL_COLOR_NEAR(w / 4, 3 * h / 4, kExpectedRgbA, 1);
+}
+
+// Test sub-image uploads of data to ANGLE_yuv_internal_format 2-plane
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index cc0f892..aaf5758 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -90,6 +90,7 @@
 42265137 VULKAN : UniformTest.UnusedStructInlineUniform/* = SKIP
 42265137 VULKAN : UniformTest.UnusedStructInlineUniformWithSampler/* = SKIP
 42265405 VULKAN : Texture2DTestES3.TextureRGBUpdateWithPBO/ES3_Vulkan* = SKIP
+506193593 VULKAN : Texture2DTestES3.YUVTexSubImage2DNonZeroOffset_3Plane/* = SKIP
 42265226 VULKAN : GLSLTest_ES3.GLVertexIDIntegerTextureDrawElementsU8Line/* = SKIP
 42265226 VULKAN : GLSLTest_ES3.GLVertexIDIntegerTextureDrawElementsU8LineIds/* = SKIP
 235877059 VULKAN : MultisampleTestES3.ResolveToFBO/ES3_Vulkan_EmulatedPrerotation180 = SKIP
diff --git a/src/tests/gl_tests/TextureTest.cpp b/src/tests/gl_tests/TextureTest.cpp
index 5484dbb..c562a86 100644
--- a/src/tests/gl_tests/TextureTest.cpp
+++ b/src/tests/gl_tests/TextureTest.cpp
@@ -397,6 +397,7 @@
 {
   protected:
     Texture2DTestES3() : Texture2DTest() {}
+    void runYUVTexSubImage2DNonZeroOffsetTest(GLenum format);
 
     const char *getVertexShaderSource() override
     {
@@ -4916,6 +4917,138 @@
     ASSERT_GL_NO_ERROR();
 }
 
+void Texture2DTestES3::runYUVTexSubImage2DNonZeroOffsetTest(GLenum format)
+{
+    ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_ANGLE_yuv_internal_format"));
+
+    constexpr size_t kWidth  = 4;
+    constexpr size_t kHeight = 4;
+
+    struct YUVColor
+    {
+        GLubyte y;
+        GLubyte cb;
+        GLubyte cr;
+    };
+
+    // Color A: Y=0, Cb=128, Cr=128 -> RGB=(0, 0, 0)
+    constexpr YUVColor kColorA = {0, 128, 128};
+    const GLColor kExpectedRgbA(0, 0, 0, 255);
+
+    // Color B: Y=255, Cb=128, Cr=128 -> RGB=(255, 255, 255)
+    constexpr YUVColor kColorB = {255, 128, 128};
+    const GLColor kExpectedRgbB(255, 255, 255, 255);
+
+    std::vector<GLubyte> initData(24);
+    std::fill(initData.begin(), initData.begin() + 16, kColorA.y);  // Y
+    if (format == GL_G8_B8R8_2PLANE_420_UNORM_ANGLE)
+    {
+        for (size_t i = 16; i < 24; i += 2)
+        {
+            initData[i]     = kColorA.cb;
+            initData[i + 1] = kColorA.cr;
+        }
+    }
+    else
+    {
+        std::fill(initData.begin() + 16, initData.begin() + 20, kColorA.cb);
+        std::fill(initData.begin() + 20, initData.end(), kColorA.cr);
+    }
+
+    GLTexture yuvTexture;
+    glBindTexture(GL_TEXTURE_2D, yuvTexture);
+    glTexStorage2D(GL_TEXTURE_2D, 1, format, kWidth, kHeight);
+    ASSERT_GL_NO_ERROR();
+
+    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kWidth, kHeight, format, GL_UNSIGNED_BYTE,
+                    initData.data());
+    ASSERT_GL_NO_ERROR();
+
+    // Update quadrant (2,2) with Color B
+    std::vector<GLubyte> updateData(6);
+    std::fill(updateData.begin(), updateData.begin() + 4, kColorB.y);  // Y
+    updateData[4] = kColorB.cb;
+    updateData[5] = kColorB.cr;
+
+    glTexSubImage2D(GL_TEXTURE_2D, 0, 2, 2, 2, 2, format, GL_UNSIGNED_BYTE, updateData.data());
+    ASSERT_GL_NO_ERROR();
+
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+    ASSERT_GL_NO_ERROR();
+
+    // Draw a quad with the target texture
+    glUseProgram(mProgram);
+    glBindTexture(GL_TEXTURE_2D, yuvTexture);
+    glUniform1i(mTexture2DUniformLocation, 0);
+
+    drawQuad(mProgram, "position", 0.5f);
+    ASSERT_GL_NO_ERROR();
+
+    int w = getWindowWidth();
+    int h = getWindowHeight();
+
+    // Bottom-left (Color A)
+    EXPECT_PIXEL_COLOR_NEAR(w / 4, h / 4, kExpectedRgbA, 1);
+    // Top-right (Color B)
+    EXPECT_PIXEL_COLOR_NEAR(3 * w / 4, 3 * h / 4, kExpectedRgbB, 1);
+    // Bottom-right (Color A)
+    EXPECT_PIXEL_COLOR_NEAR(3 * w / 4, h / 4, kExpectedRgbA, 1);
+    // Top-left (Color A)
+    EXPECT_PIXEL_COLOR_NEAR(w / 4, 3 * h / 4, kExpectedRgbA, 1);
+}
+
+// Test sub-image uploads of data to ANGLE_yuv_internal_format 2-plane
+// textures with non-zero offsets.
+TEST_P(Texture2DTestES3, YUVTexSubImage2DNonZeroOffset_2Plane)
+{
+    runYUVTexSubImage2DNonZeroOffsetTest(GL_G8_B8R8_2PLANE_420_UNORM_ANGLE);
+}
+
+// Test sub-image uploads of data to ANGLE_yuv_internal_format 3-plane
+// textures with non-zero offsets.
+TEST_P(Texture2DTestES3, YUVTexSubImage2DNonZeroOffset_3Plane)
+{
+    runYUVTexSubImage2DNonZeroOffsetTest(GL_G8_B8_R8_3PLANE_420_UNORM_ANGLE);
+}
+
+// Verify newly added errors when uploading odd offsets and sizes to
+// ANGLE_yuv_internal_format textures.
+TEST_P(Texture2DTestES3, YUVTexSubImage2DInvalidOffsetsAndDimensions)
+{
+    ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_ANGLE_yuv_internal_format"));
+
+    constexpr size_t kWidth  = 4;
+    constexpr size_t kHeight = 4;
+
+    GLTexture yuvTexture;
+    glBindTexture(GL_TEXTURE_2D, yuvTexture);
+    glTexStorage2D(GL_TEXTURE_2D, 1, GL_G8_B8R8_2PLANE_420_UNORM_ANGLE, kWidth, kHeight);
+    ASSERT_GL_NO_ERROR();
+
+    std::vector<GLubyte> updateData(24, 0);  // Size doesn't matter much for error generation
+
+    // Odd xoffset
+    glTexSubImage2D(GL_TEXTURE_2D, 0, 1, 0, 2, 2, GL_G8_B8R8_2PLANE_420_UNORM_ANGLE,
+                    GL_UNSIGNED_BYTE, updateData.data());
+    EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+
+    // Odd yoffset
+    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 1, 2, 2, GL_G8_B8R8_2PLANE_420_UNORM_ANGLE,
+                    GL_UNSIGNED_BYTE, updateData.data());
+    EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+
+    // Odd width
+    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 1, 2, GL_G8_B8R8_2PLANE_420_UNORM_ANGLE,
+                    GL_UNSIGNED_BYTE, updateData.data());
+    EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+
+    // Odd height
+    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 2, 1, GL_G8_B8R8_2PLANE_420_UNORM_ANGLE,
+                    GL_UNSIGNED_BYTE, updateData.data());
+    EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+}
+
 // Test functionality of GL_ANGLE_yuv_internal_format glCopyTextureCHROMIUM
 TEST_P(Texture2DTestES3YUV, CopyTextureChromium)
 {
Loading diff…

Original Bug Report

reported by [email protected]

OOB GPU memory write in ANGLE Vulkan via unscaled YUV plane offsets

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: ANGLE’s Vulkan backend fails to scale offsets for subsampled chroma planes during YUV texture updates. A compromised renderer can bypass Luma-based validation to trigger a Vulkan OOB memory write. This enables a potential GPU sandbox escape.

Affected files:

  • third_party/angle/src/libANGLE/renderer/vulkan/vk_helpers.cpp
  • third_party/angle/src/libANGLE/validationES3.cpp

Estimated timestamp from git blame: 2021-07-19

Final Impact

This vulnerability culminates in a confirmed out-of-bounds write of attacker-controlled data into adjacent VkDeviceMemory via a vkCmdCopyBufferToImage command, granting a compromised renderer process a direct path to code execution within the highly-privileged GPU process.

Vulnerability Details

Initial logic and parameters are validated. A compromised renderer process establishes a standard GLES3 context utilizing the Passthrough Command Decoder and successfully enables the GL_ANGLE_yuv_internal_format extension. When the attacker issues a glTexSubImage2D command for a YUV texture, the validation routine in ValidateES3TexImageParametersBase (third_party/angle/src/libANGLE/validationES3.cpp:705) evaluates the provided xoffset and yoffset against the overall texture dimensions (the full Luma plane).

From here, execution jumps directly to the final transformation in the Vulkan backend (ImageHelper::stageSubresourceUpdateImpl within vk_helpers.cpp). The logic iterates through the YUV planes and maps the parameters into a VkBufferImageCopy structure. For subsampled chroma planes, the code directly applies gl_vk::GetOffset(offset, &copy.imageOffset). This injects the unscaled, luma-space offset into the command stream for a half-sized chroma plane. The Vulkan driver executes vkCmdCopyBufferToImage using an out-of-bounds destination coordinate, corrupting adjacent GPU heap memory.

Potential Reproduction Steps

(Note: These are suggested/potential steps as our tooling agent doesn’t yet have the ability to run code.)

  1. From a compromised renderer, initialize a GLES3 context via the passthrough command decoder and request the GL_ANGLE_yuv_internal_format extension.
  2. Allocate a 1024x1024 4:2:0 YUV texture (e.g., GL_G8_B8R8_2PLANE_420_UNORM_ANGLE) using glTexStorage2DEXT.
  3. Issue a glTexSubImage2D command with an offset of 768 and dimensions of 256. Standard processing applies.
  4. Validation passes because 768 + 256 <= 1024 (the luma plane dimension).
  5. The Vulkan backend stages the copy for Plane 1 (the 512x512 chroma plane) with an unscaled offset of 768, triggering the OOB write upon execution.

Suggested Fix

In third_party/angle/src/libANGLE/renderer/vulkan/vk_helpers.cpp within ImageHelper::stageSubresourceUpdateImpl, apply the correct subsampling division to the offset when computing copy.imageOffset for chroma planes, identical to how yuvInfo.planeExtent correctly scales the width and height.

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.

View on issue tracker