High CVSS 4.3 chrome OOB 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInteger overflow in ANGLE
DescriptionInteger overflow in ANGLE
ComponentANGLE
Sub-componentGL
Bug ClassOOB
Tracker499205491
Fix commit6044531d6116 (angle/angle) +3/-3
CWECWE-472
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-04-03 (per vendor advisory)
Fix landed2026-05-01 (public git)
Disclosed2026-05-27
Patch gap26 days — fix public in source before the advisory

Background

ANGLE OpenGL (GL) backend
Translates GLES calls to native OpenGL; TextureGL.cpp implements texture uploads for the GL backend (common on Linux/macOS).
Last-row/last-image padding workaround
Some drivers mishandle the final row/slice of an upload with certain pack/unpack state, so ANGLE uploads the last row (and, for 3D, last slice) “manually” via a separate texSubImage2D/3D, after computing its byte offset into the source.
Pixel Buffer Object (PBO)
When a PIXEL_UNPACK_BUFFER is bound, the pixels pointer is interpreted by the driver as a byte offset into the PBO, not a client pointer.
Offset arithmetic
skipBytes, rowBytes, imageBytes, area.height, area.depth are combined to locate the last row: e.g. skipBytes + (area.height - 1) * rowBytes. The result is used as pixels + offset.
`UNPACK_ROW_LENGTH`
Client-controlled state inflating rowBytes, widening the magnitude of the offset computation.

Root Cause Analysis

The last-row/last-image byte offsets in TextureGL.cpp were computed into a signed 32-bit GLint. The expressions (area.depth - 1) * imageBytes and skipBytes + (area.depth - 1) * imageBytes + (area.height - 1) * rowBytes and skipBytes + (area.height - 1) * rowBytes mix unsigned operands (dimensions and byte strides) and can either wrap or, when area.height/area.depth is 0, produce (0 - 1) == -1 times a large rowBytes/imageBytes. Stored into a GLint, the value truncates/underflows to a negative number; const GLubyte *lastRowPixels = pixels + lastRowOffset; then forms a negative offset. With a PBO bound, that negative offset is handed to the native driver as a position before the start of the PBO mapping, producing an out-of-bounds read of GPU-process memory (an info leak back to the renderer via readback).

The patch widens all three offset variables from GLint to size_t: lastImageOffset, the combined 3D lastRowOffset, and the 2D lastRowOffset. The commit message frames this as correctness for >2GB PBOs (“All the arguments are unsigned ints and the result is used as an offset. The buffer has already been validated to be large enough for the offset.”), and notes no test was added because triggering the overflow needs a >2GB PBO. Using size_t keeps the computation in a wide unsigned type matching the operands, so the offset no longer truncates into a negative GLint. Note the diff does not add the empty-area early-return or a base::CheckedNumeric guard that the advisory suggested; it relies on the stated precondition that the buffer was already validated large enough. The advisory labels this an integer overflow/truncation whose consequence is an OOB read — the diff substantiates the truncation mechanism directly (signed→unsigned width change).

Key insight
A memory offset was computed and stored in a signed 32-bit GLint from unsigned, attacker-influenced operands (UNPACK_ROW_LENGTH, dimensions), so it could truncate/underflow negative and index before the PBO; widening every offset to size_t keeps the arithmetic in the correct unsigned width.

Attack Path

  1. Create context Attacker creates a WebGL 2.0 context running on the ANGLE GL backend (Linux/macOS).
  2. Allocate texture Bind a 2D texture of at least W x 1 so the manual last-row path is exercised.
  3. Bind small PBO Bind a small PIXEL_UNPACK_BUFFER so pixels is treated as a PBO offset.
  4. Inflate stride pixelStorei(UNPACK_ROW_LENGTH, large) to make rowBytes large.
  5. Trigger Issue a zero-height (or otherwise boundary) texSubImage2D; ShouldApplyLastRowPaddingWorkaround returns true and setSubImagePaddingWorkaround computes skipBytes + (0 - 1) * rowBytes, underflowing into a negative GLint offset (inferred for the exact height=0 path; the diff fixes the same offset expression).
  6. OOB read / leak The driver reads from memory before the PBO mapping into the texture; readPixels returns that out-of-bounds GPU-process memory to the renderer — an information leak.

