Critical watch ~ plausible CVSS 9.6 chrome OOB 📄 Reporter bug report 🔧 Commit mapped

Overview

Critical
Severity
9.6
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds write in GPU
DescriptionOut of bounds write in GPU
ComponentGPU
Bug ClassOOB
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable — revised by independent review by AI
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.
Independent review by AIPlausible (tier revised from patch-now)
Reviewer notes: Diff shows no CPU staging-buffer allocation; glTexImage2D(...,0) implies a bound PBO, so overflow location/driver behavior is asserted. Path is validating-decoder copy-texture readback fallback (specific formats/drivers), not default everywhere; PACK_ROW_LENGTH needs ES3/WebGL2.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker505077859
Fix commitae8c96f69539 (chromium/src) +11/-0
CWECWE-787 (Out-of-bounds write)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
Creditedcinzinga
Reported2026-04-21 · bounty $43000 (per vendor advisory)
Fix landed2026-04-23 (public git)
Disclosed2026-05-27
Patch gap34 days — fix public in source before the advisory

Background

Pixel store pack/unpack parameters
GL state set via glPixelStorei (GL_PACK_ROW_LENGTH, GL_PACK_ALIGNMENT, GL_UNPACK_ROW_LENGTH, GL_UNPACK_IMAGE_HEIGHT, GL_UNPACK_ALIGNMENT) that control the row stride and alignment used when the driver reads pixels out to (pack) or reads pixels in from (unpack) client memory.
`glReadPixels` stride
with a non-default GL_PACK_ROW_LENGTH or GL_PACK_ALIGNMENT, glReadPixels writes each row at a larger stride than the image width, so the total bytes touched exceed a tightly-packed width*height*bpp buffer.
`DoReadbackAndTexImage`
the copy-texture helper that reads framebuffer pixels into an internal, tightly-packed staging buffer and then re-uploads them with glTexImage2D/glTexSubImage2D.
`RestoreGlobalState`
a decoder method that re-applies the client’s tracked GL global state after the helper has temporarily changed it.

Root Cause Analysis

Verify before building a PoC

This analysis is consistent with the fixing commit but not fully provable from it: part of the vulnerable path lives in code the fix commit does not touch. Confirm the trigger and the sink against the source before relying on it for exploit development.
What the commit does not establish: Diff shows no CPU staging-buffer allocation; glTexImage2D(...,0) implies a bound PBO, so overflow location/driver behavior is asserted. Path is validating-decoder copy-texture readback fallback (specific formats/drivers), not default everywhere; PACK_ROW_LENGTH needs ES3/WebGL2.

DoReadbackAndTexImage allocates an internal buffer sized for a tightly-packed image and then calls glReadPixels to fill it and glTexImage2D/glTexSubImage2D to re-upload it, but before the patch it did not reset the GL pack/unpack pixel-store parameters, so whatever GL_PACK_ROW_LENGTH/GL_PACK_ALIGNMENT (and unpack equivalents) the client had previously set via glPixelStorei remained in effect. With an attacker-chosen non-zero GL_PACK_ROW_LENGTH or larger GL_PACK_ALIGNMENT, the driver’s glReadPixels computes a row stride larger than the buffer’s tightly-packed stride and writes past the end of the internal staging buffer — an out-of-bounds write in the GPU process; the unpack params analogously misdescribe the buffer layout on the re-upload path.

The patch resets GL_PACK_ROW_LENGTH to 0 and GL_PACK_ALIGNMENT to 1 immediately before the readback, resets GL_UNPACK_ROW_LENGTH/GL_UNPACK_IMAGE_HEIGHT to 0 and GL_UNPACK_ALIGNMENT to 1 before the glTexImage2D upload so the tightly-packed buffer is described correctly, and adds decoder->RestoreGlobalState() on both the error and success exit paths so the client’s real pixel-store state is restored afterward rather than left clobbered.

