Skip to content

Commit 3d0fe3a

Browse files
andyleisersonjimblandy
authored andcommitted
Check the math for buffer-texture copies
1 parent 12591e4 commit 3d0fe3a

File tree

3 files changed

+194
-42
lines changed

3 files changed

+194
-42
lines changed

cts_runner/test.lst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ webgpu:api,validation,encoding,programmable,pipeline_bind_group_compat:bind_grou
4343
webgpu:api,validation,encoding,programmable,pipeline_bind_group_compat:bind_groups_and_pipeline_layout_mismatch:encoderType="render%20pass";*
4444
webgpu:api,validation,encoding,programmable,pipeline_bind_group_compat:buffer_binding,render_pipeline:*
4545
webgpu:api,validation,encoding,programmable,pipeline_bind_group_compat:sampler_binding,render_pipeline:*
46+
webgpu:api,validation,image_copy,layout_related:copy_end_overflows_u64:*
4647
webgpu:api,validation,image_copy,texture_related:format:dimension="1d";*
4748
webgpu:api,validation,queue,submit:command_buffer,device_mismatch:*
4849
webgpu:api,validation,queue,submit:command_buffer,duplicate_buffers:*

wgpu-core/src/command/transfer.rs

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ use arrayvec::ArrayVec;
44
use thiserror::Error;
55
use wgt::{
66
error::{ErrorType, WebGpuError},
7-
BufferAddress, BufferUsages, Extent3d, TextureSelector, TextureUsages,
7+
BufferAddress, BufferTextureCopyInfoError, BufferUsages, Extent3d, TextureSelector,
8+
TextureUsages,
89
};
910

