CVE-2026-79269
Overview
Background
- `glReadPixels`
- the GL call that copies a rectangle of framebuffer pixels into a client-supplied memory buffer; on failure it can leave that buffer wholly or partially unwritten.
- `ANGLE_GL_TRY`
- ANGLE’s macro that invokes a backend GL call and, in the removed form, does not unconditionally verify
glGetErrorafterward, so a failed call can be treated as if it succeeded. - `ANGLE_GL_TRY_ALWAYS_CHECK`
- the macro that always calls
glGetErrorafter the wrapped call and propagates any error as an ANGLE failure, aborting the operation instead of continuing. - Readback destination buffers
sourceMemoryinBlitGL.cppandreadbackPixels/workaround.Pixels()inFramebufferGL.cpp, the intermediate buffers that receivereadPixelsoutput before being handed onward.
Root Cause Analysis
The GL backend performed readPixels into intermediate CPU buffers using ANGLE_GL_TRY, which did not reliably check for a GL error after the call. If a readPixels failed at the driver level, the destination buffer was left un-written (uninitialized), yet control flowed on to consume that buffer as though it held valid pixel data. This violates the invariant that a buffer returned to (or copied on behalf of) the client must be fully populated with defined bytes.
The patch replaces each such call with ANGLE_GL_TRY_ALWAYS_CHECK, which always inspects glGetError and converts a failed readPixels into an ANGLE error that propagates out before the uninitialized memory is used or returned. The change tightens error handling; it adds no new fast path and does not alter the arguments to readPixels.
readPixels always succeeds and populates its destination, so a silent driver failure left uninitialized heap memory to be consumed; the fix forces a post-call error check (ANGLE_GL_TRY_ALWAYS_CHECK) so a failed read aborts instead of leaking undefined bytes.Attack Path
- Drive a readback path
A page issues an operation that funnels through ANGLE’s GL backend readback code (a blit/copy in
BlitGL.cppor a framebuffer read inFramebufferGL.cpp), supplying parameters that cause the underlying driverreadPixelsto fail. - Provoke a `readPixels` failure
The failing
readPixelsreturns without writing (or without fully writing) its destination buffer (sourceMemory/readbackPixels/workaround.Pixels()) (inferred that specific failure triggers are driver/format dependent). - Skip the error and consume stale memory
With the pre-patch
ANGLE_GL_TRY, the missing error check lets execution continue and copy the uninitialized destination buffer onward as if it were valid pixel data. - Observe leaked bytes The uninitialized contents surface to the client/page as read-back pixels, disclosing uninitialized GPU-process memory (inferred that the leaked bytes are attacker-observable rendered/read output).
Impact Assessment
readPixels failure whose unwritten destination buffer is then returned as pixel data, producing an information leak. Preconditions are the ability to reach these GL-backend readback paths and to make the driver readPixels fail. The advisory classifies this as UninitializedMemory (“Uninitialized resource in ANGLE”) and does not report in-the-wild exploitation; any further use of the leaked data is (inferred).Files Changed
src/libANGLE/renderer/gl/FramebufferGL.cppsrc/libANGLE/renderer/gl/BlitGL.cpp
Audit Directions
- Unchecked GPU call resultsSearch for backend calls (especially readbacks,
getTexImage,mapBuffer, downloads) wrapped in non-always-checking try macros where the destination is subsequently read or returned. - Failure-path buffer stateFor every buffer handed to a driver call that may fail, confirm the buffer is either fully written or explicitly zeroed/abandoned on the error path before any downstream consumer touches it.
- Loop and per-row readbacksReview multi-call readback loops (per-row / last-row-separately patterns) where one failing iteration can leave part of a larger buffer uninitialized while the overall operation still reports success.
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.