Impact Assessment

Reachable from an unprivileged renderer via WebGL2 on the ANGLE GL backend. The primitive is an out-of-bounds read of GPU-process memory surfaced back to the renderer via texture readback (C:L, no integrity/availability impact, matching the CVSS). The diff proves the integer-truncation root cause (narrow signed offset) and its use as a memory offset; the specific height=0/UNPACK_ROW_LENGTH trigger and the >2GB-PBO path are the two reachability variants (one inferred from the advisory, one stated by the committer). No write primitive is shown, so this is info disclosure.

Files Changed

  • src/libANGLE/renderer/gl/TextureGL.cpp

Audit Directions

  • Signed offset locals
    Audit TextureGL.cpp and sibling GL-backend files for other GLint/int locals that hold byte offsets or sizes derived from rowBytes/imageBytes/skipBytes and client pack state.
  • Empty-dimension fast paths
    The advisory’s suggested early-return for empty width/height/depth was not added; review setImage/setSubImage and ShouldApplyLastRowPaddingWorkaround for other misbehavior when a dimension is 0.
  • PBO offset validation
    Review all paths that pass a computed pixels + offset to the driver under a bound PBO to confirm bounds are validated against the PBO size on every backend.
From 6044531d611644bf8399535180903c8892105cd8 Mon Sep 17 00:00:00 2001
From: Geoff Lang <[email protected]>
Date: Fri, 01 May 2026 11:03:14 -0400
Subject: [PATCH] GL: Use size_t for calculating lastRowOffset in TextureGL.

All the arguments are unsigned ints and the result is used as an offset.

The buffer has already been validated to be large enough for the offset.

No test case for this one because it requires >2gb PBOs to even have a
potential of overflow.

Fixed: chromium:499205491
Change-Id: I890ef851e4d2c8fcdeadb4f471cb3c96ba8b3089
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7807650
Reviewed-by: Shahbaz Youssefi <[email protected]>
Commit-Queue: Geoff Lang <[email protected]>
---

