Skip to content

Commit f52e385

Browse files
andyleisersonjimblandy
authored andcommitted
fix(core): Use more checked arithmetic (#9357)
1 parent 5a9b30f commit f52e385

9 files changed

Lines changed: 138 additions & 107 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ depth_stencil: Some(wgpu::DepthStencilState::stencil(
122122

123123
#### General
124124

125+
- Added new `InvalidWorkgroupSizeError`, which is now used by `DrawError::InvalidGroupSize` and `StageError::InvalidWorkgroupSize`. By @andyleiserson in [#9357](https://github.com/gfx-rs/wgpu/pull/9357).
125126
- Added support for cooperative load/store operations in shaders. Currently only WGSL on the input and SPIR-V, METAL, and WGSL on the output are supported. By @kvark in [#8251](https://github.com/gfx-rs/wgpu/issues/8251).
126127
- Added support for per-vertex attributes in fragment shaders. Currently only WGSL input is supported, and only SPIR-V or WGSL output is supported. By @atlv24 in [#8821](https://github.com/gfx-rs/wgpu/issues/8821).
127128
- Added support for no-perspective barycentric coordinates. By @atlv24 in [#8852](https://github.com/gfx-rs/wgpu/issues/8852).

wgpu-core/src/binding_model.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use alloc::{
55
sync::{Arc, Weak},
66
vec::Vec,
77
};
8-
use core::{fmt, mem::ManuallyDrop, ops::Range};
8+
use core::{fmt, mem::ManuallyDrop, num::Saturating, ops::Range};
99

1010
use arrayvec::ArrayVec;
1111
use thiserror::Error;
@@ -363,9 +363,9 @@ impl BindingTypeMaxCountErrorKind {
363363

364364
#[derive(Debug, Default)]
365365
pub(crate) struct PerStageBindingTypeCounter {
366-
vertex: u32,
367-
fragment: u32,
368-
compute: u32,
366+
vertex: Saturating<u32>,
367+
fragment: Saturating<u32>,
368+
compute: Saturating<u32>,
369369
}
370370

371371
impl PerStageBindingTypeCounter {
@@ -393,7 +393,7 @@ impl PerStageBindingTypeCounter {
393393
if max_value == self.compute {
394394
stage |= wgt::ShaderStages::COMPUTE
395395
}
396-
(BindingZone::Stage(stage), max_value)
396+
(BindingZone::Stage(stage), max_value.0)
397397
}
398398

399399
pub(crate) fn merge(&mut self, other: &Self) {

wgpu-core/src/command/bundle.rs

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ use crate::{
116116
resource_log,
117117
snatch::SnatchGuard,
118118
track::RenderBundleScope,
119+
validation::check_workgroup_sizes,
119120
Label, LabelHelpers,
120121
};
121122

@@ -791,21 +792,20 @@ fn draw_mesh_tasks(
791792
) -> Result<(), RenderBundleErrorInner> {
792793
state.is_ready(DrawCommandFamily::DrawMeshTasks)?;
793794

794-
let groups_size_limit = state.device.limits.max_task_mesh_workgroups_per_dimension;
795-
let max_groups = state.device.limits.max_task_mesh_workgroup_total_count;
796-
if group_count_x > groups_size_limit
797-
|| group_count_y > groups_size_limit
798-
|| group_count_z > groups_size_limit
799-
|| group_count_x * group_count_y * group_count_z > max_groups
800-
{
801-
return Err(RenderBundleErrorInner::Draw(DrawError::InvalidGroupSize {
802-
current: [group_count_x, group_count_y, group_count_z],
803-
limit: groups_size_limit,
804-
max_total: max_groups,
805-
}));
806-
}
807-
808-
if group_count_x > 0 && group_count_y > 0 && group_count_z > 0 {
795+
let total_count = check_workgroup_sizes(
796+
&[group_count_x, group_count_y, group_count_z],
797+
&[
798+
state.device.limits.max_task_mesh_workgroups_per_dimension,
799+
state.device.limits.max_task_mesh_workgroups_per_dimension,
800+
state.device.limits.max_task_mesh_workgroups_per_dimension,
801+
],
802+
"max_task_mesh_workgroups_per_dimension",
803+
state.device.limits.max_task_mesh_workgroup_total_count,
804+
"max_task_mesh_workgroup_total_count",
805+
)
806+
.map_err(|err| RenderBundleErrorInner::Draw(err.into()))?;
807+
808+
if total_count > 0 {
809809
state.flush_bindings();
810810
state.commands.push(ArcRenderCommand::DrawMeshTasks {
811811
group_count_x,
@@ -838,6 +838,7 @@ fn multi_draw_indirect(
838838
let vertex_limits = super::VertexLimits::new(state.vertex_buffer_sizes(), &pipeline.steps);
839839

840840
let stride = super::get_stride_of_indirect_args(family);
841+
assert!(offset <= wgt::BufferAddress::MAX - stride);
841842
state
842843
.buffer_memory_init_actions
843844
.extend(buffer.initialization_status.read().create_action(

wgpu-core/src/command/compute.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -885,11 +885,11 @@ fn dispatch_indirect(
885885
return Err(ComputePassErrorInner::UnalignedIndirectBufferOffset(offset));
886886
}
887887

888-
let end_offset = offset + size_of::<wgt::DispatchIndirectArgs>() as u64;
889-
if end_offset > buffer.size {
888+
let args_size = size_of::<wgt::DispatchIndirectArgs>() as u64;
889+
if buffer.size < args_size || buffer.size - args_size < offset {
890890
return Err(ComputePassErrorInner::IndirectBufferOverrun {
891891
offset,
892-
end_offset,
892+
end_offset: offset + args_size,
893893
buffer_size: buffer.size,
894894
});
895895
}

wgpu-core/src/command/draw.rs

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use wgt::error::{ErrorType, WebGpuError};
66

77
use super::bind::BinderError;
88
use crate::command::pass;
9+
use crate::validation::InvalidWorkgroupSizeError;
910
use crate::{
1011
binding_model::{BindingError, ImmediateUploadError, LateMinBufferBindingSizeMismatch},
1112
resource::{
@@ -60,14 +61,8 @@ pub enum DrawError {
6061
if *wanted_mesh_pipeline {"standard"} else {"mesh shader"},
6162
)]
6263
WrongPipelineType { wanted_mesh_pipeline: bool },
63-
#[error(
64-
"Each current draw group size dimension ({current:?}) must be less or equal to {limit}, and the product must be less or equal to {max_total}"
65-
)]
66-
InvalidGroupSize {
67-
current: [u32; 3],
68-
limit: u32,
69-
max_total: u32,
70-
},
64+
#[error(transparent)]
65+
InvalidGroupSize(#[from] InvalidWorkgroupSizeError),
7166
#[error(
7267
"Mesh shader calls in multiview render passes require enabling the `EXPERIMENTAL_MESH_SHADER_MULTIVIEW` feature, and the highest bit ({highest_view_index}) in the multiview mask must be <= `Limits::max_multiview_view_count` ({max_multiviews})"
7368
)]

wgpu-core/src/command/ray_tracing.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -703,9 +703,10 @@ fn iter_blas<'snatch_guard: 'buffers, 'buffers>(
703703
}) {
704704
input_barriers.push(barrier);
705705
}
706-
if vertex_buffer.size
707-
< (mesh.size.vertex_count + mesh.first_vertex) as u64
708-
* mesh.vertex_stride
706+
if u64::from(mesh.size.vertex_count)
707+
.checked_add(u64::from(mesh.first_vertex))
708+
.and_then(|end_index| end_index.checked_mul(mesh.vertex_stride))
709+
.is_none_or(|end| vertex_buffer.size < end)
709710
{
710711
return Err(BuildAccelerationStructureError::InsufficientBufferSize(
711712
vertex_buffer.error_ident(),

wgpu-core/src/command/render.rs

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ use crate::{
4444
},
4545
snatch::SnatchGuard,
4646
track::{ResourceUsageCompatibilityError, Tracker, UsageScope},
47-
validation, Label,
47+
validation::{self, check_workgroup_sizes},
48+
Label,
4849
};
4950

5051
#[cfg(feature = "serde")]
@@ -2731,21 +2732,17 @@ fn draw_mesh_tasks(
27312732
.device
27322733
.limits
27332734
.max_task_mesh_workgroup_total_count;
2734-
if group_count_x > groups_size_limit
2735-
|| group_count_y > groups_size_limit
2736-
|| group_count_z > groups_size_limit
2737-
|| group_count_x * group_count_y * group_count_z > max_groups
2738-
{
2739-
return Err(DrawError::InvalidGroupSize {
2740-
current: [group_count_x, group_count_y, group_count_z],
2741-
limit: groups_size_limit,
2742-
max_total: max_groups,
2743-
}
2744-
.into());
2745-
}
2735+
let total_count = check_workgroup_sizes(
2736+
&[group_count_x, group_count_y, group_count_z],
2737+
&[groups_size_limit, groups_size_limit, groups_size_limit],
2738+
"max_task_mesh_workgroups_per_dimension",
2739+
max_groups,
2740+
"max_task_mesh_workgroup_total_count",
2741+
)
2742+
.map_err(|err| RenderPassErrorInner::Draw(err.into()))?;
27462743

27472744
unsafe {
2748-
if group_count_x > 0 && group_count_y > 0 && group_count_z > 0 {
2745+
if total_count > 0 {
27492746
state.pass.base.raw_encoder.draw_mesh_tasks(
27502747
group_count_x,
27512748
group_count_y,

wgpu-core/src/device/queue.rs

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -868,6 +868,7 @@ impl Queue {
868868
self.device.alignments.buffer_copy_pitch.get() as u32,
869869
block_size,
870870
);
871+
assert!(u32::MAX - bytes_in_last_row >= bytes_per_row_alignment);
871872
let stage_bytes_per_row = wgt::math::align_to(bytes_in_last_row, bytes_per_row_alignment);
872873

873874
// Platform validation requires that the staging buffer always be
@@ -883,17 +884,21 @@ impl Queue {
883884
} else {
884885
profiling::scope!("copy chunked");
885886
// Copy row by row into the optimal alignment.
886-
let block_rows_in_copy =
887-
(size.depth_or_array_layers - 1) * rows_per_image + height_in_blocks;
888-
let stage_size =
889-
wgt::BufferSize::new(stage_bytes_per_row as u64 * block_rows_in_copy as u64)
890-
.unwrap();
887+
let block_rows_in_copy = u64::from(size.depth_or_array_layers - 1)
888+
* u64::from(rows_per_image)
889+
+ u64::from(height_in_blocks);
890+
// The copy size was validated against the source buffer, however,
891+
// `stage_bytes_per_row` can differ, so let's be paranoid.
892+
let stage_size = u64::from(stage_bytes_per_row)
893+
.checked_mul(block_rows_in_copy)
894+
.and_then(wgt::BufferSize::new)
895+
.unwrap();
891896
let mut staging_buffer = StagingBuffer::new(&self.device, stage_size)?;
892-
for layer in 0..size.depth_or_array_layers {
893-
let rows_offset = layer * rows_per_image;
894-
for row in rows_offset..rows_offset + height_in_blocks {
895-
let src_offset = data_layout.offset as u32 + row * bytes_per_row;
896-
let dst_offset = row * stage_bytes_per_row;
897+
for layer in 0..u64::from(size.depth_or_array_layers) {
898+
let rows_offset = layer * u64::from(rows_per_image);
899+
for row in rows_offset..rows_offset + u64::from(height_in_blocks) {
900+
let src_offset = data_layout.offset + row * u64::from(bytes_per_row);
901+
let dst_offset = row * u64::from(stage_bytes_per_row);
897902
unsafe {
898903
staging_buffer.write_with_offset(
899904
data,

wgpu-core/src/validation.rs

Lines changed: 80 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -281,18 +281,8 @@ impl WebGpuError for InputError {
281281
#[derive(Clone, Debug, Error)]
282282
#[non_exhaustive]
283283
pub enum StageError {
284-
#[error(
285-
"Shader entry point's workgroup size {current:?} ({current_total} total invocations) must be less or equal to the per-dimension
286-
limit `Limits::{per_dimension_limit}` of {limit:?} and the total invocation limit `Limits::{total_limit}` of {total}"
287-
)]
288-
InvalidWorkgroupSize {
289-
current: [u32; 3],
290-
current_total: u32,
291-
limit: [u32; 3],
292-
total: u32,
293-
per_dimension_limit: &'static str,
294-
total_limit: &'static str,
295-
},
284+
#[error(transparent)]
285+
InvalidWorkgroupSize(#[from] InvalidWorkgroupSizeError),
296286
#[error("Unable to find entry point '{0}'")]
297287
MissingEntryPoint(String),
298288
#[error("Shader global {0:?} is not available in the pipeline layout")]
@@ -1384,63 +1374,48 @@ impl Interface {
13841374

13851375
// check workgroup size limits
13861376
if shader_stage.to_naga().compute_like() {
1387-
let (
1388-
max_workgroup_size_limits,
1389-
max_workgroup_size_total,
1390-
per_dimension_limit,
1391-
total_limit,
1392-
) = match shader_stage.to_naga() {
1393-
naga::ShaderStage::Compute => (
1394-
[
1377+
let total = match shader_stage.to_naga() {
1378+
naga::ShaderStage::Compute => check_workgroup_sizes(
1379+
&entry_point.workgroup_size,
1380+
&[
13951381
self.limits.max_compute_workgroup_size_x,
13961382
self.limits.max_compute_workgroup_size_y,
13971383
self.limits.max_compute_workgroup_size_z,
13981384
],
1399-
self.limits.max_compute_invocations_per_workgroup,
14001385
"max_compute_workgroup_size_*",
1386+
self.limits.max_compute_invocations_per_workgroup,
14011387
"max_compute_invocations_per_workgroup",
1402-
),
1403-
naga::ShaderStage::Task => (
1404-
[
1388+
)?,
1389+
naga::ShaderStage::Task => check_workgroup_sizes(
1390+
&entry_point.workgroup_size,
1391+
&[
14051392
self.limits.max_task_invocations_per_dimension,
14061393
self.limits.max_task_invocations_per_dimension,
14071394
self.limits.max_task_invocations_per_dimension,
14081395
],
1409-
self.limits.max_task_invocations_per_workgroup,
14101396
"max_task_invocations_per_dimension",
1397+
self.limits.max_task_invocations_per_workgroup,
14111398
"max_task_invocations_per_workgroup",
1412-
),
1413-
naga::ShaderStage::Mesh => (
1414-
[
1399+
)?,
1400+
naga::ShaderStage::Mesh => check_workgroup_sizes(
1401+
&entry_point.workgroup_size,
1402+
&[
14151403
self.limits.max_mesh_invocations_per_dimension,
14161404
self.limits.max_mesh_invocations_per_dimension,
14171405
self.limits.max_mesh_invocations_per_dimension,
14181406
],
1419-
self.limits.max_mesh_invocations_per_workgroup,
14201407
"max_mesh_invocations_per_dimension",
1408+
self.limits.max_mesh_invocations_per_workgroup,
14211409
"max_mesh_invocations_per_workgroup",
1422-
),
1410+
)?,
14231411
_ => unreachable!(),
14241412
};
1425-
let total_invocations = entry_point
1426-
.workgroup_size
1427-
.iter()
1428-
.fold(1u32, |total, &dim| total.saturating_mul(dim));
1429-
let invalid_total_invocations =
1430-
total_invocations > max_workgroup_size_total || total_invocations == 0;
1431-
1432-
let dimension_too_large = entry_point.workgroup_size[0] > max_workgroup_size_limits[0]
1433-
|| entry_point.workgroup_size[1] > max_workgroup_size_limits[1]
1434-
|| entry_point.workgroup_size[2] > max_workgroup_size_limits[2];
1435-
if invalid_total_invocations || dimension_too_large {
1436-
return Err(StageError::InvalidWorkgroupSize {
1437-
current: entry_point.workgroup_size,
1438-
current_total: total_invocations,
1439-
limit: max_workgroup_size_limits,
1440-
total: max_workgroup_size_total,
1441-
per_dimension_limit,
1442-
total_limit,
1443-
});
1413+
if total == 0 {
1414+
return Err(StageError::InvalidWorkgroupSize(
1415+
InvalidWorkgroupSizeError::Zero {
1416+
dimensions: entry_point.workgroup_size,
1417+
},
1418+
));
14441419
}
14451420
}
14461421

@@ -1810,6 +1785,62 @@ pub fn validate_color_attachment_bytes_per_sample(
18101785
Ok(())
18111786
}
18121787

1788+
#[derive(Clone, Debug, Error)]
1789+
pub enum InvalidWorkgroupSizeError {
1790+
#[error(
1791+
"Workgroup size {dimensions:?} ({total} total invocations) must be less or equal to \
1792+
the per-dimension limit `Limits::{per_dimension_limits_desc}` of {per_dimension_limits:?} \
1793+
and the total invocation limit `Limits::{total_limit_desc}` of {total_limit}"
1794+
)]
1795+
LimitExceeded {
1796+
dimensions: [u32; 3],
1797+
per_dimension_limits: [u32; 3],
1798+
per_dimension_limits_desc: &'static str,
1799+
total: u32,
1800+
total_limit: u32,
1801+
total_limit_desc: &'static str,
1802+
},
1803+
#[error("Workgroup sizes {dimensions:?} must be positive")]
1804+
Zero { dimensions: [u32; 3] },
1805+
}
1806+
1807+
/// Check X/Y/Z workgroup sizes against per-dimension and overall limits.
1808+
///
1809+
/// This function does not check that the sizes are non-zero. In a dispatch, it is legal for
1810+
/// the size to be zero. In shader or pipeline creation, it is an error for the size to be
1811+
/// zero, and the caller must check that.
1812+
pub(crate) fn check_workgroup_sizes(
1813+
sizes: &[u32; 3],
1814+
per_dimension_limits: &[u32; 3],
1815+
per_dimension_limits_desc: &'static str,
1816+
total_limit: u32,
1817+
total_limit_desc: &'static str,
1818+
) -> Result<u32, InvalidWorkgroupSizeError> {
1819+
let total = sizes
1820+
.iter()
1821+
.fold(1u32, |total, &dim| total.saturating_mul(dim));
1822+
1823+
let invalid_total_invocations = total > total_limit;
1824+
1825+
let dimension_too_large = sizes
1826+
.iter()
1827+
.zip(per_dimension_limits.iter())
1828+
.any(|(dim, limit)| dim > limit);
1829+
1830+
if invalid_total_invocations || dimension_too_large {
1831+
Err(InvalidWorkgroupSizeError::LimitExceeded {
1832+
dimensions: *sizes,
1833+
per_dimension_limits: *per_dimension_limits,
1834+
per_dimension_limits_desc,
1835+
total,
1836+
total_limit,
1837+
total_limit_desc,
1838+
})
1839+
} else {
1840+
Ok(total)
1841+
}
1842+
}
1843+
18131844
pub enum ShaderStageForValidation {
18141845
Vertex {
18151846
topology: wgt::PrimitiveTopology,

0 commit comments

Comments
 (0)