CVE-2026-9911
Overview
Files Changed
src/libANGLE/renderer/gl/TextureGL.cpp
Patch
From 6044531d611644bf8399535180903c8892105cd8 Mon Sep 17 00:00:00 2001
From: Geoff Lang <geofflang@chromium.org>
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 <syoussefi@chromium.org>
Commit-Queue: Geoff Lang <geofflang@chromium.org>
---
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,
Original Bug Report
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.
- Create a WebGL 2.0 context.
- Create and bind a 2D texture, allocating at least
100x1pixels. - Create and bind a
PIXEL_UNPACK_BUFFERwith a small allocation (e.g., 16 bytes). - Set a large row length to control the negative offset distance:
gl.pixelStorei(gl.UNPACK_ROW_LENGTH, 250000); - Trigger the bug with a zero-height update:
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 1, 100, 0, gl.RGBA, gl.UNSIGNED_BYTE, 0); - The native driver will read from the GPU process heap memory before the PBO and write it into the texture.
- Read back the texture contents using
gl.readPixels()to retrieve the leaked data.
Suggested Fix
- Implement an early return in
TextureGL::setImageandTextureGL::setSubImageif the update area is empty (e.g.,area.width == 0 || area.height == 0 || area.depth == 0). - Update
ShouldApplyLastRowPaddingWorkaroundinrenderergl_utils.cppto immediately returnfalseif the dimensions are empty to prevent unnecessary workaround execution. - Replace raw arithmetic with
base::CheckedNumericwhen calculatinglastRowOffsetinsetSubImagePaddingWorkaroundto 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.