Medium watch CVSS 4.3 chrome Uninitialized Memory 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUninitialized resource in ANGLE
DescriptionUninitialized resource in ANGLE
ComponentANGLE
Sub-componentGL
Bug ClassUninitialized Memory
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker520542088
Fix commit16935ea0e34f (angle/angle) +12/-9
CWECWE-908
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-06-05 (per vendor advisory)
Fix landed2026-07-16 (public git)
Disclosed2026-08-25
Patch gap40 days — fix public in source before the advisory

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 glGetError afterward, so a failed call can be treated as if it succeeded.
`ANGLE_GL_TRY_ALWAYS_CHECK`
the macro that always calls glGetError after the wrapped call and propagates any error as an ANGLE failure, aborting the operation instead of continuing.
Readback destination buffers
sourceMemory in BlitGL.cpp and readbackPixels / workaround.Pixels() in FramebufferGL.cpp, the intermediate buffers that receive readPixels output 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.

Key insight
The core mistake was assuming 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

  1. Drive a readback path A page issues an operation that funnels through ANGLE’s GL backend readback code (a blit/copy in BlitGL.cpp or a framebuffer read in FramebufferGL.cpp), supplying parameters that cause the underlying driver readPixels to fail.
  2. Provoke a `readPixels` failure The failing readPixels returns without writing (or without fully writing) its destination buffer (sourceMemory / readbackPixels / workaround.Pixels()) (inferred that specific failure triggers are driver/format dependent).
  3. 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.
  4. 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

An attacker executing GPU-backed content can obtain a read of uninitialized memory in the GPU process by forcing a 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.cpp
  • src/libANGLE/renderer/gl/BlitGL.cpp

Audit Directions

  • Unchecked GPU call results
    Search 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 state
    For 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 readbacks
    Review 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.
From 16935ea0e34f33b2590c48c0309a46ae332a5c49 Mon Sep 17 00:00:00 2001
From: Geoff Lang <[email protected]>
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 <[email protected]>
Commit-Queue: Shahbaz Youssefi <[email protected]>
---
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())
Loading diff…

Original Bug Report

reported by [email protected]

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

  1. 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.
  2. GPU Heap Leak: If the requested buffer size exceeds the pooled buffer’s size, a raw malloc() is executed via MemoryBuffer::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.

  1. A victim origin (Origin A) performs WebGL copy/cache operations, populating the scratch buffer with sensitive data, and is then unmade current.
  2. The attacker origin (Origin B) becomes active and invokes CopySubTextureCHROMIUM on an sRGB destination texture, forcing ANGLE’s GL backend to take the CPU readback fallback path (copySubTextureCPUReadback).
  3. The attacker’s context retrieves the dirty scratch buffer from the display pool.
  4. The attacker induces a native driver error or GL_OUT_OF_MEMORY state to make glReadPixels fail silently.
  5. The stale/uninitialized scratch buffer bytes are uploaded to the attacker’s texture, which the attacker reads back using standard WebGL readback APIs.

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.

View on issue tracker