CVE-2026-9911
Overview
Background
- ANGLE OpenGL (GL) backend
- Translates GLES calls to native OpenGL;
TextureGL.cppimplements 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_BUFFERis bound, thepixelspointer is interpreted by the driver as a byte offset into the PBO, not a client pointer. - Offset arithmetic
skipBytes,rowBytes,imageBytes,area.height,area.depthare combined to locate the last row: e.g.skipBytes + (area.height - 1) * rowBytes. The result is used aspixels + 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).
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
- Create context Attacker creates a WebGL 2.0 context running on the ANGLE GL backend (Linux/macOS).
- Allocate texture
Bind a 2D texture of at least
W x 1so the manual last-row path is exercised. - Bind small PBO
Bind a small
PIXEL_UNPACK_BUFFERsopixelsis treated as a PBO offset. - Inflate stride
pixelStorei(UNPACK_ROW_LENGTH, large)to makerowByteslarge. - Trigger
Issue a zero-height (or otherwise boundary)
texSubImage2D;ShouldApplyLastRowPaddingWorkaroundreturns true andsetSubImagePaddingWorkaroundcomputesskipBytes + (0 - 1) * rowBytes, underflowing into a negativeGLintoffset (inferred for the exactheight=0path; the diff fixes the same offset expression). - OOB read / leak
The driver reads from memory before the PBO mapping into the texture;
readPixelsreturns that out-of-bounds GPU-process memory to the renderer — an information leak.
Impact Assessment
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 localsAudit
TextureGL.cppand sibling GL-backend files for otherGLint/intlocals that hold byte offsets or sizes derived fromrowBytes/imageBytes/skipBytesand client pack state. - Empty-dimension fast pathsThe advisory’s suggested early-return for empty
width/height/depthwas not added; reviewsetImage/setSubImageandShouldApplyLastRowPaddingWorkaroundfor other misbehavior when a dimension is0. - PBO offset validationReview all paths that pass a computed
pixels + offsetto the driver under a bound PBO to confirm bounds are validated against the PBO size on every backend.
Patch
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,
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.