← All issues

[2] WebGPU CommandEncoder OOB Read/Write via Dimension Confusion

Severity: High | Component: WebGPU CommandEncoder (Swift) | b2c89c5

Rated High because the observable effect is a controlled OOB write into GPU buffer memory from web content via standard WebGPU APIs, and the attacker controls the offset magnitude through texture dimensions — projected with confidence 0.95 from the variable swap visible in the diff.

Swift version was wrong and did not match Objective-C++ version leading to out of bounds reads and writes. Credit to Jon Butler for finding a lot of these and writing the patch.

Source/WebGPU/WebGPU/CommandEncoder.swift (copyTextureToBuffer y-axis fix)

- var yTimesDestinationBytesPerImage = y
- guard destinationBytesPerImage <= UInt32.max else {
+ var yTimesDestinationBytesPerRow = y
+ guard destinationBytesPerRow <= UInt32.max else {
return
}
- (yTimesDestinationBytesPerImage, didOverflow) = yTimesDestinationBytesPerImage.multipliedReportingOverflow(
- by: UInt32(destinationBytesPerImage)
+ (yTimesDestinationBytesPerRow, didOverflow) = yTimesDestinationBytesPerRow.multipliedReportingOverflow(
+ by: UInt32(destinationBytesPerRow)
)

Source/WebGPU/WebGPU/CommandEncoder.swift (copyBufferToTexture z-axis fix)

var zTimesSourceBytesPerImage = z
- guard let sourceBytesPerRowU32 = UInt32(exactly: sourceBytesPerRow) else {
+ guard let sourceBytesPerImageU32 = UInt32(exactly: sourceBytesPerImage) else {
return
}
- (zTimesSourceBytesPerImage, didOverflow) = zTimesSourceBytesPerImage.multipliedReportingOverflow(by: sourceBytesPerRowU32)
+ (zTimesSourceBytesPerImage, didOverflow) = zTimesSourceBytesPerImage.multipliedReportingOverflow(by: sourceBytesPerImageU32)

Source/WebGPU/WebGPU/CommandEncoder.swift (missing overflow check)

- let sum = UInt(destinationOffset) + UInt(widthTimesBlockSize)
+ var sum = UInt(destinationOffset)
+ (sum, didOverflow) = sum.addingReportingOverflow(UInt(widthTimesBlockSize))
+ guard !didOverflow else {
+ return
+ }

The patch fixes multiple incorrect offset calculations in the Swift implementation of CommandEncoder.copyTextureToBuffer and CommandEncoder.copyBufferToTexture. The primary fix corrects the y-axis row offset: y * destinationBytesPerImage is changed to y * destinationBytesPerRow. A complementary fix in copyBufferToTexture corrects the z-axis slice offset: z * sourceBytesPerRow is changed to z * sourceBytesPerImage. An additional fix adds a missing overflow check for destinationOffset + widthTimesBlockSize, and changes a bounds-check early-return to continue (skip one row instead of aborting).

WebGPU copyTextureToBuffer and copyBufferToTexture are GPU command encoder operations that copy pixel data between GPU textures and GPU buffers. The copy is parameterized by bytesPerRow (stride between consecutive rows of texel data in the buffer), bytesPerImage (stride between consecutive depth slices, equal to bytesPerRow * rowsPerImage), and a 3D copySize (width, height, depth). The buffer offset for a given texel at position (x, y, z) is computed as: baseOffset + z * bytesPerImage + y * bytesPerRow + x * bytesPerBlock. WebKit's WebGPU implementation has both an Objective-C++ and a Swift version of these functions; this commit fixes the Swift version, which diverged from the C++ reference during the rewrite.

The root cause is a dimension variable swap in multi-axis buffer offset arithmetic. Before the fix, copyTextureToBuffer computed the per-row y-axis offset as y * destinationBytesPerImage instead of the correct y * destinationBytesPerRow. Since bytesPerImage = bytesPerRow * heightInRows, this inflated the offset by a factor of the texture height for each row iteration. For a texture with height > 1, the computed buffer offset grows far past the actual destination buffer bounds on the second row onward, producing out-of-bounds writes into the destination GPU buffer.

Symmetrically, copyBufferToTexture computed the z-axis slice offset as z * sourceBytesPerRow instead of z * sourceBytesPerImage, undercounting the slice stride and reading from incorrect (potentially OOB) source buffer positions for multi-slice copies. An additional missing overflow check on destinationOffset + widthTimesBlockSize could allow an integer overflow to bypass a subsequent bounds check.

The variable names bytesPerRow and bytesPerImage share the same numeric type (UInt32) — Swift's type system did not prevent the swap because both are plain integers. A newtype wrapper or dimensional type would have caught the confusion at compile time.

🔒

Detailed vulnerability analysis & security impact assessment

Subscribe to read more

🔒

Pattern-based audit directions for variant discovery

Subscribe to read more