← All reports

MallocSpan crash in GPUQueue::copyExternalImageToTexture

In WebGPU's image upload, zero bytes per pixel meant no upper bound at all.

Component: WebGPU | 9197450

LayoutTests/fast/webgpu/copy-external-image-video-frame-odd-width.html

+// An odd coded width is what a canvas backed frame never produces: it makes the chroma planes
+// ceil(width / 2) samples wide, and it gives the BGRA surface the frame is conformed to end of row
+// padding, so the rows are wider than width * 4 bytes.
+const codedWidth = 65;
+const codedHeight = 64;
...
+ return new VideoFrame(planes, { format: 'I420', codedWidth, codedHeight, timestamp: 0 });
...
+ device.queue.copyExternalImageToTexture({ source: frame, origin: { x: originX, y: originY } }, { texture }, [width, height]);

copyExternalImageToTexture() uploads pixels from an image, canvas or VideoFrame into a GPUTexture. Without a GPU-side fast path, WebKit reads pixels back on the CPU through CoreGraphics or vImage and swizzles channels, computing strides, channel counts and loop bounds from CGImage-reported properties that are not guaranteed to agree with the buffer the provider returns. The source can be a BitmapImage, SVGImage or PDFDocumentImage; the code assumed only the first two.

The fix hardens that readback path. An unhandled bits-per-component left clipTo8bitsPerChannel() with an empty span, so bytesPerPixel computed to 0 and maxChannelIndex = bpp - 1 underflowed to SIZE_MAX, letting the pixel loop read past the buffer. Now the clip returns false and the copy is rejected, bytesPerPixel derives from bits-per-pixel over bits-per-component, and loop bounds are checked against the provider's data length with CheckedSize. It also fixes a null deref on <img> holding a PDFDocumentImage, a bytesPerPixel miscalculation on padded rows, an alpha-info switch fall-through, and stride/length checks on the video frame path.

A web-reachable WebGPU API could be driven into an out-of-bounds read and a null deref by image properties the page influences. Metadata-derived sizes are replaced with bounds checked against the bytes actually returned.