diff --git a/src/libANGLE/renderer/gl/TextureGL.cpp b/src/libANGLE/renderer/gl/TextureGL.cpp
index 466fbe1..2816918 100644
--- a/src/libANGLE/renderer/gl/TextureGL.cpp
+++ b/src/libANGLE/renderer/gl/TextureGL.cpp
@@ -550,7 +550,7 @@
         {
             // Do not include skipBytes in the last image pixel start offset as it will be done by
             // the driver
-            GLint lastImageOffset          = (area.depth - 1) * imageBytes;
+            size_t lastImageOffset         = (area.depth - 1) * imageBytes;
             const GLubyte *lastImagePixels = pixels + lastImageOffset;
             ANGLE_GL_TRY(context, functions->texSubImage3D(
                                       ToGLenum(target), static_cast<GLint>(level), area.x, area.y,
@@ -561,7 +561,7 @@
         // Upload the last row of the last slice "manually"
         ANGLE_TRY(stateManager->setPixelUnpackState(context, directUnpack));
 
-        GLint lastRowOffset =
+        size_t lastRowOffset =
             skipBytes + (area.depth - 1) * imageBytes + (area.height - 1) * rowBytes;
         const GLubyte *lastRowPixels = pixels + lastRowOffset;
         ANGLE_GL_TRY(context,
@@ -584,7 +584,7 @@
         // Upload the last row "manually"
         ANGLE_TRY(stateManager->setPixelUnpackState(context, directUnpack));
 
-        GLint lastRowOffset          = skipBytes + (area.height - 1) * rowBytes;
+        size_t lastRowOffset         = skipBytes + (area.height - 1) * rowBytes;
         const GLubyte *lastRowPixels = pixels + lastRowOffset;
         ANGLE_GL_TRY(context, functions->texSubImage2D(ToGLenum(target), static_cast<GLint>(level),
                                                        area.x, area.y + area.height - 1, area.width,
Loading diff…

Original Bug Report

reported by [email protected]

Potential integer truncation in ANGLE TextureGL leads to OOB PBO read

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 without the security team.

Overview: A potential vulnerability in ANGLE’s GL backend occurs when a zero-height texture update is processed with a large UNPACK_ROW_LENGTH. This triggers a driver workaround that calculates a negative PBO offset, which is passed to the native OpenGL driver, causing an out-of-bounds read and an info leak from the GPU process.

Affected files:

  • third_party/angle/src/libANGLE/renderer/gl/TextureGL.cpp

Estimated timestamp from git blame: 2018-10-03

Summary

A potential integer truncation vulnerability exists in ANGLE’s TextureGL.cpp within the setSubImagePaddingWorkaround function. When processing a texture update with an empty height (height = 0) and a large UNPACK_ROW_LENGTH, ANGLE calculates a negative byte offset for the Pixel Buffer Object (PBO). This negative offset is passed to the native OpenGL driver, resulting in an out-of-bounds memory read from the GPU process heap. This can be exploited to leak sensitive information back to the renderer process.

Technical Details

In WebGL 2, a texture update with height = 0 is valid and passes all frontend dimension and buffer size checks, because an update of 0 pixels requires 0 bytes of PBO memory.

When the command reaches ANGLE’s OpenGL backend (which is commonly used on Linux and macOS), it enters TextureGL::setSubImage. This function checks if a driver workaround is needed by calling ShouldApplyLastRowPaddingWorkaround. For height = 0, the calculated data size is correctly 0. However, the workaround logic incorrectly adds the padding expected by a driver based on the large UNPACK_ROW_LENGTH. This artificially inflates the expected end byte, causing ShouldApplyLastRowPaddingWorkaround to return true.

Consequently, ANGLE calls setSubImagePaddingWorkaround (found at third_party/angle/src/libANGLE/renderer/gl/TextureGL.cpp). This function calculates the byte offset for the “last row” to be uploaded using unchecked arithmetic:

GLint lastRowOffset = skipBytes + (area.height - 1) * rowBytes;

With area.height = 0, (area.height - 1) evaluates to -1. When multiplied by a large unsigned rowBytes (e.g., 1,000,000 bytes, derived from UNPACK_ROW_LENGTH), the result underflows as a 32-bit unsigned integer (e.g., 0xFFF0BDC0). This value is then truncated and stored in the signed GLint lastRowOffset as -1000000.

Next, the pointer math const GLubyte *lastRowPixels = pixels + lastRowOffset; creates a negative offset. Because a PBO is bound, the native OpenGL driver interprets this pointer as an offset into the PBO. Native drivers typically do not expect negative offsets since the frontend is supposed to validate bounds, leading to a raw out-of-bounds read of GPU process memory located before the start of the PBO mapping.

Suggested Steps to Trigger (Theoretical)

Note: These are suggested/potential steps as our tooling agent doesn’t yet have the ability to run code to confirm a working Proof of Concept.

  1. Create a WebGL 2.0 context.
  2. Create and bind a 2D texture, allocating at least 100x1 pixels.
  3. Create and bind a PIXEL_UNPACK_BUFFER with a small allocation (e.g., 16 bytes).
  4. Set a large row length to control the negative offset distance: gl.pixelStorei(gl.UNPACK_ROW_LENGTH, 250000);
  5. Trigger the bug with a zero-height update: gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 1, 100, 0, gl.RGBA, gl.UNSIGNED_BYTE, 0);
  6. The native driver will read from the GPU process heap memory before the PBO and write it into the texture.
  7. Read back the texture contents using gl.readPixels() to retrieve the leaked data.

Suggested Fix

  1. Implement an early return in TextureGL::setImage and TextureGL::setSubImage if the update area is empty (e.g., area.width == 0 || area.height == 0 || area.depth == 0).
  2. Update ShouldApplyLastRowPaddingWorkaround in renderergl_utils.cpp to immediately return false if the dimensions are empty to prevent unnecessary workaround execution.
  3. Replace raw arithmetic with base::CheckedNumeric when calculating lastRowOffset in setSubImagePaddingWorkaround to prevent underflows and truncation.

Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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