CVE-2026-102331
Overview
Background
- ANGLE
- Chrome’s translation layer that implements OpenGL ES / WebGL on top of native drivers (GL, Vulkan, D3D), performing API validation before handing commands to the backend.
- `ValidImageAllocationSize()` / `computeImageSize()`
- The validation-time check (in
validationES.cpp) that computes a texture image’s byte size from itsExtentsand format and rejects it if it exceedsmaxTextureBytes. - NPOT / POT rounding
- Non-power-of-two dimensions rounded up to the next power of two via
gl::ceilPow2(); certain PowerVR Rogue drivers do this internally for 3D textures before their own size math. - `roundUp3DTextureSizeToPOTForLimit`
- A newly added
Limitations/Featureflag enabling ANGLE to mirror that driver behavior when validating 3D-texture size limits.
Root Cause Analysis
ANGLE validated 3D-texture allocation size using the requested width/height/depth in ValidImageAllocationSize() and in the glTexStorage3D-path check, so a texture whose exact dimensions fit under maxTextureBytes was accepted. On affected PowerVR Rogue drivers, however, the driver internally rounds each 3D dimension up to the next power of two before doing its own allocation size math, so an accepted NPOT texture (e.g. 65x65x65) actually causes the driver to size/allocate for the much larger POT extents (128x128x128), exceeding the intended byte limit and causing a driver-side buffer overflow.
The fix adds RoundImageAllocationExtentIfNeeded(), which — when the workaround is enabled and depth > 1 — rounds each extent up with gl::ceilPow2() (clamped via gl::clampCast<GLsizei>) before calling computeImageSize(), so validation now checks the post-rounding size the driver will really use and rejects textures that would overflow. The workaround is gated to affected PowerVR drivers (isPowerVRDriver && powerVRVersion < {26, 2}) and only to 3D textures (depth > 1), leaving 2D textures unchanged, as the test’s 724x724 2D case confirms.
Attack Path
- Run on an affected device
The victim uses Chrome/WebGL on a device with a PowerVR Rogue driver older than version
26.2, whereroundUp3dTextureSizeToPOTForLimitwould be enabled. - Request an NPOT 3D texture near the limit
Attacker-controlled WebGL/GLES content calls
glTexImage3D/glTexStorage3Dwith NPOT 3D dimensions whose exact byte size is undermaxTextureBytesbut whose POT-rounded size exceeds it (e.g.65x65x65RGB565, which rounds to128x128x128). - Pass validation pre-patch
Old
ValidImageAllocationSize()computes the size from the requested extents, finds it within limits, and allows the allocation to proceed to the driver. - Driver overflows
The PowerVR driver rounds the dimensions up to POT and performs allocation size math on the larger extents, producing a buffer overflow in the driver/GPU-process allocation
(inferred).
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
Texture3DSizeLimitPOTRoundUpTestsrc/tests/gl_tests/TextureTest.cpp |
modified | |
Texture2DTestES3_NonZeroBaseLevelGenMipmapssrc/tests/gl_tests/TextureTest.cpp |
modified |
Files Changed
src/tests/gl_tests/TextureTest.cpputil/autogen/angle_features_autogen.hsrc/libANGLE/renderer/gl/renderergl_utils.cppinclude/platform/autogen/FeaturesGL_autogen.hsrc/libANGLE/Caps.hsrc/libANGLE/validationES.cpputil/autogen/angle_features_autogen.cppinclude/platform/gl_features.json
Audit Directions
- Validation vs. driver reality mismatchAudit other ANGLE size/limit checks that use app-supplied dimensions where a backend driver silently transforms them (POT rounding, alignment padding, format expansion) before allocating, since validation must model the effective size.
- 3D/array texture size mathReview
computeImageSize()callers and 3D/array-texture paths for overflow where per-dimension rounding multiplies (a small NPOT-to-POT jump per axis compounds across three axes). - Per-vendor workaround gatingCheck that similar driver-quirk workarounds (
Workaroundsingl_features.json) are correctly conditioned and applied on every relevant validation entry point, not just one, so no allocation path bypasses the adjusted limit.
Patch
From 40b709be4f1f62cab5fac9d888cd5e8b9259d068 Mon Sep 17 00:00:00 2001
From: Kai Ninomiya <kainino@chromium.org>
Date: Thu, 17 Sep 2026 06:08:28 +0000
Subject: [PATCH] Add round_up_3d_texture_size_to_POT_for_limit for PowerVR
PowerVR drivers have an issue which seems to be specific to 3D textures
where they round up NPOT sizes to POT sizes before doing some size math.
Bug: chromium:551673541
Change-Id: I64a1efb7a1afb8021f80a3be3aef0f8a733104ae
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8424039
Auto-Submit: Kai Ninomiya <kainino@chromium.org>
Commit-Queue: Geoff Lang <geofflang@chromium.org>
Reviewed-by: Geoff Lang <geofflang@chromium.org>
---
diff --git a/include/platform/gl_features.json b/include/platform/gl_features.json
index 2a90427..080bf18 100644
--- a/include/platform/gl_features.json
+++ b/include/platform/gl_features.json
@@ -362,6 +362,15 @@
"issue": "http://crbug.com/927470"
},
{
+ "name": "round_up_3d_texture_size_to_POT_for_limit",
+ "category": "Workarounds",
+ "description": [
+ "Round up 3D texture dimensions to nearest power of two before checking image allocation ",
+ "size limits to prevent mishandling of large 3D textures on older PowerVR drivers."
+ ],
+ "issue": "http://crbug.com/551673541"
+ },
+ {
"name": "adjust_src_dst_region_for_BlitFramebuffer",
"category": "Workarounds",
"description": [
diff --git a/src/libANGLE/Caps.h b/src/libANGLE/Caps.h
index bff8ed8..ab70456 100644
--- a/src/libANGLE/Caps.h
+++ b/src/libANGLE/Caps.h
@@ -165,6 +165,9 @@
// bytes-per-pixel. 1.25Gb is chosen as a conservative limit to allow for backends to expand
// textures formats up to 3x and still stay within 32-bit sizes.
size_t maxTextureBytes = 1280 * 1024 * 1024;
+
+ // Workaround for PowerVR Rogue drivers that round up 3D texture dimensions to POT internally.
+ bool roundUp3DTextureSizeToPOTForLimit = false;
};
struct TypePrecision
diff --git a/src/libANGLE/renderer/gl/renderergl_utils.cpp b/src/libANGLE/renderer/gl/renderergl_utils.cpp
index 060321a..ce91885 100644
--- a/src/libANGLE/renderer/gl/renderergl_utils.cpp
+++ b/src/libANGLE/renderer/gl/renderergl_utils.cpp
@@ -2170,6 +2170,11 @@
{
limitations->maxBufferBytes = 1 << 30;
}
+
+ if (features.roundUp3dTextureSizeToPOTForLimit.enabled)
+ {
+ limitations->roundUp3DTextureSizeToPOTForLimit = true;
+ }
}
bool GetSystemInfoVendorIDAndDeviceID(const FunctionsGL *functions,
@@ -2384,6 +2389,8 @@
ANGLE_FEATURE_CONDITION(features, disableMSAASampleCount1, isHuaweiMaleoon);
ANGLE_FEATURE_CONDITION(features, limitMax3dArrayTextureSizeTo1024,
isIntelLinuxLessThanKernelVersion5);
+ ANGLE_FEATURE_CONDITION(features, roundUp3dTextureSizeToPOTForLimit,
+ isPowerVRDriver && powerVRVersion < (std::array<int, 2>{26, 2}));
ANGLE_FEATURE_CONDITION(features, allowClearForRobustResourceInit, IsApple());
diff --git a/src/libANGLE/validationES.cpp b/src/libANGLE/validationES.cpp
index 2030b2b..d2a5a26 100644
--- a/src/libANGLE/validationES.cpp
+++ b/src/libANGLE/validationES.cpp
@@ -1282,6 +1282,20 @@
return true;
}
+Extents RoundImageAllocationExtentIfNeeded(const Context *context,
+ GLsizei width,
+ GLsizei height,
+ GLsizei depth)
+{
+ if (context->getLimitations().roundUp3DTextureSizeToPOTForLimit && depth > 1)
+ {
+ return Extents(gl::clampCast<GLsizei>(gl::ceilPow2(width)),
+ gl::clampCast<GLsizei>(gl::ceilPow2(height)),
+ gl::clampCast<GLsizei>(gl::ceilPow2(depth)));
+ }
+ return Extents(width, height, depth);
+}
+
bool ValidImageAllocationSize(const Context *context,
angle::EntryPoint entryPoint,
GLsizei width,
@@ -1292,7 +1306,8 @@
{
const InternalFormat &formatInfo = GetSizedInternalFormatInfo(sizedInternalFormat);
GLuint allocationSize = 0;
- if (!formatInfo.computeImageSize(Extents(width, height, depth), samples, &allocationSize) ||
+ Extents extents = RoundImageAllocationExtentIfNeeded(context, width, height, depth);
+ if (!formatInfo.computeImageSize(extents, samples, &allocationSize) ||
allocationSize > context->getLimitations().maxTextureBytes)
{
ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, kTextureSizeLimitation);
@@ -8302,8 +8317,9 @@
// Make sure computeImageSize sets expectedImageSize.
GLuint expectedImageSize = std::numeric_limits<GLuint>::max();
{
- const bool isSizeValid = internalFormatInfo.computeImageSize(Extents(width, height, depth),
- 0, &expectedImageSize);
+ Extents extents = RoundImageAllocationExtentIfNeeded(context, width, height, depth);
+ const bool isSizeValid =
+ internalFormatInfo.computeImageSize(extents, 0, &expectedImageSize);
if (ANGLE_UNLIKELY(!isSizeValid ||
expectedImageSize > context->getLimitations().maxTextureBytes))
{
diff --git a/src/tests/gl_tests/TextureTest.cpp b/src/tests/gl_tests/TextureTest.cpp
index 4bd16cd..181c9dc 100644
--- a/src/tests/gl_tests/TextureTest.cpp
+++ b/src/tests/gl_tests/TextureTest.cpp
@@ -24541,6 +24541,64 @@
ASSERT_GL_NO_ERROR();
}
+class Texture3DSizeLimitPOTRoundUpTest : public ANGLETest<>
+{
+ protected:
+ Texture3DSizeLimitPOTRoundUpTest() = default;
+};
+
+// Test that 3D texture dimensions are rounded up to power of two when checking size limits.
+TEST_P(Texture3DSizeLimitPOTRoundUpTest, RGB565)
+{
+ // 64x64x64 * sizeof(RGB565) is <= 1 MiB.
+ {
+ GLTexture tex;
+ glBindTexture(GL_TEXTURE_3D, tex);
+ glTexImage3D(GL_TEXTURE_3D, 0, GL_RGB565, 64, 64, 64, 0, GL_RGB, GL_UNSIGNED_SHORT_5_6_5,
+ nullptr);
+ EXPECT_GL_NO_ERROR();
+ }
+ {
+ GLTexture texStorage;
+ glBindTexture(GL_TEXTURE_3D, texStorage);
+ glTexStorage3D(GL_TEXTURE_3D, 1, GL_RGB565, 64, 64, 64);
+ EXPECT_GL_NO_ERROR();
+ }
+
+ // 65x65x65 * sizeof(RGB565) is still <= 1 MiB, but when rounded up becomes
+ // 128x128x128 * sizeof(RGB565) which is > 1MiB.
+ {
+ GLTexture tex;
+ glBindTexture(GL_TEXTURE_3D, tex);
+ glTexImage3D(GL_TEXTURE_3D, 0, GL_RGB565, 65, 65, 65, 0, GL_RGB, GL_UNSIGNED_SHORT_5_6_5,
+ nullptr);
+ EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+ }
+ {
+ GLTexture texStorage;
+ glBindTexture(GL_TEXTURE_3D, texStorage);
+ glTexStorage3D(GL_TEXTURE_3D, 1, GL_RGB565, 65, 65, 65);
+ EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+ }
+
+ // The workaround does not apply to 2D textures.
+ // 724x724x1 * sizeof(RGB565) is <= 1MiB, but rounded up would be
+ // 1024x1024x1 * sizeof(RGB565) which is > 1MiB.
+ {
+ GLTexture tex2D;
+ glBindTexture(GL_TEXTURE_2D, tex2D);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB565, 724, 724, 0, GL_RGB, GL_UNSIGNED_SHORT_5_6_5,
+ nullptr);
+ EXPECT_GL_NO_ERROR();
+ }
+ {
+ GLTexture tex2DStorage;
+ glBindTexture(GL_TEXTURE_2D, tex2DStorage);
+ glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGB565, 724, 724);
+ EXPECT_GL_NO_ERROR();
+ }
+}
+
class Texture2DTestES3_NonZeroBaseLevelGenMipmaps : public Texture2DTestES3
{};
@@ -25477,8 +25535,21 @@
ES2_VULKAN().enable(Feature::LimitMaxTextureBytesTo1MB),
ES3_VULKAN().enable(Feature::LimitMaxTextureBytesTo1MB),
ES2_OPENGL().enable(Feature::LimitMaxTextureBytesTo1MB),
- ES3_OPENGL().enable(Feature::LimitMaxTextureBytesTo1MB),
+ ES3_OPENGL()
+ .enable(Feature::LimitMaxTextureBytesTo1MB)
+ .disable(Feature::RoundUp3dTextureSizeToPOTForLimit),
ES2_OPENGLES().enable(Feature::LimitMaxTextureBytesTo1MB),
- ES3_OPENGLES().enable(Feature::LimitMaxTextureBytesTo1MB));
+ ES3_OPENGLES()
+ .enable(Feature::LimitMaxTextureBytesTo1MB)
+ .disable(Feature::RoundUp3dTextureSizeToPOTForLimit));
+
+GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(Texture3DSizeLimitPOTRoundUpTest);
+ANGLE_INSTANTIATE_TEST(Texture3DSizeLimitPOTRoundUpTest,
+ ES3_OPENGL()
+ .enable(Feature::LimitMaxTextureBytesTo1MB)
+ .enable(Feature::RoundUp3dTextureSizeToPOTForLimit),
+ ES3_OPENGLES()
+ .enable(Feature::LimitMaxTextureBytesTo1MB)
+ .enable(Feature::RoundUp3dTextureSizeToPOTForLimit));
} // anonymous namespace
diff --git a/include/platform/autogen/FeaturesGL_autogen.h b/include/platform/autogen/FeaturesGL_autogen.h
index 59e2223..7ce8359 100644
--- a/include/platform/autogen/FeaturesGL_autogen.h
+++ b/include/platform/autogen/FeaturesGL_autogen.h
@@ -290,6 +290,12 @@
&members,
};
+ FeatureInfo roundUp3dTextureSizeToPOTForLimit = {
+ "roundUp3dTextureSizeToPOTForLimit",
+ FeatureCategory::OpenGLWorkarounds,
+ &members,
+ };
+
FeatureInfo adjustSrcDstRegionForBlitFramebuffer = {
"adjustSrcDstRegionForBlitFramebuffer",
FeatureCategory::OpenGLWorkarounds,
diff --git a/util/autogen/angle_features_autogen.cpp b/util/autogen/angle_features_autogen.cpp
index ccdceb1..93423e5 100644
--- a/util/autogen/angle_features_autogen.cpp
+++ b/util/autogen/angle_features_autogen.cpp
@@ -328,6 +328,7 @@
{Feature::RGBA4IsNotSupportedForColorRendering, "RGBA4IsNotSupportedForColorRendering"},
{Feature::RGBDXT1TexturesSampleZeroAlpha, "RGBDXT1TexturesSampleZeroAlpha"},
{Feature::RoundOutputAfterDithering, "roundOutputAfterDithering"},
+ {Feature::RoundUp3dTextureSizeToPOTForLimit, "roundUp3dTextureSizeToPOTForLimit"},
{Feature::SanitizeAMDGPURendererString, "sanitizeAMDGPURendererString"},
{Feature::ScalarizeVecAndMatConstructorArgs, "scalarizeVecAndMatConstructorArgs"},
{Feature::SelectViewInGeometryShader, "selectViewInGeometryShader"},
diff --git a/util/autogen/angle_features_autogen.h b/util/autogen/angle_features_autogen.h
index 2ef049b..1eddacf 100644
--- a/util/autogen/angle_features_autogen.h
+++ b/util/autogen/angle_features_autogen.h
@@ -328,6 +328,7 @@
RGBA4IsNotSupportedForColorRendering,
RGBDXT1TexturesSampleZeroAlpha,
RoundOutputAfterDithering,
+ RoundUp3dTextureSizeToPOTForLimit,
SanitizeAMDGPURendererString,
ScalarizeVecAndMatConstructorArgs,
SelectViewInGeometryShader,
Regression Test / PoC
diff --git a/src/tests/gl_tests/TextureTest.cpp b/src/tests/gl_tests/TextureTest.cpp
index 4bd16cd..181c9dc 100644
--- a/src/tests/gl_tests/TextureTest.cpp
+++ b/src/tests/gl_tests/TextureTest.cpp
@@ -24541,6 +24541,64 @@
ASSERT_GL_NO_ERROR();
}
+class Texture3DSizeLimitPOTRoundUpTest : public ANGLETest<>
+{
+ protected:
+ Texture3DSizeLimitPOTRoundUpTest() = default;
+};
+
+// Test that 3D texture dimensions are rounded up to power of two when checking size limits.
+TEST_P(Texture3DSizeLimitPOTRoundUpTest, RGB565)
+{
+ // 64x64x64 * sizeof(RGB565) is <= 1 MiB.
+ {
+ GLTexture tex;
+ glBindTexture(GL_TEXTURE_3D, tex);
+ glTexImage3D(GL_TEXTURE_3D, 0, GL_RGB565, 64, 64, 64, 0, GL_RGB, GL_UNSIGNED_SHORT_5_6_5,
+ nullptr);
+ EXPECT_GL_NO_ERROR();
+ }
+ {
+ GLTexture texStorage;
+ glBindTexture(GL_TEXTURE_3D, texStorage);
+ glTexStorage3D(GL_TEXTURE_3D, 1, GL_RGB565, 64, 64, 64);
+ EXPECT_GL_NO_ERROR();
+ }
+
+ // 65x65x65 * sizeof(RGB565) is still <= 1 MiB, but when rounded up becomes
+ // 128x128x128 * sizeof(RGB565) which is > 1MiB.
+ {
+ GLTexture tex;
+ glBindTexture(GL_TEXTURE_3D, tex);
+ glTexImage3D(GL_TEXTURE_3D, 0, GL_RGB565, 65, 65, 65, 0, GL_RGB, GL_UNSIGNED_SHORT_5_6_5,
+ nullptr);
+ EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+ }
+ {
+ GLTexture texStorage;
+ glBindTexture(GL_TEXTURE_3D, texStorage);
+ glTexStorage3D(GL_TEXTURE_3D, 1, GL_RGB565, 65, 65, 65);
+ EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+ }
+
+ // The workaround does not apply to 2D textures.
+ // 724x724x1 * sizeof(RGB565) is <= 1MiB, but rounded up would be
+ // 1024x1024x1 * sizeof(RGB565) which is > 1MiB.
+ {
+ GLTexture tex2D;
+ glBindTexture(GL_TEXTURE_2D, tex2D);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB565, 724, 724, 0, GL_RGB, GL_UNSIGNED_SHORT_5_6_5,
+ nullptr);
+ EXPECT_GL_NO_ERROR();
+ }
+ {
+ GLTexture tex2DStorage;
+ glBindTexture(GL_TEXTURE_2D, tex2DStorage);
+ glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGB565, 724, 724);
+ EXPECT_GL_NO_ERROR();
+ }
+}
+
class Texture2DTestES3_NonZeroBaseLevelGenMipmaps : public Texture2DTestES3
{};
@@ -25477,8 +25535,21 @@
ES2_VULKAN().enable(Feature::LimitMaxTextureBytesTo1MB),
ES3_VULKAN().enable(Feature::LimitMaxTextureBytesTo1MB),
ES2_OPENGL().enable(Feature::LimitMaxTextureBytesTo1MB),
- ES3_OPENGL().enable(Feature::LimitMaxTextureBytesTo1MB),
+ ES3_OPENGL()
+ .enable(Feature::LimitMaxTextureBytesTo1MB)
+ .disable(Feature::RoundUp3dTextureSizeToPOTForLimit),
ES2_OPENGLES().enable(Feature::LimitMaxTextureBytesTo1MB),
- ES3_OPENGLES().enable(Feature::LimitMaxTextureBytesTo1MB));
+ ES3_OPENGLES()
+ .enable(Feature::LimitMaxTextureBytesTo1MB)
+ .disable(Feature::RoundUp3dTextureSizeToPOTForLimit));
+
+GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(Texture3DSizeLimitPOTRoundUpTest);
+ANGLE_INSTANTIATE_TEST(Texture3DSizeLimitPOTRoundUpTest,
+ ES3_OPENGL()
+ .enable(Feature::LimitMaxTextureBytesTo1MB)
+ .enable(Feature::RoundUp3dTextureSizeToPOTForLimit),
+ ES3_OPENGLES()
+ .enable(Feature::LimitMaxTextureBytesTo1MB)
+ .enable(Feature::RoundUp3dTextureSizeToPOTForLimit));
} // anonymous namespace