Skip to content

Soundness: ReinterpretSink::new lets safe code produce invalid values (bool, char, NonZero*) #9995

Description

@jianhe25

Discovered during unsafe review.

ReinterpretSink::new is a safe constructor with no bounds on F/T — it only asserts matching size and alignment:

impl<'a, F, T> ReinterpretSink<'a, F, T> {
/// Construct a `ReinterpretSink` from `&mut [F]`.
///
/// # Panics
///
/// Panics if `size_of::<F>() != size_of::<T>()` or
/// `align_of::<F>() != align_of::<T>()`.
pub fn new(slice: &'a mut [F]) -> Self {
assert_eq!(
size_of::<F>(),
size_of::<T>(),
"ReinterpretSink requires F and T to have the same size",
);
assert_eq!(
align_of::<F>(),
align_of::<T>(),
"ReinterpretSink requires F and T to have the same alignment",
);
Self {
slice,
_phantom: PhantomData,
}
}
}

and the IndexedSink impl requires only F: Copy, T: Copy:

impl<F: Copy, T: Copy> IndexedSink for ReinterpretSink<'_, F, T> {
type Write = T;
#[inline]
unsafe fn set_unchecked(&mut self, i: usize, value: T) {
// SAFETY: caller guarantees i < self.slice.len(); `new` enforces
// size_of::<F>() == size_of::<T>() and align_of::<F>() == align_of::<T>(),
// so the F-slot can hold a `T` without overflow or misalignment.
unsafe {
let ptr = self.slice.as_mut_ptr().add(i) as *mut T;
ptr.write(value);
}
}

Copy does not imply freedom from validity invariants (unlike bytemuck::Pod or zerocopy::FromBytes + IntoBytes), so set_unchecked can write a T bit pattern into an F slot that cannot legally hold it. map_into_in_place is safe, so the caller needs no unsafe:

let mut bools = [false; 4];
ReinterpretSink::<bool, u8>::new(&mut bools).map_into_in_place(|_| 42u8);
let _ = bools[0]; // UB: 0x2A is not a valid bool

Sizes and alignments match, so the asserts pass. Same applies to char, NonZero*, fieldless enums, and &'static T.

Related: ReinterpretSink holds slice: &'a mut [F] while the memory is written as T, and get_unchecked after set_unchecked reads those T bits back as F.

This requires deliberately instantiating with a validity-constrained type, so it's a public-API soundness hole rather than a live miscompile in the current kernels — but it's reachable with zero unsafe on the caller side.

Possible fixes:

  1. Make new an unsafe fn with a # Safety contract on F/T.
  2. Bound F/T on a bitwise-transmutability trait (bytemuck::Pod, zerocopy::FromBytes + IntoBytes + Immutable).
  3. Restrict to a sealed internal trait over the primitive types the kernels actually use.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions