Unsoundness with as_uninit_slice_mut
Hi, we found two soundness bugs, which both can trigger undefined behaviors via as_uninit_slice_mut():
- Bug 1:
as_uninit_slice_mut() allows to overwrite existing initialized elements, which can trigger UB.
- Bug 2:
Drain::next() can make uninitialized memory exposure.
Both can be triggered in pure-safe Rust code.
Direct UB via as_uninit_slice_mut()
|
pub fn as_uninit_slice_mut(&mut self) -> &mut [MaybeUninit<T>] { |
|
unsafe { std::slice::from_raw_parts_mut(self.xs.as_mut_ptr().cast(), CAP) } |
|
} |
This method exposes the whole buffer(capacity CAP)as &mut [MaybeUninit<T>], which allows the caller to:
- Overwrite the existing initialized
T as MaybeUninit::uninit()
- Not triggering original
drop
- Break the invariant hold for
len, which counts for initialized elements
PoC 1: Direct Read UB
use arraydeque::ArrayDeque;
use std::mem::MaybeUninit;
fn main() {
let mut deque: ArrayDeque<String, 2> = ArrayDeque::new();
deque.push_back(String::from("hello"));
deque.push_back(String:: from("world"));
let slice = deque.as_uninit_slice_mut();
slice[0] = MaybeUninit::uninit();
// uninitialized memory access
println!("{}", deque[0]);
}
UB in Drain::next() via Drop
|
#[inline] |
|
fn next(&mut self) -> Option<T> { |
|
self.iter.next().map(|elt| unsafe { ptr::read(elt) }) |
|
} |
Location: Line 2389 (in Drain::drop)
fn drop(&mut self) {
for _ in self.by_ref() {} // call next()
// ...
}
Location: Line 2074-2078 (in ArrayDeque::drop)
impl<T, const CAP: usize, B: Behavior> Drop for ArrayDeque<T, CAP, B> {
fn drop(&mut self) {
self.clear(); // calls drain(. .)
}
}
When as_uninit_slice_mut() breaks the invariant hold by len thenArrayDeque is dropped:
[Drop::drop]() calls clear() (lib.rs#L2074-L2077)
clear() calls drain(..)
Drain::drop calls next()
next() calls ptr::read(&T) while T actuallys is uninitialized
- Read uninitalized memory as
String(e.g., Vec.cap)
PoC 2: Drop UB
use arraydeque::ArrayDeque;
use std::mem::MaybeUninit;
fn main() {
let mut deque: ArrayDeque<String, 1> = ArrayDeque::new();
deque.push_back(String::from("hello"));
let slice = deque.as_uninit_slice_mut();
slice[0] = MaybeUninit::uninit();
// when dropped
// Drop::drop → clear() → drain(..) → Drain::drop → next() → ptr::read()
} // UB occurs
Miri Output:
error: Undefined Behavior: constructing invalid value at .vec.buf.inner.cap.0: encountered uninitialized memory, but expected an integer
--> /home/usr/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/arraydeque-0.5.1/src/lib.rs:2434:45
|
2434 | self.iter.next().map(|elt| unsafe { ptr::read(elt) })
| ^^^^^^^^^^^^^^ Undefined Behavior occurred here
|
= help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
Recommended Fixes
Option 1: Make as_uninit_slice_mut() Unsafe (Minimal Fix)
/// # Safety
///
/// Caller must not overwrite initialized elements (indices where the deque
/// has valid data) with `MaybeUninit::uninit()`. Doing so will cause
/// undefined behavior when the deque is dropped or the elements are accessed.
///
/// The initialized region is from index `tail` to `tail + len` (wrapping).
pub unsafe fn as_uninit_slice_mut(&mut self) -> &mut [MaybeUninit<T>] {
unsafe { std::slice::from_raw_parts_mut(self.xs. as_mut_ptr().cast(), CAP) }
}
Option 2: Only Expose Uninitialized Regions
Remove as_uninit_slice_mut() entirely, or redesign it to only return unused capacity:
Unsoundness with
as_uninit_slice_mutHi, we found two soundness bugs, which both can trigger undefined behaviors via
as_uninit_slice_mut():as_uninit_slice_mut()allows to overwrite existing initialized elements, which can trigger UB.Drain::next()can make uninitialized memory exposure.Both can be triggered in pure-safe Rust code.
Direct UB via
as_uninit_slice_mut()arraydeque/src/lib.rs
Lines 1076 to 1078 in 2ccf305
This method exposes the whole buffer(capacity
CAP)as&mut [MaybeUninit<T>], which allows the caller to:TasMaybeUninit::uninit()droplen, which counts for initialized elementsPoC 1: Direct Read UB
UB in
Drain::next()viaDroparraydeque/src/lib.rs
Lines 2432 to 2435 in 2ccf305
Location: Line 2389 (in
Drain::drop)Location: Line 2074-2078 (in
ArrayDeque::drop)When
as_uninit_slice_mut()breaks the invariant hold bylenthenArrayDequeis dropped:[Drop::drop]()callsclear()(lib.rs#L2074-L2077)clear()callsdrain(..)Drain::dropcallsnext()next()callsptr::read(&T)whileTactuallys is uninitializedString(e.g.,Vec.cap)PoC 2: Drop UB
Miri Output:
error: Undefined Behavior: constructing invalid value at .vec.buf.inner.cap.0: encountered uninitialized memory, but expected an integer --> /home/usr/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/arraydeque-0.5.1/src/lib.rs:2434:45 | 2434 | self.iter.next().map(|elt| unsafe { ptr::read(elt) }) | ^^^^^^^^^^^^^^ Undefined Behavior occurred here | = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined BehaviorRecommended Fixes
Option 1: Make
as_uninit_slice_mut()Unsafe (Minimal Fix)Option 2: Only Expose Uninitialized Regions
Remove
as_uninit_slice_mut()entirely, or redesign it to only return unused capacity: