← 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

+// 홀수 coded width는 canvas 기반 frame에서는 나오지 않는 값입니다. chroma plane의 폭이
+// ceil(width / 2) sample이 되고, frame이 변환되는 BGRA surface에는 row 끝 padding이 생깁니다.
+// 따라서 각 row는 width * 4 byte보다 넓어집니다.
+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()는 image, canvas, VideoFrame의 pixel을 GPUTexture로 업로드합니다. GPU 측 fast path를 사용할 수 없으면 WebKit은 CoreGraphics나 vImage를 거쳐 CPU에서 pixel을 다시 읽어 들이고, channel 순서를 swizzle합니다. 이때 stride, channel 수, loop 범위는 모두 CGImage가 보고하는 속성값을 기준으로 계산됩니다. 문제는 이 속성값이 provider가 실제로 반환하는 buffer와 일치한다는 보장이 없다는 점입니다. 한편 source로는 BitmapImage, SVGImage, PDFDocumentImage가 모두 올 수 있지만, 기존 코드는 앞의 두 가지만 가정하고 있었습니다.

이번 fix로 해당 readback 경로가 전반적으로 강화되었습니다. 패치 이전에는 처리 대상이 아닌 bits-per-component 값이 들어오면 clipTo8bitsPerChannel()이 빈 span을 남겼습니다. 그 결과 bytesPerPixel이 0으로 계산되었고, maxChannelIndex = bpp - 1이 underflow되어 SIZE_MAX가 되었습니다. 즉 pixel loop가 buffer 끝을 넘어 읽을 수 있는 상태였습니다.

수정 후에는 clip 단계에서 false를 반환하고 copy 자체가 거부됩니다. 또한 bytesPerPixel은 bits-per-pixel과 bits-per-component의 비율로 도출되도록 변경되었습니다. loop 범위 역시 CheckedSize를 이용해 provider의 data 길이와 비교 검증됩니다. 이 밖에도 PDFDocumentImage를 담은 <img>에서 발생하던 null deref, padding이 있는 row에서의 bytesPerPixel 계산 오류, alpha-info switch의 fall-through가 함께 수정되었습니다. video frame 경로에는 stride/length 검사가 추가되었습니다.

웹에서 도달 가능한 WebGPU API가, 페이지가 영향을 줄 수 있는 image 속성 때문에 out-of-bounds read와 null deref로 이어질 수 있는 상태였습니다. 이번 수정으로 metadata에서 도출한 크기 대신, 실제로 반환된 byte를 기준으로 검증된 범위를 사용하게 되었습니다.