Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactHeap buffer overflow in ANGLE
DescriptionHeap buffer overflow in ANGLE
ComponentANGLE
Bug ClassOOB
Tracker500429259
Fix commit2251af425547 (angle/angle) +32/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Files Changed

  • src/libANGLE/validationES.cpp
  • src/tests/gl_tests/PalettedTextureTest.cpp
From 2251af425547303ed762901a24bbb8ea929aa31e Mon Sep 17 00:00:00 2001
From: Geoff Lang <[email protected]>
Date: Mon, 20 Apr 2026 12:14:55 -0400
Subject: [PATCH] Disallow palleted texture formats in glCopyTex[Sub]Image2D

The spec disallows these as CopyTexImage destinations.

Fixed: chromium:500429259
Change-Id: Ic4dd7839ae4867b421ee7fcf5d1c1cd681c48062
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7775619
Reviewed-by: Amirali Abdolrashidi <[email protected]>
Commit-Queue: Geoff Lang <[email protected]>
---

diff --git a/src/libANGLE/validationES.cpp b/src/libANGLE/validationES.cpp
index f9f902f..f3fed68 100644
--- a/src/libANGLE/validationES.cpp
+++ b/src/libANGLE/validationES.cpp
@@ -3924,7 +3924,7 @@
         isSubImage ? *texture->getFormat(target, level).info
                    : GetInternalFormatInfo(internalformat, GL_UNSIGNED_BYTE);
 
-    if (formatInfo.depthBits > 0 || formatInfo.compressed)
+    if (formatInfo.depthBits > 0 || formatInfo.compressed || formatInfo.paletted)
     {
         ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, kInvalidFormat);
         return false;
diff --git a/src/tests/gl_tests/PalettedTextureTest.cpp b/src/tests/gl_tests/PalettedTextureTest.cpp
index 991d212..34aae04 100644
--- a/src/tests/gl_tests/PalettedTextureTest.cpp
+++ b/src/tests/gl_tests/PalettedTextureTest.cpp
@@ -221,6 +221,37 @@
     EXPECT_GL_ERROR(GL_INVALID_ENUM);
 }
 
+// glCopyTexImage2D with paletted formats should fail.
+TEST_P(PalettedTextureTestES2, CopyTexImageShouldFail)
+{
+    ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_OES_compressed_paletted_texture"));
+
+    constexpr GLsizei W = 2;
+    constexpr GLsizei H = 2;
+
+    GLTexture srcTex;
+    glBindTexture(GL_TEXTURE_2D, srcTex);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, W, H, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+
+    GLFramebuffer fbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, srcTex, 0);
+    ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+    ASSERT_GL_NO_ERROR();
+
+    GLTexture dstTex;
+    glBindTexture(GL_TEXTURE_2D, dstTex);
+    glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_PALETTE4_RGBA8_OES, 0, 0, W, H, 0);
+    EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+
+    glCompressedTexImage2D(GL_TEXTURE_2D, 0, GL_PALETTE4_RGBA8_OES, W, H, 0, sizeof testImage,
+                           &testImage);
+    EXPECT_GL_NO_ERROR();
+
+    glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, W, H);
+    EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+}
+
 ANGLE_INSTANTIATE_TEST_ES1(PalettedTextureTest);
 
 ANGLE_INSTANTIATE_TEST_ES2(PalettedTextureTestES2);
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/gl_tests/PalettedTextureTest.cpp b/src/tests/gl_tests/PalettedTextureTest.cpp
index 991d212..34aae04 100644
--- a/src/tests/gl_tests/PalettedTextureTest.cpp
+++ b/src/tests/gl_tests/PalettedTextureTest.cpp
@@ -221,6 +221,37 @@
     EXPECT_GL_ERROR(GL_INVALID_ENUM);
 }
 
+// glCopyTexImage2D with paletted formats should fail.
+TEST_P(PalettedTextureTestES2, CopyTexImageShouldFail)
+{
+    ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_OES_compressed_paletted_texture"));
+
+    constexpr GLsizei W = 2;
+    constexpr GLsizei H = 2;
+
+    GLTexture srcTex;
+    glBindTexture(GL_TEXTURE_2D, srcTex);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, W, H, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+
+    GLFramebuffer fbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, srcTex, 0);
+    ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+    ASSERT_GL_NO_ERROR();
+
+    GLTexture dstTex;
+    glBindTexture(GL_TEXTURE_2D, dstTex);
+    glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_PALETTE4_RGBA8_OES, 0, 0, W, H, 0);
+    EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+
+    glCompressedTexImage2D(GL_TEXTURE_2D, 0, GL_PALETTE4_RGBA8_OES, W, H, 0, sizeof testImage,
+                           &testImage);
+    EXPECT_GL_NO_ERROR();
+
+    glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, W, H);
+    EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+}
+
 ANGLE_INSTANTIATE_TEST_ES1(PalettedTextureTest);
 
 ANGLE_INSTANTIATE_TEST_ES2(PalettedTextureTestES2);
