Skip to content

Commit ee35b0e

Browse files
jimblandyErichDonGubler
authored andcommitted
[core, hal, types] Clarify wgpu_hal's bounds check promises.
In `wgpu_hal`: - Document that `wgpu_hal` guarantees that shaders will not access buffer contents beyond the bindgroups' bound regions, rounded up to some adapter-specific alignment. Introduce the term "accessible region" for the portion of the buffer that shaders can actually get at. - Document that all bets are off if you disable bounds checks with `ShaderModuleDescriptor::runtime_checks`. - Provide this alignment in `wgpu_hal::Alignments`. Update all backends appropriately. - In the Vulkan backend, use Naga to inject bounds checks on buffer accesses unless `robustBufferAccess2` is available; `robustBufferAccess` is not sufficient. Retrieve `VK_EXT_robustness2`'s properties, as needed to discover the alignment above. In `wgpu_core`: - Use buffer bindings' accessible regions to determine which parts of the buffer need to be initialized. In `wgpu_types`: - Document some of the possible effects of using `ShaderBoundsChecks::unchecked`. Fixes #1813.
1 parent de7765b commit ee35b0e

File tree

10 files changed

+192
-10
lines changed

10 files changed

+192
-10
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ By @teoxoy [#6134](https://github.com/gfx-rs/wgpu/pull/6134).
9696
- Fix crash when dropping the surface after the device. By @wumpf in [#6052](https://github.com/gfx-rs/wgpu/pull/6052)
9797
- Fix error message that is thrown in create_render_pass to no longer say `compute_pass`. By @matthew-wong1 [#6041](https://github.com/gfx-rs/wgpu/pull/6041)
9898
- Add `VideoFrame` to `ExternalImageSource` enum. By @jprochazk in [#6170](https://github.com/gfx-rs/wgpu/pull/6170)
99+
- Document `wgpu_hal` bounds-checking promises, and adapt `wgpu_core`'s lazy initialization logic to the slightly weaker-than-expected guarantees. By @jimblandy in [#6201](https://github.com/gfx-rs/wgpu/pull/6201)
99100

100101
#### GLES / OpenGL
101102

wgpu-core/src/binding_model.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -884,6 +884,16 @@ pub(crate) fn buffer_binding_type_alignment(
884884
}
885885
}
886886

887+
pub(crate) fn buffer_binding_type_bounds_check_alignment(
888+
alignments: &hal::Alignments,
889+
binding_type: wgt::BufferBindingType,
890+
) -> wgt::BufferAddress {
891+
match binding_type {
892+
wgt::BufferBindingType::Uniform => alignments.uniform_bounds_check_alignment.get(),
893+
wgt::BufferBindingType::Storage { .. } => wgt::COPY_BUFFER_ALIGNMENT,
894+
}
895+
}
896+
887897
#[derive(Debug)]
888898
pub struct BindGroup {
889899
pub(crate) raw: Snatchable<Box<dyn hal::DynBindGroup>>,

wgpu-core/src/device/resource.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ use once_cell::sync::OnceCell;
3939

4040
use smallvec::SmallVec;
4141
use thiserror::Error;
42-
use wgt::{DeviceLostReason, TextureFormat, TextureSampleType, TextureViewDimension};
42+
use wgt::{
43+
math::align_to, DeviceLostReason, TextureFormat, TextureSampleType, TextureViewDimension,
44+
};
4345

4446
use std::{
4547
borrow::Cow,
@@ -2004,10 +2006,21 @@ impl Device {
20042006
late_buffer_binding_sizes.insert(binding, late_size);
20052007
}
20062008

2009+
// This was checked against the device's alignment requirements above,
2010+
// which should always be a multiple of `COPY_BUFFER_ALIGNMENT`.
20072011
assert_eq!(bb.offset % wgt::COPY_BUFFER_ALIGNMENT, 0);
2012+
2013+
// `wgpu_hal` only restricts shader access to bound buffer regions with
2014+
// a certain resolution. For the sake of lazy initialization, round up
2015+
// the size of the bound range to reflect how much of the buffer is
2016+
// actually going to be visible to the shader.
2017+
let bounds_check_alignment =
2018+
binding_model::buffer_binding_type_bounds_check_alignment(&self.alignments, binding_ty);
2019+
let visible_size = align_to(bind_size, bounds_check_alignment);
2020+
20082021
used_buffer_ranges.extend(buffer.initialization_status.read().create_action(
20092022
buffer,
2010-
bb.offset..bb.offset + bind_size,
2023+
bb.offset..bb.offset + visible_size,
20112024
MemoryInitKind::NeedsInitializedMemory,
20122025
));
20132026

wgpu-hal/src/dx12/adapter.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,9 @@ impl super::Adapter {
519519
Direct3D12::D3D12_TEXTURE_DATA_PITCH_ALIGNMENT as u64,
520520
)
521521
.unwrap(),
522+
// Direct3D correctly bounds-checks all array accesses:
523+
// https://microsoft.github.io/DirectX-Specs/d3d/archive/D3D11_3_FunctionalSpec.htm#18.6.8.2%20Device%20Memory%20Reads
524+
uniform_bounds_check_alignment: wgt::BufferSize::new(1).unwrap(),
522525
},
523526
downlevel,
524527
},

wgpu-hal/src/gles/adapter.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -841,6 +841,16 @@ impl super::Adapter {
841841
alignments: crate::Alignments {
842842
buffer_copy_offset: wgt::BufferSize::new(4).unwrap(),
843843
buffer_copy_pitch: wgt::BufferSize::new(4).unwrap(),
844+
// #6151: `wgpu_hal::gles` doesn't ask Naga to inject bounds
845+
// checks in GLSL, and it doesn't request extensions like
846+
// `KHR_robust_buffer_access_behavior` that would provide
847+
// them, so we can't really implement the checks promised by
848+
// [`crate::BufferBinding`].
849+
//
850+
// Since this is a pre-existing condition, for the time
851+
// being, provide 1 as the value here, to cause as little
852+
// trouble as possible.
853+
uniform_bounds_check_alignment: wgt::BufferSize::new(1).unwrap(),
844854
},
845855
},
846856
})

wgpu-hal/src/lib.rs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1647,9 +1647,27 @@ pub struct InstanceDescriptor<'a> {
16471647
pub struct Alignments {
16481648
/// The alignment of the start of the buffer used as a GPU copy source.
16491649
pub buffer_copy_offset: wgt::BufferSize,
1650+
16501651
/// The alignment of the row pitch of the texture data stored in a buffer that is
16511652
/// used in a GPU copy operation.
16521653
pub buffer_copy_pitch: wgt::BufferSize,
1654+
1655+
/// The finest alignment of bound range checking for uniform buffers.
1656+
///
1657+
/// When `wgpu_hal` restricts shader references to the [accessible
1658+
/// region][ar] of a [`Uniform`] buffer, the size of the accessible region
1659+
/// is the bind group binding's stated [size], rounded up to the next
1660+
/// multiple of this value.
1661+
///
1662+
/// We don't need an analogous field for storage buffer bindings, because
1663+
/// all our backends promise to enforce the size at least to a four-byte
1664+
/// alignment, and `wgpu_hal` requires bound range lengths to be a multiple
1665+
/// of four anyway.
1666+
///
1667+
/// [ar]: struct.BufferBinding.html#accessible-region
1668+
/// [`Uniform`]: wgt::BufferBindingType::Uniform
1669+
/// [size]: BufferBinding::size
1670+
pub uniform_bounds_check_alignment: wgt::BufferSize,
16531671
}
16541672

16551673
#[derive(Clone, Debug)]
@@ -1819,6 +1837,40 @@ pub struct PipelineLayoutDescriptor<'a, B: DynBindGroupLayout + ?Sized> {
18191837
pub push_constant_ranges: &'a [wgt::PushConstantRange],
18201838
}
18211839

