CVE-2026-16393
Overview
Background
- WebGPU snapshot readback
- the GPU/parent-process operation that copies a rendered
WebGPUtexture back into a shared-memory buffer so content can read the pixels. - Stride
- the byte width of one image row including any trailing padding, which is normally
>=the actual pixel bytes (width * 4). - `ipc::Shmem`
- a shared-memory region used to hand image data across the process boundary between the GPU/parent process and content.
- `MOZ_RELEASE_ASSERT`
- a Firefox assertion that aborts in all builds (including release) when its condition is false, turning a memory-safety violation into a controlled crash.
Root Cause Analysis
Every affected row-copy loop computed min_stride = std::min(src_stride, aDestStride) and did memcpy(dst, src, min_stride) per row, an approach whose own comment admitted it “might still copy some padding bytes.”
The invariant that was violated is that only the meaningful pixel bytes (width * 4) should cross the process boundary and that every destination byte must be explicitly initialized. When src_stride exceeded width * 4 (source row padding) and the destination stride was at least as wide, memcpy pulled adjacent source padding into the destination; conversely, when the destination row was wider than what was copied, the trailing destination bytes were left uninitialized, exposing stale shared-memory contents.
The fix recomputes the true bytesPerRow as width * 4, copies exactly that many bytes, and memsets the remaining destination padding to zero, so no source padding leaks and no destination byte is left stale. It additionally guards both strides with MOZ_RELEASE_ASSERT(src_stride >= bytesPerRow) and MOZ_RELEASE_ASSERT(aDestStride >= bytesPerRow), converting any boundary miscalculation into a safe abort rather than an out-of-bounds access.
width * 4), which both copied source padding and left destination padding uninitialized; the fix pins the copy to exactly bytesPerRow and zero-fills the rest.Attack Path
- Trigger a snapshot
A malicious page uses
WebGPUto render into a texture and requests a readback/snapshot that routes through one of theGetSnapshotorReadback*Callbackcopy loops in the parent process. - Exploit stride mismatch
The source texture’s
src_strideincludes padding beyondwidth * 4, or the destination stride differs, somin_strideno longer equals the real pixel width. - Copy or expose padding
The per-row
memcpyofmin_stridebytes drags neighboring source padding into the shared buffer, and/or leaves trailing destination bytes uninitialized with stale contents. - Read back the leaked bytes The content process reads the shared-memory image (e.g. via canvas/pixel readback) and recovers the padding bytes as image data.
Impact Assessment
WebGPU rendering and a snapshot/readback, and that a stride padding mismatch exists between source and destination buffers. No memory corruption or code execution is indicated by the diff; the assertions further ensure malformed strides abort instead of overflowing.Changed Functions
| Function | Change | Notes |
|---|---|---|
fordom/webgpu/SharedTextureD3D11.cpp |
modified | |
ifdom/webgpu/SharedTextureD3D11.cpp |
modified | |
fordom/webgpu/SharedTextureDMABuf.cpp |
modified | |
ifdom/webgpu/SharedTextureDMABuf.cpp |
modified | |
fordom/webgpu/SharedTextureMacIOSurface.cpp |
modified | |
ifdom/webgpu/SharedTextureMacIOSurface.cpp |
modified | |
fordom/webgpu/ipc/WebGPUParent.cpp |
modified | |
ifdom/webgpu/ipc/WebGPUParent.cpp |
modified | |
forgfx/layers/RemoteTextureMap.cpp |
modified | |
ifgfx/layers/RemoteTextureMap.cpp |
modified |
Files Changed
dom/webgpu/SharedTextureD3D11.cppdom/webgpu/SharedTextureDMABuf.cppdom/webgpu/SharedTextureMacIOSurface.cppdom/webgpu/ipc/WebGPUParent.cppgfx/layers/RemoteTextureMap.cpp
Audit Directions
- Stride-minimum copiesFlag any
std::min(src_stride, dst_stride)used as amemcpylength in image row loops; the length should be the real payload width (width * bytesPerPixel), not a stride. - Uninitialized destination paddingWhen a destination row is wider than the copied pixels, ensure the trailing bytes are explicitly
memsetto zero before the buffer crosses a process or trust boundary. - Cross-process pixel buffersAudit other
GetSnapshot/readback paths that write intoipc::Shmemto confirm strides are release-asserted>= width * bytesPerPixelso malformed dimensions abort instead of over-reading.
Patch
diff --git a/dom/webgpu/SharedTextureD3D11.cpp b/dom/webgpu/SharedTextureD3D11.cpp
index e5d27e77898..87619a1c747 100644
--- a/dom/webgpu/SharedTextureD3D11.cpp
+++ b/dom/webgpu/SharedTextureD3D11.cpp
@@ -206,11 +206,15 @@ void SharedTextureD3D11::GetSnapshot(const ipc::Shmem& aDestShmem,
uint8_t* dst = aDestShmem.get<uint8_t>();
const size_t src_stride = static_cast<size_t>(map.RowPitch);
- // note that this might still copy some padding bytes
- const size_t min_stride = std::min(src_stride, aDestStride);
+ const size_t bytesPerRow = static_cast<size_t>(mWidth) * 4;
+ MOZ_RELEASE_ASSERT(src_stride >= bytesPerRow);
+ MOZ_RELEASE_ASSERT(aDestStride >= bytesPerRow);
for (uint32_t y = 0; y < mHeight; y++) {
- memcpy(dst, src, min_stride);
+ memcpy(dst, src, bytesPerRow);
+ if (bytesPerRow < aDestStride) {
+ memset(dst + bytesPerRow, 0, aDestStride - bytesPerRow);
+ }
src += src_stride;
dst += aDestStride;
}
diff --git a/dom/webgpu/SharedTextureDMABuf.cpp b/dom/webgpu/SharedTextureDMABuf.cpp
index bdd1c84886c..7ce3c967808 100644
--- a/dom/webgpu/SharedTextureDMABuf.cpp
+++ b/dom/webgpu/SharedTextureDMABuf.cpp
@@ -146,11 +146,15 @@ void SharedTextureDMABuf::GetSnapshot(const ipc::Shmem& aDestShmem,
uint8_t* dst = aDestShmem.get<uint8_t>();
const size_t src_stride = static_cast<size_t>(map.GetStride());
- // note that this might still copy some padding bytes
- const size_t min_stride = std::min(src_stride, aDestStride);
+ const size_t bytesPerRow = static_cast<size_t>(mWidth) * 4;
+ MOZ_RELEASE_ASSERT(src_stride >= bytesPerRow);
+ MOZ_RELEASE_ASSERT(aDestStride >= bytesPerRow);
for (uint32_t y = 0; y < mHeight; y++) {
- memcpy(dst, src, min_stride);
+ memcpy(dst, src, bytesPerRow);
+ if (bytesPerRow < aDestStride) {
+ memset(dst + bytesPerRow, 0, aDestStride - bytesPerRow);
+ }
src += src_stride;
dst += aDestStride;
}
diff --git a/dom/webgpu/SharedTextureMacIOSurface.cpp b/dom/webgpu/SharedTextureMacIOSurface.cpp
index 1dd14953558..874449a0f94 100644
--- a/dom/webgpu/SharedTextureMacIOSurface.cpp
+++ b/dom/webgpu/SharedTextureMacIOSurface.cpp
@@ -85,16 +85,20 @@ void SharedTextureMacIOSurface::GetSnapshot(const ipc::Shmem& aDestShmem,
return;
}
- const size_t bytesPerRow = mSurface->GetBytesPerRow();
+ const size_t src_stride = mSurface->GetBytesPerRow();
uint8_t* src = (uint8_t*)mSurface->GetBaseAddress();
uint8_t* dst = aDestShmem.get<uint8_t>();
- // note that this might still copy some padding bytes
- const size_t min_stride = std::min(bytesPerRow, aDestStride);
+ const size_t bytesPerRow = static_cast<size_t>(mWidth) * 4;
+ MOZ_RELEASE_ASSERT(src_stride >= bytesPerRow);
+ MOZ_RELEASE_ASSERT(aDestStride >= bytesPerRow);
for (uint32_t y = 0; y < mHeight; y++) {
- memcpy(dst, src, min_stride);
- src += bytesPerRow;
+ memcpy(dst, src, bytesPerRow);
+ if (bytesPerRow < aDestStride) {
+ memset(dst + bytesPerRow, 0, aDestStride - bytesPerRow);
+ }
+ src += src_stride;
dst += aDestStride;
}
diff --git a/dom/webgpu/ipc/WebGPUParent.cpp b/dom/webgpu/ipc/WebGPUParent.cpp
index 556757ee03b..660ef6a6949 100644
--- a/dom/webgpu/ipc/WebGPUParent.cpp
+++ b/dom/webgpu/ipc/WebGPUParent.cpp
@@ -1078,19 +1078,25 @@ static void ReadbackPresentCallback(uint8_t* userdata,
uint8_t* src = mapped.ptr;
uint8_t* dst = mappedData.data;
- const uint32_t dst_stride = mappedData.stride;
+ const size_t dst_stride = static_cast<size_t>(mappedData.stride);
// `mappedData.stride` is computed via
// `ImageDataSerializer::ComputeRGBStride` and returns 0 if it overflows
MOZ_RELEASE_ASSERT(dst_stride != 0);
- // note that this might still copy some padding bytes
- const uint32_t min_stride = std::min(data->mBufferStride, dst_stride);
+ const size_t src_stride = static_cast<size_t>(data->mBufferStride);
+ const size_t bytesPerRow =
+ static_cast<size_t>(data->mDesc.size().width) * 4;
+ MOZ_RELEASE_ASSERT(src_stride >= bytesPerRow);
+ MOZ_RELEASE_ASSERT(dst_stride >= bytesPerRow);
// The height is in bounds for both buffers since we just requested a new
// destination buffer with the same height of the source.
for (auto row = 0; row < size.height; ++row) {
- memcpy(dst, src, min_stride);
- src += data->mBufferStride;
+ memcpy(dst, src, bytesPerRow);
+ if (bytesPerRow < dst_stride) {
+ memset(dst + bytesPerRow, 0, dst_stride - bytesPerRow);
+ }
+ src += src_stride;
dst += dst_stride;
}
req->mRemoteTextureOwner->PushTexture(req->mTextureId, req->mOwnerId,
@@ -1176,13 +1182,17 @@ static void ReadbackSnapshotCallback(uint8_t* userdata,
uint8_t* dst = req->mDestShmem.get<uint8_t>();
const size_t src_stride = static_cast<size_t>(data->mBufferStride);
- // note that this might still copy some padding bytes
- const size_t min_stride = std::min(src_stride, req->mDestStride);
+ const size_t bytesPerRow = static_cast<size_t>(data->mDesc.size().width) * 4;
+ MOZ_RELEASE_ASSERT(src_stride >= bytesPerRow);
+ MOZ_RELEASE_ASSERT(req->mDestStride >= bytesPerRow);
// The height is in bounds for both buffers since we previously created a new
// destination buffer with the same height of the source.
for (auto row = 0; row < data->mDesc.size().height; ++row) {
- memcpy(dst, src, min_stride);
+ memcpy(dst, src, bytesPerRow);
+ if (bytesPerRow < req->mDestStride) {
+ memset(dst + bytesPerRow, 0, req->mDestStride - bytesPerRow);
+ }
src += src_stride;
dst += req->mDestStride;
}
diff --git a/gfx/layers/RemoteTextureMap.cpp b/gfx/layers/RemoteTextureMap.cpp
index 3635a685505..ae65668d34f 100644
--- a/gfx/layers/RemoteTextureMap.cpp
+++ b/gfx/layers/RemoteTextureMap.cpp
@@ -557,15 +557,20 @@ void RemoteTextureMap::GetLatestBufferSnapshot(
uint8_t* src = bufferTextureHost->GetBuffer();
uint8_t* dst = aDestShmem.get<uint8_t>();
- const Maybe<int32_t> src_stride = ImageDataSerializer::GetRGBStride(
+ const Maybe<int32_t> maybe_src_stride = ImageDataSerializer::GetRGBStride(
bufferTextureHost->GetBufferDescriptor());
- MOZ_RELEASE_ASSERT(src_stride.isSome());
- // note that this might still copy some padding bytes
- const size_t min_stride = std::min(size_t(src_stride.value()), aDestStride);
+ MOZ_RELEASE_ASSERT(maybe_src_stride.isSome());
+ const size_t src_stride = static_cast<size_t>(maybe_src_stride.value());
+ const size_t bytesPerRow = static_cast<size_t>(src_size.width) * 4;
+ MOZ_RELEASE_ASSERT(src_stride >= bytesPerRow);
+ MOZ_RELEASE_ASSERT(aDestStride >= bytesPerRow);
for (int y = 0; y < src_size.height; y++) {
- memcpy(dst, src, min_stride);
- src += src_stride.value();
+ memcpy(dst, src, bytesPerRow);
+ if (bytesPerRow < aDestStride) {
+ memset(dst + bytesPerRow, 0, aDestStride - bytesPerRow);
+ }
+ src += src_stride;
dst += aDestStride;
}
}