Loading diff…

Original Bug Report

reported by [email protected]

Potential heap buffer overflow in ANGLE Vulkan via format mismatch in stageSubresourceUpdateFromFramebuffer

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 format size mismatch in ANGLE’s Vulkan backend leads to a potential heap buffer overflow in the GPU process during texture updates from a framebuffer. When falling back to a CPU readback for paletted textures, the scratch buffer is allocated assuming 4 bytes per pixel, but the pixel packing logic advances the pointer by 5 bytes per pixel, causing an out-of-bounds write.

Affected files:

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

Estimated timestamp from git blame: 2026-03-06

Vulnerability Details

In third_party/angle/src/libANGLE/renderer/vulkan/vk_helpers.cpp, the function stageSubresourceUpdateFromFramebuffer handles updating a texture’s subresource from a framebuffer. If hardware copy paths fail, it falls back to a CPU readback. If format conversion is required (loadFunction.requiresConversion), it allocates a scratch MemoryBuffer.

The size of this scratch buffer is calculated using storageFormat.pixelBytes:

const size_t bufferSize = static_cast<size_t>(clippedRectangle.width) * 
                          static_cast<size_t>(clippedRectangle.height) * 
                          storageFormat.pixelBytes;

For a texture with internal format GL_PALETTE8_RGBA8_OES, the actual Vulkan storage format resolves to R8G8B8A8_UNORM, which has a pixelBytes value of 4. Therefore, the buffer is allocated with 4 * width * height bytes.

However, the subsequent call to readPixelsImpl uses a PackPixelsParams object initialized with copyFormat:

const angle::Format &copyFormat =
    GetFormatFromFormatType(formatInfo.internalFormat, formatInfo.type);
PackPixelsParams params(clippedRectangle, copyFormat, static_cast<GLuint>(outputRowPitch),
                        isViewportFlipEnabled, nullptr, 0);

For GL_PALETTE8_RGBA8_OES, copyFormat resolves to PALETTE8_R8G8B8A8_UNORM, which has a pixelBytes value of 5 (Format_table_autogen.cpp:153).

When framebufferVk->readPixelsImpl subsequently invokes PackPixels (renderer_utils.cpp), the pointer for each pixel (x, y) is calculated as: dest = destWithOffset + y * params.outputPitch + x * params.destFormat->pixelBytes

Because destFormat->pixelBytes is 5 instead of 4, the horizontal offset grows by 5 bytes per pixel instead of 4. On the final row, the pointer advances width - 2 bytes past the end of the 4 * width * height allocated buffer. PackPixels then writes 4 bytes of attacker-controlled framebuffer data at this out-of-bounds location, resulting in a heap buffer overflow in the GPU process.

Potential Exploitation Steps

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

This vulnerability is reachable from a compromised renderer process sending commands to the GPU process.

  1. Create a Paletted Texture: The attacker issues a command to create a texture with the GL_PALETTE8_RGBA8_OES format.
  2. Create an AHB without the SAMPLED bit: The attacker allocates an Android AHardwareBuffer (AHB) with AHARDWAREBUFFER_USAGE_GPU_COLOR_OUTPUT but explicitly omits AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE. This AHB is imported into the GPU process via a SharedImage IPC. The resulting Vulkan image lacks the VK_IMAGE_USAGE_SAMPLED_BIT.
  3. Bind to FBO: The attacker binds this AHB-backed SharedImage as the color attachment of a Framebuffer Object.
  4. Issue Copy Command: The attacker issues glCopyTexSubImage2D, copying from the AHB FBO to the paletted texture. In release builds, Chrome sets EGL_CONTEXT_OPENGL_NO_ERROR_KHR, skipping ANGLE validation and allowing the command to reach the Vulkan backend.
  5. Trigger Fallback: In TextureVk::copySubImage, hardware transfer and draw copies both fail (because paletted formats are emulated, causing a format mismatch, and the AHB lacks the SAMPLED bit required for a draw copy). ANGLE falls back to stageSubresourceUpdateFromFramebuffer, triggering the undersized buffer allocation and the out-of-bounds write.

Suggested Fix

Ensure that the scratch buffer allocation size in stageSubresourceUpdateFromFramebuffer is calculated using copyFormat.pixelBytes rather than storageFormat.pixelBytes, so that the allocation matches the pointer arithmetic performed inside PackPixels.

const size_t bufferSize = static_cast<size_t>(clippedRectangle.width) *
                          static_cast<size_t>(clippedRectangle.height) *
                          copyFormat.pixelBytes;

Alternatively, PackPixels could be adjusted to rely entirely on storageFormat for this operation.

Evaluated with Chrome root at commit: 137d451a126685dd5010e6609db9f6d4a78d8234


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