Skip to content

Commit 141c74d

Browse files
authored
Fix soundness issues (#74)
The first commit adds tests that demonstrate undefined behavior in safe code under `miri`. The second commit fixes the issues with minimal API changes. The API could be adjusted to make such issues less likely (e.g. pre-validated fields should not be public and mutable), but this PR only provides a semver-compatible fix. The third commit updates documentation to more clearly communicate alignment requirements to API users. ### Impact analysis The alignment issue is mostly theoretical and should not impact real-world uses. The missing field validation results in buffer overflow, but you need to edit the fields after the initial validation pass; I don't expect this to be common in the wild (albeit I haven't verified that), so I don't think it warrants a full-on security advisory, but you might want to get an unsoundness advisory into [RustSec](https://github.com/rustsec/advisory-db/) with the `informational = unsound` field.
1 parent e18c52a commit 141c74d

5 files changed

Lines changed: 118 additions & 14 deletions

File tree

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ A Rust library providing efficient data structures and utilities for handling YU
1414
- **Flexible plane structure**: Efficient memory layout with configurable padding for SIMD operations
1515
- **Multiple chroma formats**: Support for YUV 4:2:0, 4:2:2, 4:4:4, and monochrome
1616
- **Builder pattern API**: Safe and ergonomic frame construction with compile-time guarantees
17-
- **SIMD-friendly alignment**: 64-byte alignment (8-byte on WASM) for optimal performance
17+
- **SIMD-friendly alignment**: non-empty planes are aligned to at least 64 bytes
18+
on most targets, 8 bytes on non-WASI `wasm32`, or `align_of::<T>()` if larger
1819
- **WebAssembly support**: Works in both browser (`wasm32-unknown-unknown`) and WASI environments
1920
- **Zero-copy iterators**: Efficient row-based and pixel-based iteration without allocations
2021

@@ -158,7 +159,10 @@ cargo build --target wasm32-wasi
158159
wasm-pack test --headless --chrome --firefox
159160
```
160161

161-
The crate automatically adjusts memory alignment for WASM targets (8-byte vs 64-byte on native).
162+
The crate automatically adjusts memory alignment for WebAssembly: non-WASI
163+
`wasm32` targets use an 8-byte minimum alignment, while WASI and native targets
164+
use a 64-byte minimum. Over-aligned pixel data uses `align_of::<T>()` if that is
165+
larger.
162166

163167
## Feature Flags
164168

src/plane.rs

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,19 @@
1616
//! # Memory Layout
1717
//!
1818
//! Planes store data in a contiguous, aligned buffer with support for padding on all sides:
19-
//! - Data is aligned to 64 bytes on non-WASM platforms (SIMD-friendly)
20-
//! - Data is aligned to 8 bytes on WASM platforms
19+
//! - Non-empty allocations are aligned to at least 64 bytes on most targets
20+
//! (SIMD-friendly)
21+
//! - Non-empty allocations are aligned to at least 8 bytes on `wasm32` targets
22+
//! that are not WASI
23+
//! - If `T` has stricter alignment requirements, the allocation uses
24+
//! `std::mem::align_of::<T>()` instead
2125
//! - Padding pixels surround the visible area for codec algorithms that need border access
2226
//!
27+
//! More precisely, non-empty plane data is allocated with
28+
//! `max(DATA_ALIGNMENT, std::mem::align_of::<T>())`, where `DATA_ALIGNMENT` is
29+
//! 64 except on non-WASI `wasm32` targets, where it is 8. Empty planes do not
30+
//! allocate and must not be assumed to have this extra SIMD alignment.
31+
//!
2332
//! # API Design
2433
//!
2534
//! The public API exposes only the "visible" pixels by default, abstracting away the padding.
@@ -58,8 +67,17 @@ use crate::pixel::Pixel;
5867
/// # Memory Layout
5968
///
6069
/// The data is stored in a contiguous, aligned buffer:
61-
/// - 64-byte alignment on non-WASM platforms (optimized for SIMD operations)
62-
/// - 8-byte alignment on WASM platforms
70+
/// - Non-empty allocations are aligned to at least 64 bytes on most targets
71+
/// (optimized for SIMD operations)
72+
/// - Non-empty allocations are aligned to at least 8 bytes on `wasm32` targets
73+
/// that are not WASI
74+
/// - If `T` has stricter alignment requirements, the allocation uses
75+
/// `std::mem::align_of::<T>()` instead
76+
///
77+
/// More precisely, non-empty plane data is allocated with
78+
/// `max(DATA_ALIGNMENT, std::mem::align_of::<T>())`, where `DATA_ALIGNMENT` is
79+
/// 64 except on non-WASI `wasm32` targets, where it is 8. Empty planes do not
80+
/// allocate and must not be assumed to have this extra SIMD alignment.
6381
///
6482
/// The visible pixels are surrounded by optional padding pixels. The public API
6583
/// provides access only to the visible area by default; padding access requires
@@ -115,6 +133,18 @@ impl<T> Plane<T> {
115133
///
116134
/// [`data_mut`][Plane::data_mut] can be used to initialize the underlying memory.
117135
///
136+
/// # Allocation Alignment
137+
///
138+
/// For non-empty planes, the allocation returned by [`data`][Plane::data]
139+
/// and [`data_mut`][Plane::data_mut] is aligned to
140+
/// `max(DATA_ALIGNMENT, std::mem::align_of::<T>())`, where `DATA_ALIGNMENT`
141+
/// is 64 except on non-WASI `wasm32` targets, where it is 8. This matters
142+
/// if you use unsafe code to access the backing pointer directly, especially
143+
/// with over-aligned `T`.
144+
///
145+
/// Empty planes do not allocate and must not be assumed to have the extra
146+
/// SIMD alignment beyond what Rust requires for an empty `[T]` slice.
147+
///
118148
/// # Example
119149
///
120150
/// ```
@@ -138,11 +168,15 @@ impl<T> Plane<T> {
138168
#[inline]
139169
#[must_use]
140170
pub fn new_uninit(geometry: PlaneGeometry) -> Plane<MaybeUninit<T>> {
141-
let rows = geometry.alloc_height();
142-
let pixels = rows.saturating_mul(geometry.stride);
171+
let geometry = geometry
172+
.normalized()
173+
.expect("plane geometry dimensions must not overflow");
174+
let pixels = geometry
175+
.allocation_len()
176+
.expect("plane allocation size must not overflow usize");
143177

144178
Plane {
145-
data: AlignedData::new_uninit(pixels.get()),
179+
data: AlignedData::new_uninit(pixels),
146180
geometry,
147181
}
148182
}
@@ -198,8 +232,12 @@ impl<T> Plane<MaybeUninit<T>> {
198232
impl<T: Pixel> Plane<T> {
199233
/// Creates a new plane with the given geometry, initialized with zero-valued pixels.
200234
pub(crate) fn new(geometry: PlaneGeometry) -> Self {
201-
let rows = geometry.alloc_height();
202-
let pixels = rows.get() * geometry.stride.get();
235+
let geometry = geometry
236+
.normalized()
237+
.expect("plane geometry dimensions must not overflow");
238+
let pixels = geometry
239+
.allocation_len()
240+
.expect("plane allocation size must not overflow usize");
203241

204242
Self {
205243
data: AlignedData::new(pixels),

src/plane/aligned.rs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
use std::alloc::{Layout, alloc, alloc_zeroed, dealloc, handle_alloc_error};
22
use std::fmt::Debug;
33
use std::marker::PhantomData;
4-
use std::mem::{ManuallyDrop, MaybeUninit};
4+
use std::mem::{ManuallyDrop, MaybeUninit, align_of};
55
use std::num::NonZeroUsize;
66
use std::ops::{Deref, DerefMut};
77
use std::ptr::NonNull;
88

99
use crate::pixel::Pixel;
1010

11-
// Minimum data alignment to help with SIMD
11+
// Minimum data alignment to help with SIMD. Non-empty allocations use this or
12+
// `align_of::<T>()`, whichever is larger.
1213
const DATA_ALIGNMENT: usize = {
1314
if cfg!(target_arch = "wasm32") && cfg!(not(target_os = "wasi")) {
1415
// wasm32-unknown-unknown, wasm32-unknown-emscripten
@@ -34,13 +35,18 @@ unsafe impl<T: Sync> Sync for AlignedData<T> {}
3435
impl<T> AlignedData<T> {
3536
const fn layout(len: NonZeroUsize) -> Layout {
3637
const { assert!(DATA_ALIGNMENT.is_power_of_two()) };
38+
let alignment = if align_of::<T>() > DATA_ALIGNMENT {
39+
align_of::<T>()
40+
} else {
41+
DATA_ALIGNMENT
42+
};
3743
let t_size = const { NonZeroUsize::new(size_of::<T>()).expect("T is Sized") };
3844

3945
let size = len
4046
.checked_mul(t_size)
4147
.expect("allocation size does not overflow usize");
4248

43-
match Layout::from_size_align(size.get(), DATA_ALIGNMENT) {
49+
match Layout::from_size_align(size.get(), alignment) {
4450
Ok(l) => l,
4551
_ => panic!("invalid layout"),
4652
}
@@ -219,6 +225,25 @@ mod tests {
219225
AlignedData::<String>::new_uninit(0);
220226
}
221227

228+
#[cfg(miri)]
229+
#[test]
230+
fn new_uninit_underaligns_overaligned_types() {
231+
#[allow(dead_code)]
232+
#[repr(align(1048576))]
233+
struct OverAligned([u8; 1]);
234+
235+
// Issue: `AlignedData::new_uninit` allocates with `DATA_ALIGNMENT`
236+
// rather than `max(DATA_ALIGNMENT, align_of::<T>())`. Safe callers can
237+
// therefore create `AlignedData<MaybeUninit<T>>` for an over-aligned
238+
// `T`, and the safe slice accessors form references whose required
239+
// alignment is stronger than the allocation's layout. The public
240+
// `Plane::new_uninit` padding API forwards to this helper for arbitrary
241+
// `T`, so this can be reached without an unsafe call before any
242+
// `assume_init`.
243+
let mut data = AlignedData::<OverAligned>::new_uninit(1);
244+
data[0].write(OverAligned([0]));
245+
}
246+
222247
#[test]
223248
#[should_panic(expected = "invalid layout")]
224249
fn invalid_layout_panic() {

src/plane/geometry.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,29 @@ impl PlaneGeometry {
9292
Self::new(width, height, 0, 0, 0, 0, subsampling_x, subsampling_y)
9393
}
9494

95+
#[inline]
96+
pub(crate) fn normalized(self) -> Option<Self> {
97+
Self::new(
98+
self.width.get(),
99+
self.height.get(),
100+
self.pad_left,
101+
self.pad_right,
102+
self.pad_top,
103+
self.pad_bottom,
104+
self.subsampling_x.get(),
105+
self.subsampling_y.get(),
106+
)
107+
}
108+
109+
#[inline]
110+
pub(crate) fn allocation_len(self) -> Option<usize> {
111+
self.height
112+
.get()
113+
.checked_add(self.pad_top)?
114+
.checked_add(self.pad_bottom)?
115+
.checked_mul(self.stride.get())
116+
}
117+
95118
/// Returns a new [`PlaneGeometry`] based on `self` and according to `subsampling`.
96119
///
97120
/// Returns:

src/plane/tests.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,20 @@ fn plane_new_u16() {
5959
}
6060
}
6161

62+
#[cfg(miri)]
63+
#[test]
64+
fn mutable_geometry_fields_can_break_row_slice_invariant() {
65+
let mut geometry = simple_geometry(1, 1);
66+
geometry.pad_left = 1;
67+
68+
// Regression test: `PlaneGeometry` fields are public, so constructors must
69+
// restore the dependent `stride = width + pad_left + pad_right` invariant
70+
// before row iteration relies on it.
71+
let plane: Plane<u8> = Plane::new(geometry);
72+
assert_eq!(plane.geometry.stride.get(), 2);
73+
assert_eq!(plane.rows().next(), Some(&[0][..]));
74+
}
75+
6276
#[test]
6377
fn plane_dimensions() {
6478
let geometry = simple_geometry(16, 9);

0 commit comments

Comments
 (0)