1840+
/// A region of a buffer made visible to shaders via a [`BindGroup`].
1841+
///
1842+
/// [`BindGroup`]: Api::BindGroup
1843+
///
1844+
/// ## Accessible region
1845+
///
1846+
/// `wgpu_hal` guarantees that shaders compiled with
1847+
/// [`ShaderModuleDescriptor::runtime_checks`] set to `true` cannot read or
1848+
/// write data via this binding outside the *accessible region* of [`buffer`]:
1849+
///
1850+
/// - The accessible region starts at [`offset`].
1851+
///
1852+
/// - For [`Storage`] bindings, the size of the accessible region is [`size`],
1853+
/// which must be a multiple of 4.
1854+
///
1855+
/// - For [`Uniform`] bindings, the size of the accessible region is [`size`]
1856+
/// rounded up to the next multiple of
1857+
/// [`Alignments::uniform_bounds_check_alignment`].
1858+
///
1859+
/// Note that this guarantee is stricter than WGSL's requirements for
1860+
/// [out-of-bounds accesses][woob], as WGSL allows them to return values from
1861+
/// elsewhere in the buffer. But this guarantee is necessary anyway, to permit
1862+
/// `wgpu-core` to avoid clearing uninitialized regions of buffers that will
1863+
/// never be read by the application before they are overwritten. This
1864+
/// optimization consults bind group buffer binding regions to determine which
1865+
/// parts of which buffers shaders might observe. This optimization is only
1866+
/// sound if shader access is bounds-checked.
1867+
///
1868+
/// [`buffer`]: BufferBinding::buffer
1869+
/// [`offset`]: BufferBinding::offset
1870+
/// [`size`]: BufferBinding::size
1871+
/// [`Storage`]: wgt::BufferBindingType::Storage
1872+
/// [`Uniform`]: wgt::BufferBindingType::Uniform
1873+
/// [woob]: https://gpuweb.github.io/gpuweb/wgsl/#out-of-bounds-access-sec
18221874
#[derive(Debug)]
18231875
pub struct BufferBinding<'a, B: DynBuffer + ?Sized> {
18241876
/// The buffer being bound.
@@ -1937,6 +1989,26 @@ pub enum ShaderInput<'a> {
19371989

