CVE-2026-9872
Overview
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_LENGTHorGL_PACK_ALIGNMENT,glReadPixelswrites each row at a larger stride than the image width, so the total bytes touched exceed a tightly-packedwidth*height*bppbuffer. - `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
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.
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
- Set hostile pixel-store state
The renderer issues
glPixelStoreito set a largeGL_PACK_ROW_LENGTH(orGL_PACK_ALIGNMENT) so the pack stride exceeds the image width. - Invoke the copy-texture readback
It triggers the copy-texture path that runs
DoReadbackAndTexImage, allocating an internal buffer sized for a tightly-packed image. - Overflow on readback
glReadPixelsuses 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
(inferred). The precondition is the ability to set pixel-store state and drive the copy-texture code path that reaches DoReadbackAndTexImage.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifgpu/command_buffer/service/gles2_cmd_copy_texture_chromium.cc |
modified | |
CopyTextureResourceManagerImplgpu/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 assumptionsAudit internal readback/upload helpers that size a buffer as
width*height*bppand callglReadPixels/glTexImage2Dwithout first forcingROW_LENGTH/IMAGE_HEIGHT/ALIGNMENTto their packed defaults. - Client pixel-store state leaking into service codeLook for GPU-service paths that call GL pixel-transfer functions while the client’s
glPixelStoreistate is still active, which lets a renderer control driver-side stride/alignment. - Missing state restore on temporary changesReview 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.
Patch
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
Original Bug Report
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?
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
- Symbolized ASan stack trace (from Android content shell) attached as symbolized_stack_trace.txt
- Chrome Stable tombstone is from com.android.chrome:privileged_process* (CrGpuMain), signal 11 SEGV_ACCERR (write), with the fault in ReadPixels/glReadPixels
- chrome://crashes
0e2b36268c1779c4or311f8eaf5e9c3abb
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)
Is the security issue related to a crash?
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