CVE-2026-79269
Overview
Files Changed
src/libANGLE/renderer/gl/BlitGL.cppsrc/libANGLE/renderer/gl/FramebufferGL.cpp
Patch
From 16935ea0e34f33b2590c48c0309a46ae332a5c49 Mon Sep 17 00:00:00 2001
From: Geoff Lang <geofflang@chromium.org>
Date: Wed, 15 Jul 2026 11:53:59 -0400
Subject: [PATCH] GL: Check for GL errors after glReadPixels calls.
If glReadPixels calls fail, client memory can be left un-written,
leading to potential leaks. Be conservative and check for errors after
these calls.
Fixed: chromium:520542088
Change-Id: I0d296af9c0a0e8f1052aef4ca8125732c70e5e9a
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8100063
Reviewed-by: Shahbaz Youssefi <syoussefi@chromium.org>
Commit-Queue: Shahbaz Youssefi <syoussefi@chromium.org>
---
diff --git a/src/libANGLE/renderer/gl/BlitGL.cpp b/src/libANGLE/renderer/gl/BlitGL.cpp
index efada9a..c33ee53 100644
--- a/src/libANGLE/renderer/gl/BlitGL.cpp
+++ b/src/libANGLE/renderer/gl/BlitGL.cpp
@@ -862,9 +862,10 @@
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));
+ ANGLE_GL_TRY_ALWAYS_CHECK(
+ context, mFunctions->readPixels(readPixelsArea.x, readPixelsArea.y, readPixelsArea.width,
+ readPixelsArea.height, readPixelsFormat, GL_UNSIGNED_BYTE,
+ sourceMemory));
angle::FormatID destFormatID =
angle::Format::InternalFormatToID(destInternalFormatInfo.sizedInternalFormat);
diff --git a/src/libANGLE/renderer/gl/FramebufferGL.cpp b/src/libANGLE/renderer/gl/FramebufferGL.cpp
index abbf39c..38e521f 100644
--- a/src/libANGLE/renderer/gl/FramebufferGL.cpp
+++ b/src/libANGLE/renderer/gl/FramebufferGL.cpp
@@ -1665,8 +1665,8 @@
ANGLE_UNSAFE_TODO(readbackPixels += skipBytes);
for (GLint y = area.y; y < area.y + area.height; ++y)
{
- ANGLE_GL_TRY(context,
- functions->readPixels(area.x, y, area.width, 1, format, type, readbackPixels));
+ ANGLE_GL_TRY_ALWAYS_CHECK(
+ context, functions->readPixels(area.x, y, area.width, 1, format, type, readbackPixels));
ANGLE_UNSAFE_TODO(readbackPixels += rowBytes);
}
@@ -1714,8 +1714,9 @@
if (height > 0)
{
ANGLE_TRY(stateManager->setPixelPackState(context, pack));
- ANGLE_GL_TRY(context, functions->readPixels(area.x, area.y, area.width, height, format,
- type, workaround.Pixels()));
+ ANGLE_GL_TRY_ALWAYS_CHECK(
+ context, functions->readPixels(area.x, area.y, area.width, height, format, type,
+ workaround.Pixels()));
}
if (readLastRowSeparately)
@@ -1726,8 +1727,9 @@
GLubyte *readbackPixels = workaround.Pixels();
ANGLE_UNSAFE_TODO(readbackPixels += skipBytes + (area.height - 1) * rowBytes);
- ANGLE_GL_TRY(context, functions->readPixels(area.x, area.y + area.height - 1, area.width, 1,
- format, type, readbackPixels));
+ ANGLE_GL_TRY_ALWAYS_CHECK(
+ context, functions->readPixels(area.x, area.y + area.height - 1, area.width, 1, format,
+ type, readbackPixels));
}
if (workaround.IsEnabled())
Original Bug Report
Potential cross-origin pixel and heap leak in BlitGL::copySubTextureCPUReadback
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A vulnerability in ANGLE’s GL backend allows potential cross-origin pixel or uninitialized heap leaks by reusing dirty Display-pooled scratch buffers. If a native glReadPixels call fails silently in a release build, unmodified stale or uninitialized memory in the scratch buffer is uploaded to an attacker-controlled texture. This occurs because scratch buffers are shared across origins in a Display-level pool and are not zero-initialized upon retrieval.
Affected files:
third_party/angle/src/libANGLE/renderer/gl/BlitGL.cpp
Estimated timestamp from git blame: 2020-02-03
Background
ANGLE’s GL backend uses a Display-level LIFO pool to share scratch buffers across contexts in the same GPU process. When a context is made non-current, its scratch buffer is returned to the Display pool via Context::unMakeCurrent (third_party/angle/src/libANGLE/Context.cpp:1154) and stored in Display::mScratchBuffers (third_party/angle/src/libANGLE/Display.cpp:2664). When another context becomes current and requests a scratch buffer, it can retrieve this exact buffer with its prior contents intact.
The Vulnerability
In BlitGL::copySubTextureCPUReadback (third_party/angle/src/libANGLE/renderer/gl/BlitGL.cpp:843-894), ANGLE obtains a scratch buffer without zero-filling it:
angle::MemoryBuffer *buffer = nullptr;
ANGLE_CHECK_GL_ALLOC(contextGL,
context->getScratchBuffer(sourceBufferSize + destBufferSize, &buffer));
It then performs a native glReadPixels call to fill sourceMemory:
ANGLE_GL_TRY(context, mFunctions->readPixels(readPixelsArea.x, readPixelsArea.y,
readPixelsArea.width, readPixelsArea.height,
readPixelsFormat, GL_UNSIGNED_BYTE, sourceMemory));
In Chrome production/release builds, ANGLE_GL_TRY compiles to a bare call with no error-checking or context-loss triggers:
#define ANGLE_GL_TRY(context, call) call
If the native glReadPixels call fails silently (due to GL_OUT_OF_MEMORY, an incomplete framebuffer state, or other driver-specific errors), the error is ignored, and the stale bytes in the scratch buffer remain unmodified.
Subsequently, CopyImageCHROMIUM reads the stale bytes from sourceMemory, converts them, and writes the results to destMemory, which is then uploaded directly to the attacker-controlled texture via texSubImage2D:
ANGLE_GL_TRY(context, mFunctions->texSubImage2D(..., destMemory));
Exposure Vectors
- Cross-Origin Pixel Leak: The scratch buffer is reused across different WebGL contexts/origins without being cleared, meaning the attacker can read stale pixels belonging to a victim origin.
- GPU Heap Leak: If the requested buffer size exceeds the pooled buffer’s size, a raw
malloc()is executed viaMemoryBuffer::resize(third_party/angle/src/common/MemoryBuffer.cpp:56). No zero-fill is performed, exposing raw, uninitialized GPU-process heap memory.
Potential Steps to Trigger
Note: These are suggested steps based on source code analysis; our tooling does not currently have the capability to execute code in a live environment.
- A victim origin (Origin A) performs WebGL copy/cache operations, populating the scratch buffer with sensitive data, and is then unmade current.
- The attacker origin (Origin B) becomes active and invokes
CopySubTextureCHROMIUMon an sRGB destination texture, forcing ANGLE’s GL backend to take the CPU readback fallback path (copySubTextureCPUReadback). - The attacker’s context retrieves the dirty scratch buffer from the display pool.
- The attacker induces a native driver error or
GL_OUT_OF_MEMORYstate to makeglReadPixelsfail silently. - The stale/uninitialized scratch buffer bytes are uploaded to the attacker’s texture, which the attacker reads back using standard WebGL readback APIs.
Recommended Fix
Before executing the copy operation in BlitGL::copySubTextureCPUReadback, initialize the scratch buffer to zero. This is similar to the mitigation applied in the D3D11 backend (third_party/angle/src/libANGLE/renderer/d3d/d3d11/Image11.cpp:434):
buffer->fill(0);
Evaluated with Chrome root at commit: e9507a33bb4148ee071aaaf8a7e9ad68770359bf
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.