CVE-2026-6296
Overview
Files Changed
src/libANGLE/renderer/gl/BlitGL.cppsrc/tests/capture_replay_tests/capture_replay_expectations.txtsrc/tests/gl_tests/CopyTextureTest.cpp
Patch
From b149a5c62d76ab536929afc5bba2b2774da46102 Mon Sep 17 00:00:00 2001
From: Geoff Lang <geofflang@chromium.org>
Date: Fri, 27 Mar 2026 16:13:31 -0400
Subject: [PATCH] GL: Fix pack state for BlitGL::copySubTextureCPUReadback
copySubTextureCPUReadback does both ReadPixels and TexImage calls and
needs to make sure the client's pack states are not used. It does this
but in the wrong order causing an invalid pack state to be used for the
ReadPixels call.
Bug: chromium:490170083
Change-Id: I93dcabf52edd6e4e08f999aaa0d96d1fc325211a
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7708753
Reviewed-by: Shahbaz Youssefi <syoussefi@chromium.org>
Commit-Queue: Geoff Lang <geofflang@chromium.org>
---
diff --git a/src/libANGLE/renderer/gl/BlitGL.cpp b/src/libANGLE/renderer/gl/BlitGL.cpp
index b156d2d..154be25 100644
--- a/src/libANGLE/renderer/gl/BlitGL.cpp
+++ b/src/libANGLE/renderer/gl/BlitGL.cpp
@@ -859,10 +859,10 @@
readFunction = angle::ReadColor<angle::R8G8B8A8, GLfloat>;
}
- gl::PixelUnpackState unpack;
- unpack.alignment = 1;
- ANGLE_TRY(mStateManager->setPixelUnpackState(context, unpack));
- ANGLE_TRY(mStateManager->setPixelUnpackBuffer(context, nullptr));
+ gl::PixelPackState pack;
+ pack.alignment = 1;
+ ANGLE_TRY(mStateManager->setPixelPackState(context, pack));
+ ANGLE_TRY(mStateManager->setPixelPackBuffer(context, nullptr));
ANGLE_GL_TRY(context, mFunctions->readPixels(readPixelsArea.x, readPixelsArea.y,
readPixelsArea.width, readPixelsArea.height,
readPixelsFormat, GL_UNSIGNED_BYTE, sourceMemory));
@@ -877,10 +877,10 @@
destInternalFormatInfo.format, destInternalFormatInfo.componentType, readPixelsArea.width,
readPixelsArea.height, 1, unpackFlipY, unpackPremultiplyAlpha, unpackUnmultiplyAlpha);
- gl::PixelPackState pack;
- pack.alignment = 1;
- ANGLE_TRY(mStateManager->setPixelPackState(context, pack));
- ANGLE_TRY(mStateManager->setPixelPackBuffer(context, nullptr));
+ gl::PixelUnpackState unpack;
+ unpack.alignment = 1;
+ ANGLE_TRY(mStateManager->setPixelUnpackState(context, unpack));
+ ANGLE_TRY(mStateManager->setPixelUnpackBuffer(context, nullptr));
nativegl::TexSubImageFormat texSubImageFormat =
nativegl::GetTexSubImageFormat(mFunctions, mFeatures, destFormat, destType);
diff --git a/src/tests/capture_replay_tests/capture_replay_expectations.txt b/src/tests/capture_replay_tests/capture_replay_expectations.txt
index 74e572d..201c64a 100644
--- a/src/tests/capture_replay_tests/capture_replay_expectations.txt
+++ b/src/tests/capture_replay_tests/capture_replay_expectations.txt
@@ -80,6 +80,7 @@
42264831 : FramebufferTest_ES3.AttachmentsWithUnequalDimensions/* = SKIP_FOR_CAPTURE
42264831 : FramebufferTest_ES3.ChangeAttachmentThenInvalidateAndDraw/* = SKIP_FOR_CAPTURE
42264831 : FramebufferTest_ES3.RenderAndInvalidateImmutableTextureWithBeyondMaxLevel/* = SKIP_FOR_CAPTURE
+490170083 : CopyTextureTestES3.SRGBWithPackParameters/* = SKIP_FOR_CAPTURE
# The following tests fail with forceRobustResourceInit
# They were accidentally passing until http://crrev/c/5588816
diff --git a/src/tests/gl_tests/CopyTextureTest.cpp b/src/tests/gl_tests/CopyTextureTest.cpp
index cd9bc97..44effc4 100644
--- a/src/tests/gl_tests/CopyTextureTest.cpp
+++ b/src/tests/gl_tests/CopyTextureTest.cpp
@@ -1988,6 +1988,50 @@
EXPECT_PIXEL_COLOR_EQ(0, 0, expectedPixels);
}
+// Regression test for TextureGL doing CPU readback when a PBO is bound
+TEST_P(CopyTextureTestES3, SRGBWithPackParameters)
+{
+ ANGLE_SKIP_TEST_IF(!checkExtensions());
+ ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_EXT_sRGB"));
+
+ GLColor originalPixels(50u, 100u, 150u, 155u);
+
+ glBindTexture(GL_TEXTURE_2D, mTextures[1]);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, &originalPixels);
+ EXPECT_GL_NO_ERROR();
+
+ glBindTexture(GL_TEXTURE_2D, mTextures[0]);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB_ALPHA_EXT, 1, 1, 0, GL_SRGB_ALPHA_EXT, GL_UNSIGNED_BYTE,
+ nullptr);
+ EXPECT_GL_NO_ERROR();
+
+ GLFramebuffer dstFBO;
+ glBindFramebuffer(GL_DRAW_FRAMEBUFFER, dstFBO);
+ glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mTextures[0],
+ 0);
+
+ // Should have no effect on the copy
+ glPixelStorei(GL_PACK_SKIP_PIXELS, 100);
+ glPixelStorei(GL_PACK_SKIP_ROWS, 100);
+ glPixelStorei(GL_PACK_ROW_LENGTH, 100);
+ glPixelStorei(GL_PACK_ALIGNMENT, 8);
+ EXPECT_GL_NO_ERROR();
+
+ std::array<uint8_t, 100 * 100 * 4 * 2> bigPackBuffer = {0};
+ glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, bigPackBuffer.data());
+
+ glCopySubTextureCHROMIUM(mTextures[1], 0, GL_TEXTURE_2D, mTextures[0], 0, 0, 0, 0, 0, 1, 1,
+ false, false, false);
+ EXPECT_GL_NO_ERROR();
+
+ glPixelStorei(GL_PACK_SKIP_PIXELS, 0);
+ glPixelStorei(GL_PACK_SKIP_ROWS, 0);
+ glPixelStorei(GL_PACK_ROW_LENGTH, 0);
+ glPixelStorei(GL_PACK_ALIGNMENT, 1);
+
+ EXPECT_PIXEL_COLOR_EQ(0, 0, originalPixels);
+}
+
// Bug where TEXTURE_SWIZZLE_RGBA was not reset after the Luminance workaround. (crbug.com/1022080)
TEST_P(CopyTextureTestES3, LuminanceWorkaroundTextureSwizzleBug)
{
Regression Test / PoC
diff --git a/src/tests/capture_replay_tests/capture_replay_expectations.txt b/src/tests/capture_replay_tests/capture_replay_expectations.txt
index 74e572d..201c64a 100644
--- a/src/tests/capture_replay_tests/capture_replay_expectations.txt
+++ b/src/tests/capture_replay_tests/capture_replay_expectations.txt
@@ -80,6 +80,7 @@
42264831 : FramebufferTest_ES3.AttachmentsWithUnequalDimensions/* = SKIP_FOR_CAPTURE
42264831 : FramebufferTest_ES3.ChangeAttachmentThenInvalidateAndDraw/* = SKIP_FOR_CAPTURE
42264831 : FramebufferTest_ES3.RenderAndInvalidateImmutableTextureWithBeyondMaxLevel/* = SKIP_FOR_CAPTURE
+490170083 : CopyTextureTestES3.SRGBWithPackParameters/* = SKIP_FOR_CAPTURE
# The following tests fail with forceRobustResourceInit
# They were accidentally passing until http://crrev/c/5588816
diff --git a/src/tests/gl_tests/CopyTextureTest.cpp b/src/tests/gl_tests/CopyTextureTest.cpp
index cd9bc97..44effc4 100644
--- a/src/tests/gl_tests/CopyTextureTest.cpp
+++ b/src/tests/gl_tests/CopyTextureTest.cpp
@@ -1988,6 +1988,50 @@
EXPECT_PIXEL_COLOR_EQ(0, 0, expectedPixels);
}
+// Regression test for TextureGL doing CPU readback when a PBO is bound
+TEST_P(CopyTextureTestES3, SRGBWithPackParameters)
+{
+ ANGLE_SKIP_TEST_IF(!checkExtensions());
+ ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_EXT_sRGB"));
+
+ GLColor originalPixels(50u, 100u, 150u, 155u);
+
+ glBindTexture(GL_TEXTURE_2D, mTextures[1]);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, &originalPixels);
+ EXPECT_GL_NO_ERROR();
+
+ glBindTexture(GL_TEXTURE_2D, mTextures[0]);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB_ALPHA_EXT, 1, 1, 0, GL_SRGB_ALPHA_EXT, GL_UNSIGNED_BYTE,
+ nullptr);
+ EXPECT_GL_NO_ERROR();
+
+ GLFramebuffer dstFBO;
+ glBindFramebuffer(GL_DRAW_FRAMEBUFFER, dstFBO);
+ glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mTextures[0],
+ 0);
+
+ // Should have no effect on the copy
+ glPixelStorei(GL_PACK_SKIP_PIXELS, 100);
+ glPixelStorei(GL_PACK_SKIP_ROWS, 100);
+ glPixelStorei(GL_PACK_ROW_LENGTH, 100);
+ glPixelStorei(GL_PACK_ALIGNMENT, 8);
+ EXPECT_GL_NO_ERROR();
+
+ std::array<uint8_t, 100 * 100 * 4 * 2> bigPackBuffer = {0};
+ glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, bigPackBuffer.data());
+
+ glCopySubTextureCHROMIUM(mTextures[1], 0, GL_TEXTURE_2D, mTextures[0], 0, 0, 0, 0, 0, 1, 1,
+ false, false, false);
+ EXPECT_GL_NO_ERROR();
+
+ glPixelStorei(GL_PACK_SKIP_PIXELS, 0);
+ glPixelStorei(GL_PACK_SKIP_ROWS, 0);
+ glPixelStorei(GL_PACK_ROW_LENGTH, 0);
+ glPixelStorei(GL_PACK_ALIGNMENT, 1);
+
+ EXPECT_PIXEL_COLOR_EQ(0, 0, originalPixels);
+}
+
// Bug where TEXTURE_SWIZZLE_RGBA was not reset after the Luminance workaround. (crbug.com/1022080)
TEST_P(CopyTextureTestES3, LuminanceWorkaroundTextureSwizzleBug)
{
Original Bug Report
ANGLE BlitGL copySubTextureCPUReadback Controlled Heap Overflow via Unsynchronized PACK_ROW_LENGTH
Report description
ANGLE BlitGL copySubTextureCPUReadback Controlled Heap Overflow via Unsynchronized PACK_ROW_LENGTH
Bug location
Where do you want to report your vulnerability?
Chrome VRP – Report security issues affecting the Chrome browser. See program rules
Which URL (or repository) have you found the vulnerability in?
The problem
Please describe the technical details of the vulnerability
kTexImageDirtyBits in ANGLE’s Context.cpp omits DIRTY_BIT_PACK_STATE and DIRTY_BIT_PACK_BUFFER_BINDING. When copySubTextureCPUReadback (BlitGL.cpp) calls glReadPixels on a scratch heap buffer, it inherits stale GL_PACK_ROW_LENGTH from native GL, causing a controlled heap overflow. Triggered from JavaScript via WebGL2, no compromised renderer required.
Data written is attacker-controlled via source WebGL canvas pixel data (RGBA bytes rendered by attacker). Write offset is attacker-controlled via PACK_ROW_LENGTH which sets row stride. Write size is width * 4 bytes per row, tunable via source canvas dimensions.
Tight primitive: width=2, height=2, PACK_ROW_LENGTH=N gives exactly 8 controlled bytes at offset N*4 past the scratch buffer.
Attack Chain:
- Set
PACK_ROW_LENGTH=N. Forwarded to ANGLE via command buffer (gles2_implementation.ccusesbreak, notreturn, for this param). - Bind PBO, then readPixels. This bypasses
ScopedPackStateRowLengthReset(disabled when PBO bound,passthrough_doers.cc:2661). ANGLE syncsPACK_ROW_LENGTH=Nto native GL. - Unbind and delete PBO.
StateManagerGL::deleteBufferunbinds PBO in native GL.PACK_ROW_LENGTHpersists. - Call
texImage2D(SRGB8_ALPHA8, webglCanvas). SRGB dest forces CPU readback path (TextureGL.cpp:1113,1133skip fast paths whendestSRGB=true). WebGL canvas source is texture-backed, routes throughCopySubTextureCHROMIUMtocopySubTextureCPUReadback. - Overflow.
BlitGL.cpp:859calls nativeglReadPixelswith stalePACK_ROW_LENGTH=N, strideN*4bytes per row into awidth*height*4*2byte scratch buffer.
Affected Code
// third_party/angle/src/libANGLE/Context.cpp:86-89
constexpr state::DirtyBits kTexImageDirtyBits{
state::DIRTY_BIT_UNPACK_STATE,
state::DIRTY_BIT_UNPACK_BUFFER_BINDING,
// Missing: DIRTY_BIT_PACK_STATE, DIRTY_BIT_PACK_BUFFER_BINDING
};
syncStateForTexImage() uses kTexImageDirtyBits. It syncs unpack state but never pack state. Compare with kReadPixelsDirtyBits (line 94) which correctly includes DIRTY_BIT_PACK_STATE.
When copySubTextureCPUReadback (BlitGL.cpp:859) calls mFunctions->readPixels() into a scratch buffer, it reads with whatever PACK_ROW_LENGTH was last synced to native GL by a prior readPixels call. The scratch buffer is sized for width * height * 4 * 2 (32KB for 64x64), but native GL writes with stride PACK_ROW_LENGTH * 4, overflowing the buffer.
Steps to Reproduce
- Build Chromium with ASan (
is_asan = true) or use an ASan-instrumented build. - Save the attached
poc.html. - Run:
./chrome --no-sandbox --ignore-gpu-blocklist --single-process poc.html
- ASan reports
heap-buffer-overflow(orheap-use-after-free) on theChrome_InProcGpthread atBlitGL::copySubTextureCPUReadback.
No GPU hardware required. Reproduces on stock Linux VMs with Mesa llvmpipe.
ASan Output
PACK_ROW_LENGTH=4096. Full trace in asan_out.txt:
==175475==ERROR: AddressSanitizer: heap-use-after-free on address 0x75876315dab0
WRITE of size 8 at 0x75876315dab0 thread T17 (Chrome_InProcGp)
#0 memcpy
#1-#4 libgallium (Mesa readpixels)
#5 rx::BlitGL::copySubTextureCPUReadback BlitGL.cpp:859
#6 rx::TextureGL::copySubTextureHelper TextureGL.cpp:1150
...
#12 GLES2DecoderPassthroughImpl::DoCopySubTextureCHROMIUM passthrough_doers.cc:4501
0x75876315dab0 is located 16 bytes inside of 29-byte region [0x75876315daa0,0x75876315dabd)
freed by thread T17 (Chrome_InProcGp) here:
#0 operator delete
#1 llvm::MCContext::reset()
MiraclePtr Status: NOT PROTECTED
Controlled bytes written at controlled offset past the scratch buffer. Landed in a freed LLVM MCContext region, likely reclaimable via heap spray.
Fix
Add pack state dirty bits to kTexImageDirtyBits:
constexpr state::DirtyBits kTexImageDirtyBits{
state::DIRTY_BIT_UNPACK_STATE,
state::DIRTY_BIT_UNPACK_BUFFER_BINDING,
state::DIRTY_BIT_PACK_STATE, // ADD
state::DIRTY_BIT_PACK_BUFFER_BINDING, // ADD
};
Or reset pack state in copySubTextureCPUReadback before readPixels (BlitGL.cpp:859), similar to how it already resets pack state after (line 873-876).
Bisect
Introduced in ANGLE commit https://chromium.googlesource.com/angle/angle/+/aadc8f376a (“Implement the CPU fallback for CopyTextureCHROMIUM on OpenGL”, 2017-08-11). This commit added copySubTextureCPUReadback with readPixels before setPixelPackState. Pack state was never reset before the read, from day one. Rolled into Chromium via https://chromium.googlesource.com/chromium/src/+/ed7971c95c05a (2017-08-30), shipping in Chrome 63 (December 2017).
The PBO bypass path that disables ScopedPackStateRowLengthReset when a PBO is bound was added in https://chromium.googlesource.com/chromium/src/+/5899dbdc3d96d (“Implement async ReadPixels for the passthrough command decoder”, 2017-10-05), making the stale state reachable.
Impact analysis
Heap overflow in the GPU process from unprivileged WebGL2 JavaScript, no compromised renderer required. The attacker controls the write data (source canvas pixels), write offset (PACK_ROW_LENGTH), and write size (source canvas width). The overflow target is a malloc’d scratch buffer with no MiraclePtr protection. On Linux, the GPU process is sandboxed but not as tightly as the renderer.
The cause
What version of Chrome have you found the security issue in?
147.0.7703.0 (Developer Build) (64-bit)
Is the security issue related to a crash?
Yes, it is related to a crash.
Choose the type of vulnerability
Memory Corruption (in a sandboxed process)
How would you like to be publicly acknowledged for your report?
cinzinga
- https://bughunters.google.com/about/rules/5745167867576320/chrome-vulnerability-reward-program-rules
- https://chromium.googlesource.com/angle/angle/+/aadc8f376a
- https://chromium.googlesource.com/chromium/src/+/5899dbdc3d96d
- https://chromium.googlesource.com/chromium/src/+/ed7971c95c05a
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/angle/src/libANGLE/renderer/gl/BlitGL.cpp