[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
+ }
Copy-paste dimension confusion in buffer offset arithmetic — y-axis stride uses per-image size instead of per-row size, and z-axis stride uses per-row size instead of per-image size.
Patch Details
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).
Background
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.
Analysis
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.
Aaa Aaaaaaa Aaaa Aa Aaaaaaaaaaaaaaaa Aaaaaa a Aaaaaa Aaaaaaa Aaaa Aaaaaa a a Aaaaaa Aaaaa Aaaaaa a Aaaaaaaaaaa Aaaaaa Aaaaa Aaa Aaa Aaaaaaaa Aaaaa Aaaa Aaaa Aaaaaaaaaaaaaaaaaaaaaa Aaa Aa Aaa Aa a Aaaaaaaaaaaaaa Aaaa Aaa a Aaaaaa Aaaaaa Aa Aa Aaaaaa Aaaaaaaa Aa Aaa Aaaaaaa Aaaaaaa Aaaaaaa Aaaa Aaa Aaaaaaaa Aaaaaaaaa Aaaaaaa Aaa a Aaaaaaaa Aaaaa Aaaaaaa Aaaaaaaaaaaaaa a Aaaaa Aaaa Aaaaaa a Aa Aaaaaaaaaaaaaaa a Aaaaa Aaa Aaa Aaaaaa Aaaaa Aa Aaaa Aaaaaaa Aa Aaa Aaaaaaa Aaaa a a Aaaaaaaaa Aaaaaaaaa Aaaa Aaaaa Aaa Aaaa Aaa Aaaaaaaa Aaaaaaaa Aaaaaaa Aaaaaaaaaaa Aaaaaaa Aaa Aaaaaa Aaaaa Aaaaaa Aaaaaaaaaaaa Aaaaaaa Aaaa Aaa Aaa Aaaaa Aaaaaa Aaa Aaaaaaa
Aaa Aaa Aaaaaa Aaaaaa Aa Aaa Aaa Aaaaaaaa Aa Aaaaa Aaaaaaaaaa Aaa Aaaaaa Aaaaaaaaaaaaaa Aaaa Aa a Aaaaaaaa Aaa Aaaaaaaa Aa Aaaaaaaaaa Aaaa Aaa Aaaaa Aaaa a Aaaaaaaaa Aaaaaa Aaa Aaa Aaaaaaaa Aaa Aaa Aaaaaaaaaa Aaaaaaaa a Aaaa Aaaaa Aaaaa Aaaaaaa a Aaaaaaaa Aaaaaaa Aaaaaa Aaaa Aaa Aaa Aaaaaaaa
Aaaa Aaaaaaaaaaaaa Aaaaaaa Aaaaaa Aaaaaa Aa Aaa Aaa Aaaaaaaa Aa Aaaaaaaa Aaaaaaaaaa Aaaa Aaaa Aaa Aaaaaaa Aaa Aaa Aaaaaa Aaa Aaaaa Aaaa Aaaaaaaaaaaaa Aaaaaaaaaa Aaaaaa Aa Aaaaaaaaaaaaaa Aaaaaaa Aaaaaaaaaaa Aaaaaaa Aaaaa Aaaa Aaaa Aaaaa Aaaaaaaaaaa Aa Aaaaaaaaaa Aaaaaaaaa Aaaaaa Aaaaaaaa
Aaa Aaaaaa Aaaaaaa Aaa Aaaaaa Aaa Aaaaaaaa a Aaa Aa Aaaaaa a Aaaaaaaaaa a Aaaaaaaaaa Aaaaa Aa Aaa Aaaaa Aaaaaa Aaaaaaaaaaaaaa Aaaaaaa Aaa Aaaaaaaaaaaaa Aaaaaaaaaa Aaaa Aa a Aaaaaaaaa Aaaa Aaaaaaa Aaaa Aaaaaaaaaaaaaa Aaaaaaaaa Aaaaaa Aaaaaaaaaa Aa a Aaaaaa Aaaaaaaaa Aaa Aaa Aaaa Aaaaa Aaaaaaaaaaaa Aaaaaaa Aaa Aaaaa Aaaaaaaaaaaaaaaaaaa Aaaaaaaaa Aaaa Aaaaaa Aa Aaaaa Aaaaaaa Aaaaaa
Audit directions
a Aaaaaaaa Aaaaaaaaa Aaaaaaaa Aaaaaaaaa Aa Aaaaaaaaaa Aaaaaa Aaaaaa Aaaaaaaaaaa Aaaa Aaaaaaaaaaa Aaa Aaaaaaaaaaaaa Aaaaa Aaa Aaaa Aaaaaaa Aaaaa Aaaaaaaa Aaaa Aa Aaaaaaaaaaaaaa Aaaaaaaa Aaaaaaaa Aaaaa Aaaaaaa Aaaa Aaaa Aaaaaaaa Aaa Aaaaaa a a Aa Aaaaa a Aa Aaaaa Aaa Aaaaaa Aaaa Aaaaaaaaaa Aa Aaaaaaaaaaaaaaaaaaaa a Aaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaaaaaaaaaaaaa a Aa Aaaaaaaaaaaa Aaaaaaaaa Aaaa Aaaaaa Aaaaaaaaaaaaaaa Aaa Aaaaaaaaaaa Aaa Aaaaaa Aaaaaaaaaaaaaaa Aaa Aaaaaaaaaaaaaa Aaaa Aaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaa Aa Aaaaaaaaaaaaaaaaaaaa Aaa Aaaaaa Aaaa Aaaaaaaaaaaa Aaaaaaa Aaa Aaaaa
a Aaaaaaaa Aaaaa Aaaaaaaaaaaaaaaa Aaaaaaaaaa Aaaa Aaa Aaaaaaaaaa Aaa Aaaaaa Aaaaaaaaaa Aaaaaa Aaa Aaaaa Aaaaaaa Aaaa Aaa Aaaaa Aaaaaaaaaaaaa Aaaaaaaaa Aaaaa Aaaaa Aaaaa Aaaaa Aa Aaaaaaaaaaaaaaaaaaaaa Aa Aaaaaaa Aaa Aaaaaaaaaa Aaaaa Aaaaaaa Aaaaa Aaa Aaaaaaaaaaaa Aa Aaa Aaaa Aaaaaaaaaa Aaaaa Aa Aaaaaa Aaaaaa Aaaaaaaaaaaaa Aaaaaaaaaa Aaaaaaa Aaa Aaaaaaaa Aaaaaaaa a Aaaaa Aaa Aaa Aaaaa Aaaa Aaaaa Aa Aaaaaaaaaaaaa Aaaaaaa
a Aaaaa Aaa Aaaaaaaa Aaaaa Aaaa Aaa Aaaaa Aaa Aaaaaaaaaaaaaaaaaa a Aaaaaaaaaaaaaaaaaaaaa Aaaaaa Aaaaaaaaaaaaaaaaaaaa Aaa Aaaaa Aaaa Aaaaaaaaaa Aaaaaaaaaa Aaaaa Aaaa Aa Aaaaaa Aa Aaaa Aaaaaa Aaaa Aaaa Aaaaaaaaaaaaaaaaaaaaaaaaa a Aaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaa Aaa Aaaaaaaaa Aaaaaaaaaa Aa Aaaaaaaaaaaaaaaaaaa Aaaa Aaaaaaaaaa Aaaaa Aaaaaa Aaaaaaaaaa Aaaaaa Aaaaaa Aaa Aaaaaaa Aaaaaaaaa