CVE-2026-11005
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
MultisampleTexture2DTestES31src/tests/gl_tests/TextureTest.cpp |
modified |
Files Changed
src/libANGLE/formatutils.cppsrc/libANGLE/formatutils.hsrc/libANGLE/renderer/d3d/TextureD3D.cppsrc/tests/gl_tests/TextureTest.cpp
Patch
From 97d33bc6e1356dcb2e63ea4ca7e6ebd2bc81a39d Mon Sep 17 00:00:00 2001
From: Gregg Tavares <gman@chromium.org>
Date: Fri, 03 Apr 2026 01:01:17 -0700
Subject: [PATCH] Fix for Angle D3D RGBX Data Upload via PBO issue
There were 2 issues
1. in formatutils.cpp, GL_RGBX8_ANGLE format and related
were set to always be enabled. This meant, even if
the extension was not on, ANGLE would incorrectly allow
using the format. It should have instead generated a
GL error. The extension is only turned on in vulkan
but the bug allowed its use on other backends.
This is fixed. ANGLE will correctly emit an error if
the extension is not turned on. Tests added.
2. In D3D, when uploading to GL_RGBX8_ANGLE with a
GL_RGB source, the code would take the fast path for
uploading which wrongly computed the size of the upload
based on the texture's internal format (4 bytes per
pixel), not on the source's format (3 bytes per pixel),
and so would access out of bounds data.
This is fixed by forcing the slow path that handles
this use case. A test has been added so if someone
enables the fast path that does not handle this case
they will get a test failure to fix the fast path.
Bug: chromium:495052581
Change-Id: Ic7257049ba18d12e9d5f775c81722306dad96c9b
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7723225
Commit-Queue: Gregg Tavares <gman@chromium.org>
Reviewed-by: Geoff Lang <geofflang@chromium.org>
Auto-Submit: Gregg Tavares <gman@chromium.org>
---
diff --git a/src/libANGLE/formatutils.cpp b/src/libANGLE/formatutils.cpp
index 16ec7bc..e9bb2df 100644
--- a/src/libANGLE/formatutils.cpp
+++ b/src/libANGLE/formatutils.cpp
@@ -1074,6 +1074,18 @@
//
// TODO(ynovikov): http://anglebug.com/42261549 Verify support fields of BGRA, depth, stencil
// and compressed formats. Perform texturable check as part of filterable and attachment checks.
+static bool RequireRGBXSRGBSupport(const Version &clientVersion, const Extensions &extensions)
+{
+ return extensions.rgbxInternalFormatANGLE &&
+ (clientVersion >= Version(3, 0) || extensions.sRGBEXT);
+}
+
+static bool RequireBGRXSRGBSupport(const Version &clientVersion, const Extensions &extensions)
+{
+ return extensions.textureFormatBGRA8888EXT &&
+ (clientVersion >= Version(3, 0) || extensions.sRGBEXT);
+}
+
static InternalFormatInfoMap BuildInternalFormatInfoMap()
{
InternalFormatInfoMap map;
@@ -1142,12 +1154,12 @@
AddRGBAFormat(&map, GL_R10X6G10X6B10X6A10X6_UNORM_ANGLEX, true, 10, 10, 10, 10, 0, GL_RGBA, GL_UNSIGNED_SHORT, GL_UNSIGNED_NORMALIZED, false, RequireES<3, 0>, NeverSupported, NeverSupported, NeverSupported, NeverSupported);
// Special format to emulate RGB8 with RGBA8 within ANGLE.
- AddRGBAXFormat(&map, GL_RGBX8_ANGLE, true, FB< 8, 8, 8, 0, 8, 0>(), GL_RGB, GL_UNSIGNED_BYTE, GL_UNSIGNED_NORMALIZED, false, AlwaysSupported, AlwaysSupported, AlwaysSupported, AlwaysSupported, NeverSupported);
- AddRGBAXFormat(&map, GL_RGBX8_SRGB_ANGLEX, true, FB< 8, 8, 8, 0, 8, 0>(), GL_RGB, GL_UNSIGNED_BYTE, GL_UNSIGNED_NORMALIZED, true, AlwaysSupported, AlwaysSupported, AlwaysSupported, AlwaysSupported, NeverSupported);
+ AddRGBAXFormat(&map, GL_RGBX8_ANGLE, true, FB< 8, 8, 8, 0, 8, 0>(), GL_RGB, GL_UNSIGNED_BYTE, GL_UNSIGNED_NORMALIZED, false, RequireExt<&Extensions::rgbxInternalFormatANGLE>, AlwaysSupported, RequireExt<&Extensions::rgbxInternalFormatANGLE>, RequireExt<&Extensions::rgbxInternalFormatANGLE>, NeverSupported);
+ AddRGBAXFormat(&map, GL_RGBX8_SRGB_ANGLEX, true, FB< 8, 8, 8, 0, 8, 0>(), GL_RGB, GL_UNSIGNED_BYTE, GL_UNSIGNED_NORMALIZED, true, RequireRGBXSRGBSupport, AlwaysSupported, RequireRGBXSRGBSupport, RequireRGBXSRGBSupport, NeverSupported);
// Special format to emulate BGR8 with BGRA8 within ANGLE.
- AddRGBAXFormat(&map, GL_BGRX8_ANGLEX, true, FB< 8, 8, 8, 0, 8, 0>(), GL_BGRA_EXT, GL_UNSIGNED_BYTE, GL_UNSIGNED_NORMALIZED, false, NeverSupported, AlwaysSupported, NeverSupported, NeverSupported, NeverSupported);
- AddRGBAXFormat(&map, GL_BGRX8_SRGB_ANGLEX, true, FB< 8, 8, 8, 0, 8, 0>(), GL_BGRA_EXT, GL_UNSIGNED_BYTE, GL_UNSIGNED_NORMALIZED, true, NeverSupported, AlwaysSupported, NeverSupported, NeverSupported, NeverSupported);
+ AddRGBAXFormat(&map, GL_BGRX8_ANGLEX, true, FB< 8, 8, 8, 0, 8, 0>(), GL_BGRA_EXT, GL_UNSIGNED_BYTE, GL_UNSIGNED_NORMALIZED, false, RequireExt<&Extensions::textureFormatBGRA8888EXT>, AlwaysSupported, NeverSupported, NeverSupported, NeverSupported);
+ AddRGBAXFormat(&map, GL_BGRX8_SRGB_ANGLEX, true, FB< 8, 8, 8, 0, 8, 0>(), GL_BGRA_EXT, GL_UNSIGNED_BYTE, GL_UNSIGNED_NORMALIZED, true, RequireBGRXSRGBSupport, AlwaysSupported, NeverSupported, NeverSupported, NeverSupported);
// This format is supported on ES 2.0 with two extensions, so keep it out-of-line to not widen the table above even more.
// | Internal format |sized| R | G | B | A |S | Format | Type | Component type | SRGB | Texture supported | Filterable | Texture attachment | Renderbuffer | Blend
diff --git a/src/libANGLE/formatutils.h b/src/libANGLE/formatutils.h
index 64d3265..22078b6 100644
--- a/src/libANGLE/formatutils.h
+++ b/src/libANGLE/formatutils.h
@@ -558,6 +558,21 @@
}
}
+ANGLE_INLINE bool IsRGBXOrBGRXFormat(const GLenum internalFormat)
+{
+ switch (internalFormat)
+ {
+ case GL_RGBX8_ANGLE:
+ case GL_RGBX8_SRGB_ANGLEX:
+ case GL_BGRX8_ANGLEX:
+ case GL_BGRX8_SRGB_ANGLEX:
+ return true;
+
+ default:
+ return false;
+ }
+}
+
// Check if an internal format is ever valid in ES3. Makes no checks about support for a specific
// context.
bool ValidES3InternalFormat(GLenum internalFormat);
diff --git a/src/libANGLE/renderer/d3d/TextureD3D.cpp b/src/libANGLE/renderer/d3d/TextureD3D.cpp
index be59fad..14174ec 100644
--- a/src/libANGLE/renderer/d3d/TextureD3D.cpp
+++ b/src/libANGLE/renderer/d3d/TextureD3D.cpp
@@ -303,6 +303,13 @@
return false;
}
+ // Emulated RGBX/BGRX formats must always use the slow path to ensure the alpha channel is set
+ // to 1.0.
+ if (gl::IsRGBXOrBGRXFormat(internalFormat.sizedInternalFormat))
+ {
+ return false;
+ }
+
// TODO(jmadill): Handle compressed internal formats
return (mTexStorage && !internalFormat.compressed);
}
diff --git a/src/tests/gl_tests/TextureTest.cpp b/src/tests/gl_tests/TextureTest.cpp
index cb46edd..2b6399c 100644
--- a/src/tests/gl_tests/TextureTest.cpp
+++ b/src/tests/gl_tests/TextureTest.cpp
@@ -12,7 +12,9 @@
#include "test_utils/ANGLETest.h"
#include "test_utils/gl_raii.h"
+#include <cstdint>
#include <limits>
+#include <vector>
using namespace angle;
@@ -491,6 +493,9 @@
void testCopyImage(const APIExtensionVersion usedExtension);
void testCopyImageDepthStencil(const APIExtensionVersion usedExtension);
+
+ void InternalFormatNotEnabledHelper(GLenum internalFormat, GLenum uploadFormat);
+ void TextureUploadPBOHelper(GLenum internalFormat, GLenum uploadFormat);
};
class MultisampleTexture2DTestES31 : public Texture2DTest
@@ -7150,6 +7155,98 @@
ASSERT_GL_NO_ERROR();
}
+void Texture2DTestES3::InternalFormatNotEnabledHelper(GLenum internalFormat, GLenum uploadFormat)
+{
+ GLTexture texture;
+ glBindTexture(GL_TEXTURE_2D, texture);
+ glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, 1, 1, 0, uploadFormat, GL_UNSIGNED_BYTE,
+ nullptr);
+ GLenum error = glGetError();
+ EXPECT_NE(static_cast<GLenum>(GL_NONE), error) << "internalFormat: " << internalFormat;
+
+ glTexStorage2D(GL_TEXTURE_2D, 1, internalFormat, 1, 1);
+ error = glGetError();
+ EXPECT_NE(static_cast<GLenum>(GL_NONE), error) << "internalFormat: " << internalFormat;
+}
+
+void Texture2DTestES3::TextureUploadPBOHelper(GLenum internalFormat, GLenum uploadFormat)
+{
+ constexpr GLint kWidth = 16;
+ constexpr GLint kHeight = 16;
+
+ GLTexture tex;
+ glBindTexture(GL_TEXTURE_2D, tex);
+ glTexStorage2D(GL_TEXTURE_2D, 1, internalFormat, kWidth, kHeight);
+ ASSERT_GL_NO_ERROR();
+
+ // Create a PBO with size matching the upload format.
+ GLsizeiptr pixelBytes = 3;
+ GLsizeiptr pboSize = kWidth * pixelBytes * kHeight;
+ GLBuffer pbo;
+ glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo);
+
+ // Initialize the PBO with unique colors.
+ std::vector<uint8_t> pboData(pboSize);
+ for (GLsizeiptr i = 0; i < pboSize; i++)
+ {
+ pboData[i] = static_cast<uint8_t>(i % 255);
+ }
+ glBufferData(GL_PIXEL_UNPACK_BUFFER, pboSize, pboData.data(), GL_STATIC_DRAW);
+
+ glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kWidth, kHeight, uploadFormat, GL_UNSIGNED_BYTE,
+ nullptr);
+ ASSERT_GL_NO_ERROR();
+
+ // Read back the texture to verify the data.
+ GLFramebuffer fbo;
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0);
+ ASSERT_GL_NO_ERROR();
+ EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+ std::vector<GLColor> readback(kWidth * kHeight);
+ glReadPixels(0, 0, kWidth, kHeight, GL_RGBA, GL_UNSIGNED_BYTE, readback.data());
Regression Test / PoC
diff --git a/src/tests/gl_tests/TextureTest.cpp b/src/tests/gl_tests/TextureTest.cpp
index cb46edd..2b6399c 100644
--- a/src/tests/gl_tests/TextureTest.cpp
+++ b/src/tests/gl_tests/TextureTest.cpp
@@ -12,7 +12,9 @@
#include "test_utils/ANGLETest.h"
#include "test_utils/gl_raii.h"
+#include <cstdint>
#include <limits>
+#include <vector>
using namespace angle;
@@ -491,6 +493,9 @@
void testCopyImage(const APIExtensionVersion usedExtension);
void testCopyImageDepthStencil(const APIExtensionVersion usedExtension);
+
+ void InternalFormatNotEnabledHelper(GLenum internalFormat, GLenum uploadFormat);
+ void TextureUploadPBOHelper(GLenum internalFormat, GLenum uploadFormat);
};
class MultisampleTexture2DTestES31 : public Texture2DTest
@@ -7150,6 +7155,98 @@
ASSERT_GL_NO_ERROR();
}
+void Texture2DTestES3::InternalFormatNotEnabledHelper(GLenum internalFormat, GLenum uploadFormat)
+{
+ GLTexture texture;
+ glBindTexture(GL_TEXTURE_2D, texture);
+ glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, 1, 1, 0, uploadFormat, GL_UNSIGNED_BYTE,
+ nullptr);
+ GLenum error = glGetError();
+ EXPECT_NE(static_cast<GLenum>(GL_NONE), error) << "internalFormat: " << internalFormat;
+
+ glTexStorage2D(GL_TEXTURE_2D, 1, internalFormat, 1, 1);
+ error = glGetError();
+ EXPECT_NE(static_cast<GLenum>(GL_NONE), error) << "internalFormat: " << internalFormat;
+}
+
+void Texture2DTestES3::TextureUploadPBOHelper(GLenum internalFormat, GLenum uploadFormat)
+{
+ constexpr GLint kWidth = 16;
+ constexpr GLint kHeight = 16;
+
+ GLTexture tex;
+ glBindTexture(GL_TEXTURE_2D, tex);
+ glTexStorage2D(GL_TEXTURE_2D, 1, internalFormat, kWidth, kHeight);
+ ASSERT_GL_NO_ERROR();
+
+ // Create a PBO with size matching the upload format.
+ GLsizeiptr pixelBytes = 3;
+ GLsizeiptr pboSize = kWidth * pixelBytes * kHeight;
+ GLBuffer pbo;
+ glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo);
+
+ // Initialize the PBO with unique colors.
+ std::vector<uint8_t> pboData(pboSize);
+ for (GLsizeiptr i = 0; i < pboSize; i++)
+ {
+ pboData[i] = static_cast<uint8_t>(i % 255);
+ }
+ glBufferData(GL_PIXEL_UNPACK_BUFFER, pboSize, pboData.data(), GL_STATIC_DRAW);
+
+ glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kWidth, kHeight, uploadFormat, GL_UNSIGNED_BYTE,
+ nullptr);
+ ASSERT_GL_NO_ERROR();
+
+ // Read back the texture to verify the data.
+ GLFramebuffer fbo;
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0);
+ ASSERT_GL_NO_ERROR();
+ EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+ std::vector<GLColor> readback(kWidth * kHeight);
+ glReadPixels(0, 0, kWidth, kHeight, GL_RGBA, GL_UNSIGNED_BYTE, readback.data());
+ ASSERT_GL_NO_ERROR();
+
+ for (int y = 0; y < kHeight; y++)
+ {
+ for (int x = 0; x < kWidth; x++)
+ {
+ size_t pixelIndex = y * kWidth + x;
+ size_t pboIndex = pixelIndex * pixelBytes;
+
+ switch (internalFormat)
+ {
+ case GL_RGBX8_ANGLE:
+ case GL_RGBX8_SRGB_ANGLEX:
+ EXPECT_EQ(readback[pixelIndex].R, pboData[pboIndex + 0])
+ << "at " << x << ", " << y;
+ EXPECT_EQ(readback[pixelIndex].G, pboData[pboIndex + 1])
+ << "at " << x << ", " << y;
+ EXPECT_EQ(readback[pixelIndex].B, pboData[pboIndex + 2])
+ << "at " << x << ", " << y;
+ break;
+
+ case GL_BGRX8_ANGLEX:
+ case GL_BGRX8_SRGB_ANGLEX:
+ // Upload is GL_BGR_EXT so pboData has [B, G, R]
+ EXPECT_EQ(readback[pixelIndex].R, pboData[pboIndex + 2])
+ << "at " << x << ", " << y;
+ EXPECT_EQ(readback[pixelIndex].G, pboData[pboIndex + 1])
+ << "at " << x << ", " << y;
+ EXPECT_EQ(readback[pixelIndex].B, pboData[pboIndex + 0])
+ << "at " << x << ", " << y;
+ break;
+ default:
+ UNREACHABLE();
+ }
+ // sRGB formats might have conversion errors, so we primarily check they didn't
+ // crash.
+ EXPECT_EQ(readback[pixelIndex].A, 255) << "at " << x << ", " << y;
+ }
+ }
+}
+
// Test basic GL_EXT_copy_image copy with a depth/stencil texture
TEST_P(Texture2DTestES3, CopyImageEXTDepthStencil)
{
@@ -8788,6 +8885,46 @@
EXPECT_PIXEL_ALPHA_EQ(0, 0, 255);
}
+// Test that GL_RGBX8_ANGLE results in GL_INVALID_ENUM if the extensions are not enabled.
+TEST_P(Texture2DTestES3, InternalFormatNotEnabled_RGBX8_ANGLE)
+{
+ // Note: This is the opposite of the usual test for extension. We only run the test
+ // if the extension is NOT available.
+ ANGLE_SKIP_TEST_IF(IsGLExtensionEnabled("GL_ANGLE_rgbx_internal_format"));
+ InternalFormatNotEnabledHelper(GL_RGBX8_ANGLE, GL_RGB);
+}
+
+// Test that GL_RGBX8_SRGB_ANGLEX results in GL_INVALID_ENUM if the extensions are not enabled.
+TEST_P(Texture2DTestES3, InternalFormatNotEnabled_RGBX8_SRGB_ANGLEX)
+{
+ // Note: This is the opposite of the usual test for extension. We only run the test
+ // if both extensions are NOT available. If neither is available, or if one both not
+ // both are available, then we need to test the format can't be used.
+ ANGLE_SKIP_TEST_IF(IsGLExtensionEnabled("GL_ANGLE_rgbx_internal_format") &&
+ IsGLExtensionEnabled("GL_EXT_sRGB"));
+ InternalFormatNotEnabledHelper(GL_RGBX8_SRGB_ANGLEX, GL_RGB);
+}
+
+// Test that GL_BGRX8_ANGLEX results in GL_INVALID_ENUM if the extensions are not enabled.
+TEST_P(Texture2DTestES3, InternalFormatNotEnabled_BGRX8_ANGLEX)
+{
+ // Note: This is the opposite of the usual test for extension. We only run the test
+ // if the extension is NOT available.
+ ANGLE_SKIP_TEST_IF(IsGLExtensionEnabled("GL_EXT_texture_format_BGRA8888"));
+ InternalFormatNotEnabledHelper(GL_BGRX8_ANGLEX, GL_BGR_EXT);
+}
+
+// Test that GL_BGRX8_SRGB_ANGLEX results in GL_INVALID_ENUM if the extensions are not enabled.
+TEST_P(Texture2DTestES3, InternalFormatNotEnabled_BGRX8_SRGB_ANGLEX)
+{
+ // Note: This is the opposite of the usual test for extension. We only run the test
+ // if both extensions are NOT available. If neither is available, or if one both not
+ // both are available, then we need to test the format can't be used.
+ ANGLE_SKIP_TEST_IF(IsGLExtensionEnabled("GL_EXT_texture_format_BGRA8888") &&
+ IsGLExtensionEnabled("GL_EXT_sRGB"));
+ InternalFormatNotEnabledHelper(GL_BGRX8_SRGB_ANGLEX, GL_BGR_EXT);
+}
+
// When sampling a texture without an alpha channel, "1" is returned as the alpha value.
// ES 3.0.4 table 3.24
TEST_P(Texture2DTestES3, TextureRGBXImplicitAlpha1)
@@ -8903,6 +9040,84 @@
ASSERT_GL_NO_ERROR();
}
+// Regression test for a bug where D3D11 backend incorrectly computes the source row pitch
+// for emulated RGBX/BGRX textures during TexSubImage2D uploads from a PBO.
+TEST_P(Texture2DTestES3, TextureUploadPBO_RGBX8_ANGLE)
+{
+ ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_ANGLE_rgbx_internal_format"));
+ TextureUploadPBOHelper(GL_RGBX8_ANGLE, GL_RGB);
+}
+
+// Regression test for a bug where D3D11 backend incorrectly computes the source row pitch
+// for emulated RGBX/BGRX textures during TexSubImage2D uploads from a PBO.
+TEST_P(Texture2DTestES3, TextureUploadPBO_RGBX8_SRGB_ANGLEX)
+{
+ ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_ANGLE_rgbx_internal_format"));
+ ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_EXT_sRGB"));
+ TextureUploadPBOHelper(GL_RGBX8_SRGB_ANGLEX, GL_RGB);
+}
+
+// Test that GL_RGBX8_ANGLE can be read back into a PBO.
+TEST_P(Texture2DTestES3, TextureRGBXReadPBO)
+{
+ ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_ANGLE_rgbx_internal_format"));
+
+ constexpr GLint kWidth = 16;
+ constexpr GLint kHeight = 16;
+
+ GLTexture tex;
+ glBindTexture(GL_TEXTURE_2D, tex);
+ glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBX8_ANGLE, kWidth, kHeight);
+ ASSERT_GL_NO_ERROR();
+
+ GLFramebuffer fbo;
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0);
+ ASSERT_GL_NO_ERROR();
+ EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+ // Clear to a unique color.
+ GLColor clearColor(10, 20, 30, 40);
+ glClearColor(clearColor.R / 255.0f, clearColor.G / 255.0f, clearColor.B / 255.0f,
+ clearColor.A / 255.0f);
+ glClear(GL_COLOR_BUFFER_BIT);
+ ASSERT_GL_NO_ERROR();
+
+ // RGBX8 means alpha should be 255.
+ GLColor expectedColor(clearColor.R, clearColor.G, clearColor.B, 255);
+
+ GLBuffer pbo;
+ glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo);
+ glBufferData(GL_PIXEL_PACK_BUFFER, kWidth * kHeight * 4, nullptr, GL_STREAM_READ);
+ ASSERT_GL_NO_ERROR();
+
+ // Read back as GL_RGBA.
+ glReadPixels(0, 0, kWidth, kHeight, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+ ASSERT_GL_NO_ERROR();
+
+ void *ptr = glMapBufferRange(GL_PIXEL_PACK_BUFFER, 0, kWidth * kHeight * 4, GL_MAP_READ_BIT);
+ ASSERT_NE(ptr, nullptr);
+ const GLColor *colors = reinterpret_cast<const GLColor *>(ptr);
+ for (int i = 0; i < kWidth * kHeight; i++)
+ {
+ EXPECT_EQ(colors[i], expectedColor) << "at index " << i;
+ }
+ glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
+
+ // Read back as GL_RGBX8_ANGLE.
+ glReadPixels(0, 0, kWidth, kHeight, GL_RGBX8_ANGLE, GL_UNSIGNED_BYTE, nullptr);
+ ASSERT_GL_NO_ERROR();
+
+ ptr = glMapBufferRange(GL_PIXEL_PACK_BUFFER, 0, kWidth * kHeight * 4, GL_MAP_READ_BIT);
+ ASSERT_NE(ptr, nullptr);
+ colors = reinterpret_cast<const GLColor *>(ptr);
+ for (int i = 0; i < kWidth * kHeight; i++)
+ {
+ EXPECT_EQ(colors[i], expectedColor) << "at index " << i;
+ }
+ glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
+}
+
// When sampling a texture without an alpha channel, "1" is returned as the alpha value.
// ES 3.0.4 table 3.24
TEST_P(Texture2DTest, TextureLuminanceImplicitAlpha1)
Original Bug Report
Heap OOB read in ANGLE D3D11 backend via `GL_RGBX8_ANGLE` row pitch mismatch between validation and texture upload
Summary
ANGLE’s D3D11 backend computes the source row pitch for texture uploads using the texture’s internal format rather than the upload format. When a compromised renderer creates a texture with GL_RGBX8_ANGLE (4 bytes per pixel) and uploads data as GL_RGB (3 bytes per pixel), the validation layer sizes the input buffer using the 3-byte format while the upload path strides through the data using the 4-byte format. The resulting heap-buffer-overflow occurs in CPU-side pixel conversion within the GPU process. GL_RGBX8_ANGLE passes TexStorage validation unconditionally because its textureSupport function is registered as AlwaysSupported without an extension gate. Platform: Windows (D3D11 ANGLE backend).
Bisect
Introducing Commit: 4f42a4d3f72518e310cad6ef5cc991078577a747
- Date: 2021-12-08
- Author: Tim Van Patten
- Review: https://chromium-review.googlesource.com/c/angle/angle/+/3312367
This commit registered GL_RGBX8_ANGLE with AlwaysSupported for textureSupport in the format table. The original GL_RGBX8_ANGLEX format was added in c0aa61082d (2021-09-28) with more restrictive support. The 4f42a4d commit broadened support but did not add an extension gate in ValidateES2TexStorageParametersBase, creating the validation gap that allows a compromised renderer to instantiate the format.
Root Cause
The vulnerability has two components that combine to produce a heap-buffer-overflow.
The first component is a validation gap in TexStorage2DEXT. ValidateES2TexStorageParametersBase checks whether the requested internal format is supported by calling the format’s textureSupport function:
// validationES2.cpp — ValidateES2TexStorageParametersBase
const InternalFormat &formatInfo = GetSizedInternalFormatInfo(internalformat);
if (!formatInfo.textureSupport(context->getClientVersion(), context->getExtensions()))
{
return false;
}
The format table registers GL_RGBX8_ANGLE with AlwaysSupported:
// formatutils.cpp
AddRGBAXFormat(&map, GL_RGBX8_ANGLE, true, ..., AlwaysSupported, AlwaysSupported, ...);
The TexImage path has a separate extension check for rgbxInternalFormatANGLE, but the TexStorage path does not. A compromised renderer can therefore create a texture with GL_RGBX8_ANGLE as its internal format through the TexStorage2DEXT command, which the passthrough decoder forwards to ANGLE without additional validation.
The second component is a row pitch mismatch between validation and the D3D11 upload implementation. When TexSubImage2D is called on the RGBX8 texture with format=GL_RGB and type=GL_UNSIGNED_BYTE, the validation layer computes the input data size using the upload format (3 bytes per pixel):
// validationES2.cpp — ValidateES2TexImageParametersBase
GLenum sizeCheckFormat = isSubImage ? format : internalformat;
return ValidImageDataSize(context, ..., sizeCheckFormat, type, pixels, imageSize);
The D3D11 upload path, however, recomputes srcRowPitch using the texture’s actual internal format:
// TextureStorage11.cpp — TextureStorage11::setData
const gl::InternalFormat &internalFormatInfo =
gl::GetInternalFormatInfo(image->getInternalFormat(), type);
internalFormatInfo.computeRowPitch(type, width, unpack.alignment, unpack.rowLength, &srcRowPitch);
For GL_RGBX8_ANGLE, formatutils.cpp special-cases the component count to 4:
// formatutils.cpp — InternalFormat::computePixelBytes
if (sizedInternalFormat == GL_RGBX8_ANGLE)
{
components = 4;
}
This produces srcRowPitch = width * 4 instead of the expected width * 3. The load function LoadToNativeByte3To4Impl then reads the source data with 3-byte pixel access but advances rows using the inflated 4-byte row pitch. For a 256×256 texture, the validation allows 196608 bytes (256 × 3 × 256), while the D3D11 backend reads up to 261888 bytes (1024 × 255 + 768), overflowing the heap allocation by 65280 bytes.
The PBO upload path is particularly suited for triggering this because ANGLE’s D3D11 backend retrieves PBO data through Buffer11::getData, which returns a pointer to SystemMemoryStorage backed by angle::MemoryBuffer, a malloc-allocated buffer. AddressSanitizer tracks this allocation and detects the overflow in the GPU process.
Reproduce
This issue was tested on Chromium commit 8a39e4056b7ab6470456e5281ebe8cf4a236ec08 running on Windows 11 with an NVIDIA GeForce RTX 4060 Ti. The bug is in the ANGLE D3D11 backend and requires only the default Windows GPU configuration.
Apply the patch, which modifies WebGL2RenderingContextBase::texSubImage3D in the renderer process to inject GL_RGBX8_ANGLE texture commands when it detects a sentinel PBO offset value. This simulates a compromised renderer sending crafted GL commands through the passthrough command buffer decoder. No GPU process code is modified.
Configure an ASAN build with out/asan/args.gn as follows, then build:
is_asan = true
is_debug = false
dcheck_always_on = false
target_cpu = "x64"
git apply patch.diff
autoninja -C out/asan chrome
Launch Chrome and open the poc page(poc.html uses an absolute path).
$ out\asan\chrome.exe --enable-logging=stderr --user-data-dir=/tmp/poc /path/to/poc.html 2>/tmp/asan.txt
$ cat /tmp/asan.txt
The PoC page creates a WebGL2 context and calls texSubImage3D with PBO offset 0x120012, which the patched renderer intercepts. The renderer unbinds the PBO, creates a 256×256 texture with internal format GL_RGBX8_ANGLE (0x96BA) via TexStorage2DEXT, then creates a 196608-byte PBO matching the RGB 3-bytes-per-pixel validation expectation. It calls TexSubImage2D with format=GL_RGB and type=GL_UNSIGNED_BYTE via the PBO. ANGLE’s validation computes endByte as 196608 using 3 bytes per pixel and passes the PBO size check. The D3D11 backend recalculates srcRowPitch using the texture’s internal format GL_RGBX8_ANGLE, which has 4 components, producing a row pitch of 1024 instead of the correct 768. The load function LoadToNativeByte3To4Impl reads with 1024-byte row stride across 256 rows, accessing a total of 261888 bytes from the 196608-byte PBO heap allocation. AddressSanitizer detects the 65280-byte heap-buffer-overflow in the GPU process.
ASAN output:
=================================================================
==25884==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x1211da001800 at pc 0x7ffabf0f6ae5 bp 0x00650bbfd6b0 sp 0x00650bbfd6f8
READ of size 4 at 0x1211da001800 thread T0
==25884==*** WARNING: Failed to initialize DbgHelp! ***
==25884==*** Most likely this means that the app is already ***
==25884==*** using DbgHelp, possibly with incompatible flags. ***
==25884==*** Due to technical reasons, symbolization might crash ***
==25884==*** or produce wrong results. ***
[5908:21516:0323/045638.730:ERROR:google_apis\gcm\engine\registration_request.cc:291] Registration response error message: DEPRECATED_ENDPOINT
#0 0x7ffabf0f6ae4 in angle::LoadToNativeByte3To4Impl D:\src\chromium\src\third_party\angle\src\image_util\loadimage.inc:198
#1 0x7ffabf0f5e2f in angle::LoadToNative3To4<unsigned char,255> D:\src\chromium\src\third_party\angle\src\image_util\loadimage.inc:243
#2 0x7ffabf25e691 in rx::TextureStorage11::setData D:\src\chromium\src\third_party\angle\src\libANGLE\renderer\d3d\d3d11\TextureStorage11.cpp:921
#3 0x7ffabf315ab0 in rx::TextureD3D::subImage D:\src\chromium\src\third_party\angle\src\libANGLE\renderer\d3d\TextureD3D.cpp:375
#4 0x7ffabf31dd77 in rx::TextureD3D_2D::setSubImage D:\src\chromium\src\third_party\angle\src\libANGLE\renderer\d3d\TextureD3D.cpp:1148
......
References
ValidateES2TexStorageParametersBasetextureSupportcheck: https://source.chromium.org/chromium/chromium/src/+/main:third_party/angle/src/libANGLE/validationES2.cpp;l=1843GL_RGBX8_ANGLEregistered asAlwaysSupported: https://source.chromium.org/chromium/chromium/src/+/main:third_party/angle/src/libANGLE/formatutils.cpp;l=1151RGBX8_ANGLEcomponent count special case: https://source.chromium.org/chromium/chromium/src/+/main:third_party/angle/src/libANGLE/formatutils.cpp;l=1692TextureStorage11::setDatauses internal format forsrcRowPitch: https://source.chromium.org/chromium/chromium/src/+/main:third_party/angle/src/libANGLE/renderer/d3d/d3d11/TextureStorage11.cpp;l=872ValidateES2TexImageParametersBaseuses upload format forsizeCheckFormat: https://source.chromium.org/chromium/chromium/src/+/main:third_party/angle/src/libANGLE/validationES2.cpp;l=1746LoadToNativeByte3To4Implrow-stride read: https://source.chromium.org/chromium/chromium/src/+/main:third_party/angle/src/image_util/loadimage.inc;l=198- Passthrough decoder
DoTexStorage2DEXT(no validation): https://source.chromium.org/chromium/chromium/src/+/main:gpu/command_buffer/service/gles2_cmd_decoder_passthrough_doers.cc;l=3574
Credit
Please use 86ac1f1587b71893ed2ad792cd7dde32 as the credit for this vulnerability. Thank you.
- https://chromium-review.googlesource.com/c/angle/angle/+/3312367
- https://source.chromium.org/chromium/chromium/src/+/main:gpu/command_buffer/service/gles2_cmd_decoder_passthrough_doers.cc;l=3574
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/angle/src/image_util/loadimage.inc;l=198
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/angle/src/libANGLE/formatutils.cpp;l=1151
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/angle/src/libANGLE/formatutils.cpp;l=1692
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/angle/src/libANGLE/renderer/d3d/d3d11/TextureStorage11.cpp;l=872
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/angle/src/libANGLE/validationES2.cpp;l=1746
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/angle/src/libANGLE/validationES2.cpp;l=1843