CVE-2026-10929
Overview
Files Changed
src/libANGLE/validationES.cppsrc/tests/gl_tests/PalettedTextureTest.cpp
Patch
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);
Regression Test / PoC
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);
Original Bug Report
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 ©Format =
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.
- Create a Paletted Texture: The attacker issues a command to create a texture with the
GL_PALETTE8_RGBA8_OESformat. - Create an AHB without the SAMPLED bit: The attacker allocates an Android
AHardwareBuffer(AHB) withAHARDWAREBUFFER_USAGE_GPU_COLOR_OUTPUTbut explicitly omitsAHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE. This AHB is imported into the GPU process via aSharedImageIPC. The resulting Vulkan image lacks theVK_IMAGE_USAGE_SAMPLED_BIT. - Bind to FBO: The attacker binds this AHB-backed SharedImage as the color attachment of a Framebuffer Object.
- Issue Copy Command: The attacker issues
glCopyTexSubImage2D, copying from the AHB FBO to the paletted texture. In release builds, Chrome setsEGL_CONTEXT_OPENGL_NO_ERROR_KHR, skipping ANGLE validation and allowing the command to reach the Vulkan backend. - 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 tostageSubresourceUpdateFromFramebuffer, 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.