Key insight
The helper assumed a tightly-packed staging buffer but left the client’s glPixelStorei pack/unpack row-length and alignment state active, so a non-default stride made glReadPixels/upload run past the buffer; the fix forces packed pack/unpack params around the operation and restores global state afterward.

Attack Path

  1. Set hostile pixel-store state The renderer issues glPixelStorei to set a large GL_PACK_ROW_LENGTH (or GL_PACK_ALIGNMENT) so the pack stride exceeds the image width.
  2. Invoke the copy-texture readback It triggers the copy-texture path that runs DoReadbackAndTexImage, allocating an internal buffer sized for a tightly-packed image.
  3. Overflow on readback glReadPixels uses the still-active pack stride to write rows at the larger stride into the tightly-packed buffer, writing past its end — an out-of-bounds write in the GPU process; the unpack params can similarly corrupt the re-upload path.

Impact Assessment

An attacker controlling the renderer’s GL command stream can leave non-default pack/unpack pixel-store parameters in effect so that the copy-texture readback writes beyond a tightly-packed internal buffer, yielding an out-of-bounds write in the GPU process as the advisory classifies it. The advisory does not report exploitation in the wild, and any escalation from the OOB write to broader memory corruption or a sandbox escape is (inferred). The precondition is the ability to set pixel-store state and drive the copy-texture code path that reaches DoReadbackAndTexImage.

Changed Functions

FunctionChangeNotes
if
gpu/command_buffer/service/gles2_cmd_copy_texture_chromium.cc
modified
CopyTextureResourceManagerImpl
gpu/command_buffer/service/gles2_cmd_copy_texture_chromium.cc
modified

Files Changed

  • gpu/command_buffer/service/gles2_cmd_copy_texture_chromium.cc

Audit Directions

  • Implicit tightly-packed assumptions
    Audit internal readback/upload helpers that size a buffer as width*height*bpp and call glReadPixels/glTexImage2D without first forcing ROW_LENGTH/IMAGE_HEIGHT/ALIGNMENT to their packed defaults.
  • Client pixel-store state leaking into service code
    Look for GPU-service paths that call GL pixel-transfer functions while the client’s glPixelStorei state is still active, which lets a renderer control driver-side stride/alignment.
  • Missing state restore on temporary changes
    Review helpers that temporarily modify global GL state for both RestoreGlobalState-style restoration and for correct behavior on every early-return/error exit, not just the success path.
From ae8c96f69539e21694476ca6e3fad3f125698850 Mon Sep 17 00:00:00 2001
From: Vasiliy Telezhnikov <[email protected]>
Date: Thu, 23 Apr 2026 13:58:48 -0700
Subject: [PATCH] Fix pack/unpack state during DoReadbackAndTexImage

Bug: 505077859
Change-Id: If5fd8a788710e0a7b0724fdfacfa309416a2b153
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7789335
Reviewed-by: Geoff Lang <[email protected]>
Commit-Queue: Vasiliy Telezhnikov <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1619750}
---

diff --git a/gpu/command_buffer/service/gles2_cmd_copy_texture_chromium.cc b/gpu/command_buffer/service/gles2_cmd_copy_texture_chromium.cc
index 98e7f6f..a73cca8 100644
--- a/gpu/command_buffer/service/gles2_cmd_copy_texture_chromium.cc
+++ b/gpu/command_buffer/service/gles2_cmd_copy_texture_chromium.cc
@@ -677,6 +677,10 @@
     return false;
   }
 
+  // We're about to read pixels, so we need to reset PACK params
+  glPixelStorei(GL_PACK_ROW_LENGTH, 0);
+  glPixelStorei(GL_PACK_ALIGNMENT, 1);
+
   // Result of glReadPixels with format == GL_RGB and type == GL_UNSIGNED_BYTE
   // from read framebuffer in RGBA format is not correct on desktop core
   // profile on both Linux Mesa and Linux NVIDIA. This may be a driver bug.
