Chrome · WebGL
CVE-2026-17726
Integer Overflow in WebGL
Overview
High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
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
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"},
Loading diff…
Regression Test / PoC
shipped with the fix
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:
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page