1011
#[cfg(feature = "trace")]
@@ -95,6 +96,8 @@ pub enum TransferError {
9596
InvalidBytesPerRow,
9697
#[error("Number of rows per image is invalid")]
9798
InvalidRowsPerImage,
99+
#[error("Overflow while computing the size of the copy")]
100+
SizeOverflow,
98101
#[error("Copy source aspects must refer to all aspects of the source texture format")]
99102
CopySrcMissingAspects,
100103
#[error(
@@ -164,6 +167,7 @@ impl WebGpuError for TransferError {
164167
| Self::UnspecifiedRowsPerImage
165168
| Self::InvalidBytesPerRow
166169
| Self::InvalidRowsPerImage
170+
| Self::SizeOverflow
167171
| Self::CopySrcMissingAspects
168172
| Self::CopyDstMissingAspects
169173
| Self::CopyAspectNotOne
@@ -181,6 +185,18 @@ impl WebGpuError for TransferError {
181185
}
182186
}
183187

188+
impl From<BufferTextureCopyInfoError> for TransferError {
189+
fn from(value: BufferTextureCopyInfoError) -> Self {
190+
match value {
191+
BufferTextureCopyInfoError::InvalidBytesPerRow => Self::InvalidBytesPerRow,
192+
BufferTextureCopyInfoError::InvalidRowsPerImage => Self::InvalidRowsPerImage,
193+
BufferTextureCopyInfoError::ImageStrideOverflow
194+
| BufferTextureCopyInfoError::ImageBytesOverflow(_)
195+
| BufferTextureCopyInfoError::ArraySizeOverflow(_) => Self::SizeOverflow,
196+
}
197+
}
198+
}
199+
184200
pub(crate) fn extract_texture_selector<T>(
185201
copy_texture: &wgt::TexelCopyTextureInfo<T>,
186202
copy_size: &Extent3d,
@@ -254,7 +270,7 @@ pub(crate) fn validate_linear_texture_data(
254270
width_blocks: _,
255271
height_blocks,
256272

257-
row_bytes_dense,
273+
row_bytes_dense: _,
258274
row_stride_bytes,
259275

260276
image_stride_rows: _,
@@ -264,7 +280,7 @@ pub(crate) fn validate_linear_texture_data(
264280
image_bytes_dense: _,
265281

266282
bytes_in_copy,
267-
} = layout.get_buffer_texture_copy_info(format, aspect, copy_size);
283+
} = layout.get_buffer_texture_copy_info(format, aspect, copy_size)?;
268284

269285
if copy_width % block_width_texels != 0 {
270286
return Err(TransferError::UnalignedCopyWidth);
@@ -276,21 +292,15 @@ pub(crate) fn validate_linear_texture_data(
276292
let requires_multiple_rows = depth_or_array_layers > 1 || height_blocks > 1;
277293
let requires_multiple_images = depth_or_array_layers > 1;
278294

279-
if let Some(raw_bytes_per_row) = layout.bytes_per_row {
280-
let raw_bytes_per_row = raw_bytes_per_row as BufferAddress;
281-
if raw_bytes_per_row < row_bytes_dense {
282-
return Err(TransferError::InvalidBytesPerRow);
283-
}
284-
} else if requires_multiple_rows {
295+
// `get_buffer_texture_copy_info()` already proceeded with defaults if these
296+
// were not specified, and ensured that the values satisfy the minima if
297+
// they were, but now we enforce the WebGPU requirement that they be
298+
// specified any time they apply.
299+
if layout.bytes_per_row.is_none() && requires_multiple_rows {
285300
return Err(TransferError::UnspecifiedBytesPerRow);
286301
}
287302

288-
if let Some(raw_rows_per_image) = layout.rows_per_image {
289-
let raw_rows_per_image = raw_rows_per_image as BufferAddress;
290-
if raw_rows_per_image < height_blocks {
291-
return Err(TransferError::InvalidRowsPerImage);
292-
}
293-
} else if requires_multiple_images {
303+
if layout.rows_per_image.is_none() && requires_multiple_images {
294304
return Err(TransferError::UnspecifiedRowsPerImage);
295305
};
296306

wgpu-types/src/transfers.rs

Lines changed: 168 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,46 +2,81 @@ use crate::{BufferAddress, Extent3d, TexelCopyBufferLayout, TextureAspect, Textu
22

33
impl TexelCopyBufferLayout {
44
/// Extract a variety of information about the given copy operation.
5+
///
6+
/// Returns an error if the size of the copy overflows a `u64`, or if the arguments are
7+
/// not valid in conjunction with the `bytes_per_row` or `rows_per_image` parameters in
8+
/// `self`.
9+
///
10+
/// This is public for use by `wgpu-core` and `wgpu-hal`, it is not a stable API.
11+
///
12+
/// Although WebGPU requires that `bytes_per_row` and `rows_per_image` be specified in
13+
/// cases where they apply, we are more lenient here (although it's not clear if that is
14+
/// necessary). Our caller, `validate_linear_texture_data`, enforces this and other
15+
/// WebGPU requirements on the copy parameters that we do not check here.
16+
#[doc(hidden)]
517
#[inline(always)]
618
pub fn get_buffer_texture_copy_info(
719
&self,
820
format: TextureFormat,
921
aspect: TextureAspect,
1022
copy_size: &Extent3d,
11-
) -> BufferTextureCopyInfo {
12-
// Convert all inputs to BufferAddress (u64) to avoid some of the overflow issues
13-
// Note: u64 is not always enough to prevent overflow, especially when multiplying
14-
// something with a potentially large depth value, so it is preferable to validate
15-
// the copy size before calling this function (for example via `validate_texture_copy_range`).
16-
let copy_width = copy_size.width as BufferAddress;
17-
let copy_height = copy_size.height as BufferAddress;
18-
let depth_or_array_layers = copy_size.depth_or_array_layers as BufferAddress;
19-
20-
let block_size_bytes = format.block_copy_size(Some(aspect)).unwrap() as BufferAddress;
23+
) -> Result<BufferTextureCopyInfo, Error> {
24+
let copy_width = BufferAddress::from(copy_size.width);
25+
let copy_height = BufferAddress::from(copy_size.height);
26+
let depth_or_array_layers = BufferAddress::from(copy_size.depth_or_array_layers);
27+
28+
let block_size_bytes = BufferAddress::from(format.block_copy_size(Some(aspect)).unwrap());
2129
let (block_width, block_height) = format.block_dimensions();
22-
let block_width_texels = block_width as BufferAddress;
23-
let block_height_texels = block_height as BufferAddress;
30+
let block_width_texels = BufferAddress::from(block_width);
31+
let block_height_texels = BufferAddress::from(block_height);
2432

2533
let width_blocks = copy_width.div_ceil(block_width_texels);
2634
let height_blocks = copy_height.div_ceil(block_height_texels);
2735

36+
// The spec calls this bytesInLastRow.
2837
let row_bytes_dense = width_blocks * block_size_bytes;
29-
30-
let row_stride_bytes = self.bytes_per_row.map_or(row_bytes_dense, u64::from);
31-
let image_stride_rows = self.rows_per_image.map_or(height_blocks, u64::from);
32-
33-
let image_stride_bytes = row_stride_bytes * image_stride_rows;
38+
let row_stride_bytes = match self.bytes_per_row.map(BufferAddress::from) {
39+
Some(bytes_per_row) if bytes_per_row >= row_bytes_dense => bytes_per_row,
40+
Some(_) => return Err(Error::InvalidBytesPerRow),
41+
None => row_bytes_dense,
42+
};
3443

3544
let image_rows_dense = height_blocks;
36-
let image_bytes_dense =
37-
image_rows_dense.saturating_sub(1) * row_stride_bytes + row_bytes_dense;
45+
let image_stride_rows = match self.rows_per_image.map(BufferAddress::from) {
46+
Some(rows_per_image) if rows_per_image >= image_rows_dense => rows_per_image,
47+
Some(_) => return Err(Error::InvalidRowsPerImage),
48+
None => image_rows_dense,
49+
};
3850

39-
let mut bytes_in_copy = image_stride_bytes * depth_or_array_layers.saturating_sub(1);
40-
if height_blocks > 0 {
41-
bytes_in_copy += row_stride_bytes * (height_blocks - 1) + row_bytes_dense;
42-
}
51+
let image_bytes_dense = match image_rows_dense.checked_sub(1) {
52+
Some(rows_minus_one) => rows_minus_one
53+
.checked_mul(row_stride_bytes)
54+
.ok_or(Error::ImageBytesOverflow(false))?
55+
.checked_add(row_bytes_dense)
56+
.ok_or(Error::ImageBytesOverflow(true))?,
57+
None => 0,
58+
};
59+
60+
// It is possible that `image_stride_bytes` overflows, but the actual
61+
// copy size does not, when the copy only has a single layer and
62+
// `image_size_bytes` is not used. We don't worry about handling this
63+
// gracefully because WebGPU texture size limits should keep things out
64+
// of this realm entirely.
65+
let image_stride_bytes = row_stride_bytes
66+
.checked_mul(image_stride_rows)
67+
.ok_or(Error::ImageStrideOverflow)?;
68+
69+
let bytes_in_copy = if depth_or_array_layers <= 1 {
70+
depth_or_array_layers * image_bytes_dense
71+
} else {
72+
(depth_or_array_layers - 1)
73+
.checked_mul(image_stride_bytes)
74+
.ok_or(Error::ArraySizeOverflow(false))?
75+
.checked_add(image_bytes_dense)
76+
.ok_or(Error::ArraySizeOverflow(true))?
77+
};
4378

44-
BufferTextureCopyInfo {
79+
Ok(BufferTextureCopyInfo {
4580
copy_width,
4681
copy_height,
4782
depth_or_array_layers,
@@ -65,7 +100,7 @@ impl TexelCopyBufferLayout {
65100
image_bytes_dense,
66101

67102
bytes_in_copy,
68-
}
103+
})
69104
}
70105
}
71106

@@ -127,16 +162,55 @@ pub struct BufferTextureCopyInfo {
127162
pub bytes_in_copy: u64,
128163
}
129164

165+
/// Errors that can occur while populating `BufferTextureCopyInfo`.
166+
//
167+
// We use the additional detail provided by these errors (over wgpu-core's
168+
// `TransferError`) to improve the reliability of the tests in this module. It
169+
// doesn't seem worth plumbing them upwards, because at the API level it
170+
// shouldn't be possible to exceed them without exceeding the WebGPU limits on
171+
// texture dimension. But the WebGPU limits are not currently enforced, so we
172+
// have to do something here to protect against overflows.
173+
//
174+
// Even when the WebGPU limits are enforced, it may still be useful to keep the
175+
// checks here as a failsafe if the correctness of the primary limit enforcement
176+
// is not immediately apparent.
177+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
178+
pub enum BufferTextureCopyInfoError {
179+
/// The `bytes_per_row` is too small for the texture width.
180+
InvalidBytesPerRow,
181+
/// The `rows_per_image` is too small for the texture height.
182+
InvalidRowsPerImage,
183+
/// The image stride overflows a `u64`.
184+
ImageStrideOverflow,
185+
/// The last-layer byte size overflows a `u64`.
186+
///
187+
/// The bool value indicates whether the multiplication (false) or the
188+
/// addition (true) overflowed.
189+
ImageBytesOverflow(bool),
190+
/// The total size of the copy overflows a `u64`.
191+
///
192+
/// The bool value indicates whether the multiplication (false) or the
193+
/// addition (true) overflowed.
194+
ArraySizeOverflow(bool),
195+
}
196+
type Error = BufferTextureCopyInfoError;
197+
130198
#[cfg(test)]
131199
mod tests {
132200
use super::*;
133201

202+
#[derive(Clone)]
134203
struct LTDTest {
135204
layout: TexelCopyBufferLayout,
136205
format: TextureFormat,
137206
aspect: TextureAspect,
138207
copy_size: Extent3d,
139208
expected_result: BufferTextureCopyInfo,
209+
// Normally a Result<BufferTextureCopyInfo, Error> would be make sense,
210+
// but since the existing tests were written to mutate
211+
// `LTDTest.expected_result`, keeping this separate avoids a bunch of
212+
// `unwrap`s.
213+
expected_error: Option<Error>,
140214
}
141215

142216
impl LTDTest {
@@ -145,7 +219,11 @@ mod tests {
145219
let linear_texture_data =
146220
self.layout
147221
.get_buffer_texture_copy_info(self.format, self.aspect, &self.copy_size);
148-
assert_eq!(linear_texture_data, self.expected_result);
222+
let expected = match self.expected_error {
223+
Some(err) => Err(err),
224+
None => Ok(self.expected_result),
225+
};
226+
assert_eq!(linear_texture_data, expected);
149227
}
150228
}
151229

@@ -182,6 +260,7 @@ mod tests {
182260
image_bytes_dense: 16,
183261
bytes_in_copy: 16,
184262
},
263+
expected_error: None,
185264
};
186265

187266
test.run();
@@ -211,7 +290,7 @@ mod tests {
211290

212291
#[test]
213292
fn linear_texture_data_2d_3d_copy() {
214-
let mut test = LTDTest {
293+
let template = LTDTest {
215294
layout: TexelCopyBufferLayout {
216295
offset: 0,
217296
bytes_per_row: None,
@@ -242,8 +321,10 @@ mod tests {
242321
image_bytes_dense: 4 * 7 * 12,
243322
bytes_in_copy: 4 * 7 * 12,
244323
},
324+
expected_error: None,
245325
};
246326

327+
let mut test = template.clone();
247328
test.run();
248329

249330
// Changing bytes_per_row changes a number of other properties.
@@ -252,12 +333,71 @@ mod tests {
252333
test.expected_result.image_stride_bytes = 48 * 12;
253334
test.expected_result.image_bytes_dense = 48 * 11 + (4 * 7);
254335
test.expected_result.bytes_in_copy = 48 * 11 + (4 * 7);
336+
test.run();
255337

256338
// Making this a 3D copy only changes the depth_or_array_layers and the bytes_in_copy.
257339
test.copy_size.depth_or_array_layers = 4;
258340
test.expected_result.depth_or_array_layers = 4;
259341
test.expected_result.bytes_in_copy = 48 * 12 * 3 + 48 * 11 + (4 * 7); // 4 layers
342+
test.run();
343+
344+
// Changing rows_per_image
345+
test.layout.rows_per_image = Some(20);
346+
test.expected_result.image_stride_rows = 20;
347+
test.expected_result.image_stride_bytes = 20 * test.expected_result.row_stride_bytes;
348+
test.expected_result.bytes_in_copy = 48 * 20 * 3 + 48 * 11 + (4 * 7); // 4 layers
349+
test.run();
350+
351+
// Invalid because the row stride is too small.
352+
let mut test = template.clone();
353+
test.layout.bytes_per_row = Some(20);
354+
test.expected_error = Some(Error::InvalidBytesPerRow);
355+
test.run();
356+
357+
// Invalid because the image stride is too small.
358+
let mut test = template.clone();
359+
test.layout.rows_per_image = Some(8);
360+
test.expected_error = Some(Error::InvalidRowsPerImage);
361+
test.run();
362+
363+
// Invalid because width * height * texel_size_bytes overflows.
364+
let mut test = template.clone();
365+
test.copy_size.width = u32::MAX;
366+
test.copy_size.height = u32::MAX;
367+
test.expected_error = Some(Error::ImageBytesOverflow(false));
368+
test.run();
369+
370+
// Invalid because the addition of row_bytes_dense overflows.
371+
// (But the product rows_minus_one * row_stride_bytes does not overflow.)
372+
let mut test = template.clone();
373+
test.copy_size.width = 0x8000_0000;
374+
test.copy_size.height = 0x8000_0000;
375+
test.expected_error = Some(Error::ImageBytesOverflow(true));
376+
test.run();
377+
378+
// Invalid because image_stride_bytes overflows.
379+
let mut test = template.clone();
380+
test.copy_size.width = 0x8000_0000;
381+
test.layout.rows_per_image = Some(0x8000_0000);
382+
test.expected_result.image_stride_rows = 0x8000_0000;
383+
test.expected_error = Some(Error::ImageStrideOverflow);
384+
test.run();
385+
386+
// Invalid because (layers - 1) * image_stride_bytes overflows.
387+
let mut test = template.clone();
388+
test.copy_size.depth_or_array_layers = 0x8000_0000;
389+
test.copy_size.width = 0x1_0000;
390+
test.copy_size.height = 0x1_0000;
391+
test.expected_error = Some(Error::ArraySizeOverflow(false));
392+
test.run();
260393

394+
// Invalid because the total size of the copy overflows (but the product
395+
// (layers - 1) * image_stride_bytes does not overflow).
396+
let mut test = template.clone();
397+
test.copy_size.depth_or_array_layers = 0x3fff_8001;
398+
test.copy_size.width = 0x1_0001;
399+
test.copy_size.height = 0x1_0001;
400+
test.expected_error = Some(Error::ArraySizeOverflow(true));
261401
test.run();
262402
}
263403

@@ -294,6 +434,7 @@ mod tests {
294434
image_bytes_dense: 8 * 2 * 4,
295435
bytes_in_copy: 8 * 2 * 4,
296436
},
437+
expected_error: None,
297438
};
298439

299440
test.run();

0 commit comments

Comments
 (0)