@@ -839,9 +843,15 @@
       decoder->RestoreActiveTexture();
       decoder->RestoreFramebufferBindings();
       decoder->RestoreBufferBindings();
+      decoder->RestoreGlobalState();
       return;
     }
 
+    // Our buffer is tightly packed, so reset unpack params.
+    glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
+    glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, 0);
+    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
+
     if (command_type == kTexImage) {
       glTexImage2D(dest_target, dest_level, dest_internal_format, width, height,
                    0, format, type, 0);
@@ -858,6 +868,7 @@
   decoder->RestoreActiveTexture();
   decoder->RestoreFramebufferBindings();
   decoder->RestoreBufferBindings();
+  decoder->RestoreGlobalState();
 }
 
 class CopyTextureResourceManagerImpl
Loading diff…

Original Bug Report

reported by [email protected]

Validating Decoder Stale PACK_ALIGNMENT causes GPU Heap OOB Write


Report description

Validating Decoder Stale PACK_ALIGNMENT causes GPU Heap OOB Write


Bug location

Where do you want to report your vulnerability?

Chrome VRP – Report security issues affecting the Chrome browser. See program rules

Which URL (or repository) have you found the vulnerability in?

https://source.chromium.org/chromium/chromium/src/+/main:gpu/command_buffer/service/gles2_cmd_copy_texture_chromium.cc


The problem

Please describe the technical details of the vulnerability

PrepareUnpackBuffer() allocates a width * height * 4 RGBA scratch buffer and calls glReadPixels(..., GL_RGBA, GL_UNSIGNED_BYTE, ...) on Android for the fallback readback path used by GL_RGB / GL_FLOAT uploads. The code assumes tightly packed rows, but attacker-controlled GL_PACK_ALIGNMENT is still honored because the validating decoder forwards pack state to native GL and Blink resets only unpack state before this path. With PACK_ALIGNMENT = 8 and width = 3, each row is written with a 16-byte stride even though only 12 bytes per row are allocated.

The PoC sets PACK_ALIGNMENT = 8 and uploads a 3 x 8192 source canvas with per-row markers via texImage2D(GL_TEXTURE_2D, 0, GL_RGB9_E5, GL_RGB, GL_HALF_FLOAT, srcCanvas), reaching PrepareUnpackBuffer() on Android with the undersized buffer.

At those dimensions, the allocation is 98304 bytes, but rows are written at 16-byte stride. The last 2048 rows therefore go out of bounds, yielding 24576 bytes of attacker-controlled data written over roughly 32 KB beyond the allocation.

Steps to Reproduce

Serve poc.html over HTTP and open it in Chrome on Android. No flags required.

Crash Evidence

  1. Symbolized ASan stack trace (from Android content shell) attached as symbolized_stack_trace.txt
  2. Chrome Stable tombstone is from com.android.chrome:privileged_process* (CrGpuMain), signal 11 SEGV_ACCERR (write), with the fault in ReadPixels/glReadPixels
  3. chrome://crashes 0e2b36268c1779c4 or 311f8eaf5e9c3abb

Proposed Fix

Either compute the allocation using the effective packed row stride, or temporarily force a safe GL_PACK_ALIGNMENT around the internal glReadPixels call and restore the previous value.

Bisect

Introduced by commit 986a220965888 (Add readback path for CopyTextureCHROMIUM, 2017-03-30), which added the fallback readback path and its tight-row allocation assumption.

Impact analysis

  • Web-accessible via WebGL2 on Android, no compromised renderer required.
  • GPU process linear heap overwrite of attacker-controlled pixel data, 24576 bytes, deterministic geometry.
  • On current Android phone builds, this reaches the GPU process without the Android GPU sandbox enabled by default.

The cause

What version of Chrome have you found the security issue in?

147.0.7727.101 (Official Build) (64-bit)

Yes, it is related to a crash.

Choose the type of vulnerability

Memory Corruption (in a non-sandboxed process)

How would you like to be publicly acknowledged for your report?

cinzinga

View on issue tracker