19381990
pub struct ShaderModuleDescriptor<'a> {
19391991
pub label: Label<'a>,
1992+
1993+
/// Enforce bounds checks in shaders, even if the underlying driver doesn't
1994+
/// support doing so natively.
1995+
///
1996+
/// When this is `true`, `wgpu_hal` promises that shaders can only read or
1997+
/// write the [accessible region][ar] of a bindgroup's buffer bindings. If
1998+
/// the underlying graphics platform cannot implement these bounds checks
1999+
/// itself, `wgpu_hal` will inject bounds checks before presenting the
2000+
/// shader to the platform.
2001+
///
2002+
/// When this is `false`, `wgpu_hal` only enforces such bounds checks if the
2003+
/// underlying platform provides a way to do so itself. `wgpu_hal` does not
2004+
/// itself add any bounds checks to generated shader code.
2005+
///
2006+
/// Note that `wgpu_hal` users may try to initialize only those portions of
2007+
/// buffers that they anticipate might be read from. Passing `false` here
2008+
/// may allow shaders to see wider regions of the buffers than expected,
2009+
/// making such deferred initialization visible to the application.
2010+
///
2011+
/// [ar]: struct.BufferBinding.html#accessible-region
19402012
pub runtime_checks: bool,
19412013
}
19422014

wgpu-hal/src/metal/adapter.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -997,6 +997,10 @@ impl super::PrivateCapabilities {
997997
alignments: crate::Alignments {
998998
buffer_copy_offset: wgt::BufferSize::new(self.buffer_alignment).unwrap(),
999999
buffer_copy_pitch: wgt::BufferSize::new(4).unwrap(),
1000+
// This backend has Naga incorporate bounds checks into the
1001+
// Metal Shading Language it generates, so from `wgpu_hal`'s
1002+
// users' point of view, references are tightly checked.
1003+
uniform_bounds_check_alignment: wgt::BufferSize::new(1).unwrap(),
10001004
},
10011005
downlevel,
10021006
}

