0bdffe8ac0cbf343c6af220ee9a803b1f3101b84 Clamp pixel readback bounds on the renderer side
The bug at a glance
The commit message states that pixels outside the GPU-clamped read rectangle were left uninitialized in recycled shared memory and returned to the web page, where they could contain earlier image buffers, including cross-origin images. A cross-origin pixel leak to ordinary web content bypasses the same-origin policy. The rating stays high rather than critical because it is a read-only disclosure that depends on Graphite and on pool reuse.
A page that asks for a readback rectangle larger than, or offset past, the source image gets back stale bytes from a pooled shared-memory buffer. This is a cross-origin data-theft primitive with no memory corruption, which makes it attractive for stealthy information theft.
Root cause
After crrev.com/c/6372122, GraphiteImageReadPixels in the GPU process clamps the read bounds to the image. The client, RasterImplementation::ReadbackImagePixelsINTERNAL, still sized and copied shared memory for the full unclipped dst_info. Only the overlapping subset was written, and the rest of the shared memory was memcpy’d into dst_pixels unchanged. The fix intersects (src_x, src_y, w, h) with gfx::Rect(source_size) on the client, allocates shared memory only for that clipped shm_info, sends the clipped rect to the GPU, and copies into a clipped subspan of the destination. RelaxedAtomicWriteMemcpyImageRowsSkippingPadding gains separate dst/src strides to support this.
Key code
Client now clips the readback rect to the source image and sizes shared memory to match
+ gfx::Rect src_rect(src_x, src_y, dst_info.width(), dst_info.height());
+ src_rect.Intersect(gfx::Rect(source_size));
+ if (src_rect.IsEmpty()) {
...
+ SkImageInfo shm_info = dst_info.makeWH(src_rect.width(), src_rect.height());
+ GLuint shm_row_bytes = shm_info.minRowBytes();
+ GLuint shm_size = shm_info.computeByteSize(shm_row_bytes);
...
- GLuint dst_size = dst_info.computeByteSize(dst_row_bytes);
Patch walkthrough
gpu/command_buffer/client/raster_implementation.cc— This is the core fix. ReadbackImagePixelsINTERNAL takes source_size, clips the request rect to the image, allocates shared memory for the clipped size, sends clipped coordinates to the GPU, and copies only into the matching dst subspan, so unwritten shared memory is never copied to the caller. dst_pixels moves from void* to a base::span/raw_span, which adds bounds checking.gpu/command_buffer/client/implementation_base.cc— RelaxedAtomicWriteMemcpyImageRowsSkippingPadding splits stride into dst_stride and src_stride, so the tightly packed clipped shared-memory buffer can be copied into a strided destination.gpu/command_buffer/client/implementation_base.h— Signature change for the dual-stride copy helper.gpu/command_buffer/client/gles2_implementation.cc— Mechanical update to the new helper signature (both strides equal). No behavior change.components/viz/test/test_raster_interface.cc— Test RasterInterface gets the new source_size parameter.cc/paint/gpu_raster_pixeltest.cc— Pixel-test call sites pass the source image size.cc/trees/layer_tree_host_pixeltest_readback.cc— Pixel-test call sites pass the source image size.
Background
RasterImplementation::ReadbackImagePixels — Client-side (renderer) raster command-buffer API that asks the GPU process to copy a shared image’s pixels into transfer shared memory, then copies them into a caller buffer.
Graphite — Skia’s newer GPU backend; GraphiteImageReadPixels on the service side clamps read bounds to the image rather than failing.
Mapped/transfer shared memory pool — Shared memory chunks are recycled between command-buffer operations, so unwritten regions keep earlier contents.
Vulnerability window
- Reachability — Reachable from ordinary web content in a stock build wherever a web-facing readback, such as a canvas pixel read (inferred), reaches ReadbackImagePixels with a rect that extends past the source image, on configurations using the Graphite backend (inferred from the GraphiteImageReadPixels reference).
- Trigger — The read rectangle (src_x, src_y, width, height) only partly overlaps the source shared image, so the GPU process writes only the overlap and leaves the rest of the shared-memory region unwritten.
- Primitive — The renderer copies the full unclipped shared-memory region, including stale bytes from recycled buffers, into the page-visible destination, giving an uninitialized-memory read.
- Escalation — If the recycled buffer earlier held cross-origin image data, the page can recover cross-origin pixels, which bypasses same-origin protections (per the commit message). This is not code execution.
Proof of concept
Conceptually: draw or read an accelerated image-backed surface so that a readback is issued for a rectangle that hangs off the edge of the source image, after other (cross-origin) images have cycled through the shared-memory pool. Then inspect the returned pixels that lie outside the image bounds. They should be zero or transparent, but held stale data before the fix.
Exploitation
- Heap grooming — Cause cross-origin images to be decoded or uploaded so that their bytes pass through the recycled transfer shared-memory pool (inferred).
- Leak — Issue repeated off-edge readbacks and harvest the uninitialized out-of-image pixels (inferred).
- Reconstruction — Reassemble the leaked fragments into cross-origin image content (inferred).
Detection & hunting
For defenders and detection engineers:
- grep -n “ReadbackImagePixels\b” -r cc/ components/ third_party/blink/ gpu/ — Find every caller and check whether the requested rect can exceed the source shared-image size, and whether the caller zero-fills its destination.
- weggli ‘ScopedMappedMemoryPtr $m($size, …); ; RelaxedAtomicWriteMemcpy(, $src);’ — Look for client copies out of mapped memory whose size comes from the request, not from what the service actually wrote.
- Fuzz/pixel test: readback with negative or out-of-range src_x/src_y on Graphite — Assert that bytes outside the image region in the destination are untouched or zero.
Audit directions
- gpu/command_buffer/service raster_decoder ReadbackARGBImagePixels / ReadbackYUVImagePixels and GLES2 ReadPixels paths — Check for other service-side clamping that silently shortens a write while the client assumes full coverage of the shared memory it allocated.
- Async readback paths (AsyncARGBReadbackRequest, YUV readback) — Verify that the async completion copy uses the clipped shm_info size and offsets consistently with the sync path (the diff is truncated here).
- Other mapped-memory consumers (transfer buffers for GetResultAs, query results) — Look for results read back from recycled shared memory without zeroing when the service may write less than requested.