CVE-2026-17726
Overview
Background
- Pixel pack buffer
- a GL buffer object bound to
GL_PIXEL_PACK_BUFFERthat receives pixel data written byglReadPixels; readback DMAs framebuffer contents into it. - Row pitch / `rowBytes`
- the byte stride between consecutive pixel rows in the pack destination, computed by
glFormat.computeRowPitch(...)inFramebufferGL::readPixels. - Stride in bits
- Mali’s GLES driver derives the packing stride as
row_length * bytes_per_pixel * 8, computed in a signed 32-bit int;0x10000000(256 MiB) is the pitch at which this multiply overflows. - `readPixelsRowByRow`
- the fallback path that issues one packed read per row, avoiding a single call with a huge stride.
Root Cause Analysis
In FramebufferGL::readPixels, ANGLE forwards the caller-controlled row pitch to the Mali driver, which computes the internal pack stride as row_length * bytes_per_pixel * 8 using a 32-bit signed int. Once rowBytes reaches 0x10000000 (256 MiB), the multiply by 8 (to convert bytes to bits) overflows the 32-bit range and wraps to a small or negative value. The driver then uses this wrong, too-small stride to advance between rows while writing into the pixel pack buffer, so row offsets no longer correspond to the buffer the application sized for the true pitch. Because ANGLE validated the destination buffer against the correct large stride but the driver writes using the wrapped small stride, the resulting write/read lands out of bounds of the pack buffer.
The fix routes readback through readPixelsRowByRow whenever packBuffer && rowBytes >= 0x10000000u under the new packLargeRowLengthSeparatelyPackBuffer feature, so no single call ever passes the overflow-triggering stride to the driver.
Attack Path
- Allocate oversized pack buffer On a Mali device, script WebGL to create a pixel pack buffer whose true row pitch is at least 256 MiB (large width and/or wide format).
- Trigger readback
Issue a
glReadPixelsthat reachesFramebufferGL::readPixelswithpackBufferbound androwBytes >= 0x10000000u. - Driver stride wrap
Mali computes
row_length * bytes_per_pixel * 8in int32, overflowing to a small stride that mismatches the buffer ANGLE sized for the real pitch. - Out-of-bounds access The driver writes packed rows using the wrapped stride, reading/writing past the pack buffer’s bounds, corrupting adjacent heap memory.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ReadPixelsPBODrawTestsrc/tests/gl_tests/ReadPixelsTest.cpp |
modified |
Files Changed
include/platform/autogen/FeaturesGL_autogen.hinclude/platform/gl_features.jsonsrc/libANGLE/renderer/gl/FramebufferGL.cppsrc/libANGLE/renderer/gl/renderergl_utils.cppsrc/tests/angle_end2end_tests_expectations.txtsrc/tests/gl_tests/ReadPixelsTest.cpputil/autogen/angle_features_autogen.cpp
Audit Directions
- Stride/pitch conversions to bitsAudit any driver or backend path that multiplies a byte pitch by 8 (or by bpp) in a fixed 32-bit int; large-but-legal dimensions can overflow before the destination is written.
- Trusted-large-value handoffsWhere ANGLE validates a buffer against a correct large value but passes it to a driver that recomputes with narrower arithmetic, look for threshold-based workarounds that must cap or split the operation.
- Other pack/unpack transfersCheck
glReadPixels, texture uploads, and other DMA paths for the same 256 MiB /0x10000000boundary under Mali and similar drivers.
Patch
From 1ca561b41b1c5172b081937d95add84155073bcd Mon Sep 17 00:00:00 2001 From: Ken Russell <[email protected]> Date: Wed, 08 Jul 2026 16:58:51 -0700 Subject: [PATCH] GL: workaround: pack large row lengths separately. Apply this workaround to Mali GPUs. Add a stress test verifying the workaround, which is skipped on Metal, Vulkan, and Intel OpenGL for the time being. Co-authored with Gemini. Fixed: chromium:529867799 Change-Id: I32e0fce82e44335598a4fc5016c9311fa45f4c76 Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8067312 Reviewed-by: Shahbaz Youssefi <[email protected]> Reviewed-by: Amirali Abdolrashidi <[email protected]> Commit-Queue: Kenneth Russell <[email protected]> --- diff --git a/include/platform/autogen/FeaturesGL_autogen.h b/include/platform/autogen/FeaturesGL_autogen.h index a0f1e29..6b43b42 100644 --- a/include/platform/autogen/FeaturesGL_autogen.h +++ b/include/platform/autogen/FeaturesGL_autogen.h @@ -86,6 +86,12 @@ &members, }; + FeatureInfo packLargeRowLengthSeparatelyPackBuffer = { + "packLargeRowLengthSeparatelyPackBuffer", + FeatureCategory::OpenGLWorkarounds, + &members, + }; + FeatureInfo initializeCurrentVertexAttributes = { "initializeCurrentVertexAttributes", FeatureCategory::OpenGLWorkarounds, diff --git a/include/platform/gl_features.json b/include/platform/gl_features.json index 73fdf1d..79e0837 100644 --- a/include/platform/gl_features.json +++ b/include/platform/gl_features.json @@ -86,6 +86,15 @@ ] }, { + "name": "pack_large_row_length_separately_pack_buffer", + "category": "Workarounds", + "description": [ + "When packing to a pixel pack buffer with a row pitch >= 256MiB, pack row by row.", + "Mali GLES computes stride in bits as int32; row_length*bpp*8 wraps at 0x10000000." + ], + "issue": "http://crbug.com/529867799" + }, + { "name": "initialize_current_vertex_attributes", "category": "Workarounds", "description": [ diff --git a/src/libANGLE/renderer/gl/FramebufferGL.cpp b/src/libANGLE/renderer/gl/FramebufferGL.cpp index 609e86b..abbf39c 100644 --- a/src/libANGLE/renderer/gl/FramebufferGL.cpp +++ b/src/libANGLE/renderer/gl/FramebufferGL.cpp @@ -792,22 +792,26 @@ stateManager->getHasSeparateFramebufferBindings() ? GL_READ_FRAMEBUFFER : GL_FRAMEBUFFER; stateManager->bindFramebuffer(framebufferTarget, mFramebufferID); + const gl::InternalFormat &glFormat = gl::GetInternalFormatInfo(readFormat, readType); + GLuint rowBytes = 0; + ANGLE_CHECK_GL_MATH(contextGL, + glFormat.computeRowPitch(readType, area.width, packState.alignment, + packState.rowLength, &rowBytes)); + bool useOverlappingRowsWorkaround = features.packOverlappingRowsSeparatelyPackBuffer.enabled && packBuffer && packState.rowLength != 0 && packState.rowLength < clippedArea.width; + bool useLargeRowLengthWorkaround = + features.packLargeRowLengthSeparatelyPackBuffer.enabled && packBuffer && + rowBytes >= 0x10000000u; // Mali int32 stride-in-bits wrap threshold + GLubyte *outPtr = static_cast<GLubyte *>(pixels); int leftClip = clippedArea.x - area.x; int topClip = clippedArea.y - area.y; if (leftClip || topClip) { // Adjust destination to match portion clipped off left and/or top. - const gl::InternalFormat &glFormat = gl::GetInternalFormatInfo(readFormat, readType); - - GLuint rowBytes = 0; - ANGLE_CHECK_GL_MATH(contextGL, - glFormat.computeRowPitch(readType, area.width, packState.alignment, - packState.rowLength, &rowBytes)); ANGLE_UNSAFE_TODO(outPtr += leftClip * glFormat.pixelBytes + topClip * rowBytes); } @@ -825,7 +829,8 @@ bool usePackSkipWorkaround = features.emulatePackSkipRowsAndPackSkipPixels.enabled && (packState.skipRows != 0 || packState.skipPixels != 0); - if (cannotSetDesiredRowLength || useOverlappingRowsWorkaround || usePackSkipWorkaround) + if (cannotSetDesiredRowLength || useOverlappingRowsWorkaround || useLargeRowLengthWorkaround || + usePackSkipWorkaround) { return readPixelsRowByRow(context, clippedArea, format, readFormat, readType, packState, outPtr); diff --git a/src/libANGLE/renderer/gl/renderergl_utils.cpp b/src/libANGLE/renderer/gl/renderergl_utils.cpp index 9d84d2e..92597d3 100644 --- a/src/libANGLE/renderer/gl/renderergl_utils.cpp +++ b/src/libANGLE/renderer/gl/renderergl_utils.cpp @@ -2403,6 +2403,13 @@ ANGLE_FEATURE_CONDITION(features, unpackOverlappingRowsSeparatelyUnpackBuffer, isNvidia); ANGLE_FEATURE_CONDITION(features, packOverlappingRowsSeparatelyPackBuffer, isNvidia); + // Mali GLES computes readPixels row_stride as (int32_t)(row_length*bpp)*8; + // wraps negative when the byte pitch >= 0x10000000 -> OOB write into the + // PBO's cmem mapping. Route through readPixelsRowByRow so Mali only ever + // sees PACK_ROW_LENGTH=0. Also apply to Imagination GPUs which crash on + // the new test. crbug.com/529867799 + ANGLE_FEATURE_CONDITION(features, packLargeRowLengthSeparatelyPackBuffer, + isMali || IsPowerVR(vendor)); std::array<int, 2> powerVRVersion = {0, 0}; bool isPowerVRDriver = diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt index 41fc454..9b93783 100644 --- a/src/tests/angle_end2end_tests_expectations.txt +++ b/src/tests/angle_end2end_tests_expectations.txt @@ -2783,6 +2783,10 @@ // Unimplemented parts. 515493666 D3D11 : Restart/DrawElementsVariantsTest.Draw/* = SKIP +529867799 METAL : ReadPixelsPBOTest.PackLargeRowLength/* = SKIP +529867799 VULKAN : ReadPixelsPBOTest.PackLargeRowLength/* = SKIP +529867799 INTEL OPENGL : ReadPixelsPBOTest.PackLargeRowLength/* = SKIP + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // Slow tests, should appear last in this file // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! diff --git a/src/tests/gl_tests/ReadPixelsTest.cpp b/src/tests/gl_tests/ReadPixelsTest.cpp index 217abca..16be449 100644 --- a/src/tests/gl_tests/ReadPixelsTest.cpp +++ b/src/tests/gl_tests/ReadPixelsTest.cpp @@ -775,6 +775,52 @@ ASSERT_GL_NO_ERROR(); } +// Test that readPixels with a large PACK_ROW_LENGTH into a PBO does not overflow int32 stride +// calculation. Ported from crbug.com/528175330 / crbug.com/529867799. +TEST_P(ReadPixelsPBOTest, PackLargeRowLength) +{ + reset(16, 8, 8); + + const GLColor kExpectedColor(65, 128, 192, 255); + constexpr GLint kLargeRowLength = 0x7fffffc; + constexpr GLsizeiptr kByteOffsetToVerify = 0x1ffffff0; + constexpr GLsizeiptr kBufferSize = kByteOffsetToVerify + 256; + + glBindBuffer(GL_PIXEL_PACK_BUFFER, mPBO); + glBufferData(GL_PIXEL_PACK_BUFFER, kBufferSize, nullptr, GL_STREAM_READ); + ANGLE_SKIP_TEST_IF(glGetError() == GL_OUT_OF_MEMORY); + ASSERT_GL_NO_ERROR(); + + glClearColor(kExpectedColor.R / 255.0f, kExpectedColor.G / 255.0f, kExpectedColor.B / 255.0f, + kExpectedColor.A / 255.0f); + glClear(GL_COLOR_BUFFER_BIT); + + glPixelStorei(GL_PACK_ROW_LENGTH, kLargeRowLength); + glPixelStorei(GL_PACK_ALIGNMENT, 4); + glReadPixels(0, 0, 1, 2, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + EXPECT_GL_NO_ERROR(); + + void *mappedPtr = glMapBufferRange(GL_PIXEL_PACK_BUFFER, 0, kBufferSize, GL_MAP_READ_BIT); + ASSERT_NE(nullptr, mappedPtr); + + const GLColor *colorPtr = static_cast<const GLColor *>(mappedPtr); + GLColor actualColorRow0; + GLColor actualColorRow1; + + // Check row 0 pixel (at byte offset 0) + actualColorRow0 = colorPtr[0]; + + // Check row 1 pixel (at byte offset kByteOffsetToVerify) + constexpr size_t kRow1OffsetInPixels = kByteOffsetToVerify / sizeof(GLColor); + // SAFETY: test-only code. + ANGLE_UNSAFE_BUFFERS(actualColorRow1 = colorPtr[kRow1OffsetInPixels]); + + glUnmapBuffer(GL_PIXEL_PACK_BUFFER); + + EXPECT_EQ(kExpectedColor, actualColorRow0); + EXPECT_EQ(kExpectedColor, actualColorRow1); +} + class ReadPixelsPBODrawTest : public ReadPixelsPBOTest { protected: diff --git a/util/autogen/angle_features_autogen.cpp b/util/autogen/angle_features_autogen.cpp index cfcf801..0869671 100644 --- a/util/autogen/angle_features_autogen.cpp +++ b/util/autogen/angle_features_autogen.cpp @@ -261,6 +261,7 @@ {Feature::MutableMipmapTextureUpload, "mutableMipmapTextureUpload"}, {Feature::NoperspectiveInterpolationBrokenWithPassthroughShaders, "noperspectiveInterpolationBrokenWithPassthroughShaders"},
Regression Test / PoC
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index 41fc454..9b93783 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -2783,6 +2783,10 @@
// Unimplemented parts.
515493666 D3D11 : Restart/DrawElementsVariantsTest.Draw/* = SKIP
+529867799 METAL : ReadPixelsPBOTest.PackLargeRowLength/* = SKIP
+529867799 VULKAN : ReadPixelsPBOTest.PackLargeRowLength/* = SKIP
+529867799 INTEL OPENGL : ReadPixelsPBOTest.PackLargeRowLength/* = SKIP
+
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// Slow tests, should appear last in this file
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
diff --git a/src/tests/gl_tests/ReadPixelsTest.cpp b/src/tests/gl_tests/ReadPixelsTest.cpp
index 217abca..16be449 100644
--- a/src/tests/gl_tests/ReadPixelsTest.cpp
+++ b/src/tests/gl_tests/ReadPixelsTest.cpp
@@ -775,6 +775,52 @@
ASSERT_GL_NO_ERROR();
}
+// Test that readPixels with a large PACK_ROW_LENGTH into a PBO does not overflow int32 stride
+// calculation. Ported from crbug.com/528175330 / crbug.com/529867799.
+TEST_P(ReadPixelsPBOTest, PackLargeRowLength)
+{
+ reset(16, 8, 8);
+
+ const GLColor kExpectedColor(65, 128, 192, 255);
+ constexpr GLint kLargeRowLength = 0x7fffffc;
+ constexpr GLsizeiptr kByteOffsetToVerify = 0x1ffffff0;
+ constexpr GLsizeiptr kBufferSize = kByteOffsetToVerify + 256;
+
+ glBindBuffer(GL_PIXEL_PACK_BUFFER, mPBO);
+ glBufferData(GL_PIXEL_PACK_BUFFER, kBufferSize, nullptr, GL_STREAM_READ);
+ ANGLE_SKIP_TEST_IF(glGetError() == GL_OUT_OF_MEMORY);
+ ASSERT_GL_NO_ERROR();
+
+ glClearColor(kExpectedColor.R / 255.0f, kExpectedColor.G / 255.0f, kExpectedColor.B / 255.0f,
+ kExpectedColor.A / 255.0f);
+ glClear(GL_COLOR_BUFFER_BIT);
+
+ glPixelStorei(GL_PACK_ROW_LENGTH, kLargeRowLength);
+ glPixelStorei(GL_PACK_ALIGNMENT, 4);
+ glReadPixels(0, 0, 1, 2, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+ EXPECT_GL_NO_ERROR();
+
+ void *mappedPtr = glMapBufferRange(GL_PIXEL_PACK_BUFFER, 0, kBufferSize, GL_MAP_READ_BIT);
+ ASSERT_NE(nullptr, mappedPtr);
+
+ const GLColor *colorPtr = static_cast<const GLColor *>(mappedPtr);
+ GLColor actualColorRow0;
+ GLColor actualColorRow1;
+
+ // Check row 0 pixel (at byte offset 0)
+ actualColorRow0 = colorPtr[0];
+
+ // Check row 1 pixel (at byte offset kByteOffsetToVerify)
+ constexpr size_t kRow1OffsetInPixels = kByteOffsetToVerify / sizeof(GLColor);
+ // SAFETY: test-only code.
+ ANGLE_UNSAFE_BUFFERS(actualColorRow1 = colorPtr[kRow1OffsetInPixels]);
+
+ glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
+
+ EXPECT_EQ(kExpectedColor, actualColorRow0);
+ EXPECT_EQ(kExpectedColor, actualColorRow1);
+}
+
class ReadPixelsPBODrawTest : public ReadPixelsPBOTest
{
protected: