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
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 검사가 추가되었습니다.
Significance
웹에서 도달 가능한 WebGPU API가, 페이지가 영향을 줄 수 있는 image 속성 때문에 out-of-bounds read와 null deref로 이어질 수 있는 상태였습니다. 이번 수정으로 metadata에서 도출한 크기 대신, 실제로 반환된 byte를 기준으로 검증된 범위를 사용하게 되었습니다.
Audit directions
- 실제 buffer 길이보다 metadata를 신뢰하는 패턴. provider 길이 대신
CGImageGet*값을 신뢰하는 패턴이 인접한 업로드 경로에도 있는지 살펴볼 필요가 있습니다. 대상은 canvas,ImageBitmap, WebGLtexImage업로드입니다. - 다뤄지지 않은 pixel format. fast path나 channel-mapping lambda에 도달하는 16-bit, float, indexed, CMYK bits-per-component 조합을 직접 시험해 볼 필요가 있습니다. 이때 새 bounds check가 origin offset과 copy extent를 포함한 모든 read를 빠짐없이 커버하는지 확인해야 합니다.
- Video frame stride. 모든
VideoFramepixel format에 대해 32bpp 가정과 stride 가정을 검증해야 합니다. 홀수 width frame과 CoreVideo가 padding을 넣은 frame도 여기에 포함됩니다. - 검사 없는
Imagedowncast.PDFDocumentImage나 Bitmap이 아닌 다른Imagesubclass를 unchecked downcast로 읽는 코드가 남아 있다면, 동일한 null deref 형태에 해당합니다.