wgpu-hal/src/vulkan/adapter.rs

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -342,9 +342,6 @@ impl PhysicalDeviceFeatures {
342342
None
343343
},
344344
robustness2: if enabled_extensions.contains(&ext::robustness2::NAME) {
345-
// Note: enabling `robust_buffer_access2` isn't requires, strictly speaking
346-
// since we can enable `robust_buffer_access` all the time. But it improves
347-
// program portability, so we opt into it if they are supported.
348345
Some(
349346
vk::PhysicalDeviceRobustness2FeaturesEXT::default()
350347
.robust_buffer_access2(private_caps.robust_buffer_access2)
@@ -842,6 +839,10 @@ pub struct PhysicalDeviceProperties {
842839
/// `VK_EXT_subgroup_size_control` extension, promoted to Vulkan 1.3.
843840
subgroup_size_control: Option<vk::PhysicalDeviceSubgroupSizeControlProperties<'static>>,
844841

842+
/// Additional `vk::PhysicalDevice` properties from the
843+
/// `VK_EXT_robustness2` extension.
844+
robustness2: Option<vk::PhysicalDeviceRobustness2PropertiesEXT<'static>>,
845+
845846
/// The device API version.
846847
///
847848
/// Which is the version of Vulkan supported for device-level functionality.
@@ -1097,13 +1098,38 @@ impl PhysicalDeviceProperties {
10971098
}
10981099
}
10991100

1100-
fn to_hal_alignments(&self) -> crate::Alignments {
1101+
/// Return a `wgpu_hal::Alignments` structure describing this adapter.
1102+
///
1103+
/// The `using_robustness2` argument says how this adapter will implement
1104+
/// `wgpu_hal`'s guarantee that shaders can only read the [accessible
1105+
/// region][ar] of bindgroup's buffer bindings:
1106+
///
1107+
/// - If this adapter will depend on `VK_EXT_robustness2`'s
1108+
/// `robustBufferAccess2` feature to apply bounds checks to shader buffer
1109+
/// access, `using_robustness2` must be `true`.
1110+
///
1111+
/// - Otherwise, this adapter must use Naga to inject bounds checks on
1112+
/// buffer accesses, and `using_robustness2` must be `false`.
1113+
///
1114+
/// [ar]: ../../struct.BufferBinding.html#accessible-region
1115+
fn to_hal_alignments(&self, using_robustness2: bool) -> crate::Alignments {
11011116
let limits = &self.properties.limits;
11021117
crate::Alignments {
11031118
buffer_copy_offset: wgt::BufferSize::new(limits.optimal_buffer_copy_offset_alignment)
11041119
.unwrap(),
11051120
buffer_copy_pitch: wgt::BufferSize::new(limits.optimal_buffer_copy_row_pitch_alignment)
11061121
.unwrap(),
1122+
uniform_bounds_check_alignment: {
1123+
let alignment = if using_robustness2 {
1124+
self.robustness2
1125+
.unwrap() // if we're using it, we should have its properties
1126+
.robust_uniform_buffer_access_size_alignment
1127+
} else {
1128+
// If the `robustness2` properties are unavailable, then `robustness2` is not available either Naga-injected bounds checks are precise.
1129+
1
1130+
};
1131+
wgt::BufferSize::new(alignment).unwrap()
1132+
},
11071133
}
11081134
}
11091135
}
@@ -1133,6 +1159,7 @@ impl super::InstanceShared {
11331159
let supports_subgroup_size_control = capabilities.device_api_version
11341160
>= vk::API_VERSION_1_3
11351161
|| capabilities.supports_extension(ext::subgroup_size_control::NAME);
1162+
let supports_robustness2 = capabilities.supports_extension(ext::robustness2::NAME);
11361163

11371164
let supports_acceleration_structure =
11381165
capabilities.supports_extension(khr::acceleration_structure::NAME);
@@ -1180,6 +1207,13 @@ impl super::InstanceShared {
11801207
properties2 = properties2.push_next(next);
11811208
}
11821209

1210+
if supports_robustness2 {
1211+
let next = capabilities
1212+
.robustness2
1213+
.insert(vk::PhysicalDeviceRobustness2PropertiesEXT::default());
1214+
properties2 = properties2.push_next(next);
1215+
}
1216+
11831217
unsafe {
11841218
get_device_properties.get_physical_device_properties2(phd, &mut properties2)
11851219
};
@@ -1191,6 +1225,7 @@ impl super::InstanceShared {
11911225
capabilities
11921226
.supported_extensions
11931227
.retain(|&x| x.extension_name_as_c_str() != Ok(ext::robustness2::NAME));
1228+
capabilities.robustness2 = None;
11941229
}
11951230
};
11961231
capabilities
@@ -1507,7 +1542,7 @@ impl super::Instance {
15071542
};
15081543
let capabilities = crate::Capabilities {
15091544
limits: phd_capabilities.to_wgpu_limits(),
1510-
alignments: phd_capabilities.to_hal_alignments(),
1545+
alignments: phd_capabilities.to_hal_alignments(private_caps.robust_buffer_access2),
15111546
downlevel: wgt::DownlevelCapabilities {
15121547
flags: downlevel_flags,
15131548
limits: wgt::DownlevelLimits {},
@@ -1779,7 +1814,7 @@ impl super::Adapter {
17791814
capabilities: Some(capabilities.iter().cloned().collect()),
17801815
bounds_check_policies: naga::proc::BoundsCheckPolicies {
17811816
index: naga::proc::BoundsCheckPolicy::Restrict,
1782-
buffer: if self.private_caps.robust_buffer_access {
1817+
buffer: if self.private_caps.robust_buffer_access2 {
17831818
naga::proc::BoundsCheckPolicy::Unchecked
17841819
} else {
17851820
naga::proc::BoundsCheckPolicy::Restrict

wgpu-hal/src/vulkan/mod.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,9 +493,36 @@ struct PrivateCapabilities {
493493
/// Ability to present contents to any screen. Only needed to work around broken platform configurations.
494494
can_present: bool,
495495
non_coherent_map_mask: wgt::BufferAddress,
496+
497+
/// True if this adapter advertises the [`robustBufferAccess`][vrba] feature.
498+
///
499+
/// Note that Vulkan's `robustBufferAccess` is not sufficient to implement
500+
/// `wgpu_hal`'s guarantee that shaders will not access buffer contents via
501+
/// a given bindgroup binding outside that binding's [accessible
502+
/// region][ar]. Enabling `robustBufferAccess` does ensure that
503+
/// out-of-bounds reads and writes are not undefined behavior (that's good),
504+
/// but still permits out-of-bounds reads to return data from anywhere
505+
/// within the buffer, not just the accessible region.
506+
///
507+
/// [ar]: ../struct.BufferBinding.html#accessible-region
508+
/// [vrba]: https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#features-robustBufferAccess
496509
robust_buffer_access: bool,
510+
497511
robust_image_access: bool,
512+
513+
/// True if this adapter supports the [`VK_EXT_robustness2`] extension's
514+
/// [`robustBufferAccess2`] feature.
515+
///
516+
/// This is sufficient to implement `wgpu_hal`'s [required bounds-checking][ar] of
517+
/// shader accesses to buffer contents. If this feature is not available,
518+
/// this backend must have Naga inject bounds checks in the generated
519+
/// SPIR-V.
520+
///
521+
/// [`VK_EXT_robustness2`]: https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VK_EXT_robustness2.html
522+
/// [`robustBufferAccess2`]: https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkPhysicalDeviceRobustness2FeaturesEXT.html#features-robustBufferAccess2
523+
/// [ar]: ../struct.BufferBinding.html#accessible-region
498524
robust_buffer_access2: bool,
525+
499526
robust_image_access2: bool,
500527
zero_initialize_workgroup_memory: bool,
501528
image_format_list: bool,

wgpu-types/src/lib.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7310,8 +7310,15 @@ impl ShaderBoundChecks {
73107310
/// Creates a new configuration where the shader isn't bound checked.
73117311
///
73127312
/// # Safety
7313-
/// The caller MUST ensure that all shaders built with this configuration don't perform any
7314-
/// out of bounds reads or writes.
7313+
///
7314+
/// The caller MUST ensure that all shaders built with this configuration
7315+
/// don't perform any out of bounds reads or writes.
7316+
///
7317+
/// Note that `wgpu_core`, in particular, initializes only those portions of
7318+
/// buffers that it expects might be read, and it does not expect contents
7319+
/// outside the ranges bound in bindgroups to be accessible, so using this
7320+
/// configuration with ill-behaved shaders could expose uninitialized GPU
7321+
/// memory contents to the application.
73157322
#[must_use]
73167323
pub unsafe fn unchecked() -> Self {
73177324
ShaderBoundChecks {

0 commit comments

Comments
 (0)