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:
- Make
new an unsafe fn with a # Safety contract on F/T.
- Bound
F/T on a bitwise-transmutability trait (bytemuck::Pod, zerocopy::FromBytes + IntoBytes + Immutable).
- Restrict to a sealed internal trait over the primitive types the kernels actually use.
Discovered during unsafe review.
ReinterpretSink::newis a safe constructor with no bounds onF/T— it only asserts matching size and alignment:vortex/vortex-compute/src/lane_kernels/sink.rs
Lines 62 to 85 in 133aacd
and the
IndexedSinkimpl requires onlyF: Copy, T: Copy:vortex/vortex-compute/src/lane_kernels/sink.rs
Lines 102 to 113 in 133aacd
Copydoes not imply freedom from validity invariants (unlikebytemuck::Podorzerocopy::FromBytes + IntoBytes), soset_uncheckedcan write aTbit pattern into anFslot that cannot legally hold it.map_into_in_placeis safe, so the caller needs nounsafe:Sizes and alignments match, so the asserts pass. Same applies to
char,NonZero*, fieldless enums, and&'static T.Related:
ReinterpretSinkholdsslice: &'a mut [F]while the memory is written asT, andget_uncheckedafterset_uncheckedreads thoseTbits back asF.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
unsafeon the caller side.Possible fixes:
newanunsafe fnwith a# Safetycontract onF/T.F/Ton a bitwise-transmutability trait (bytemuck::Pod,zerocopy::FromBytes + IntoBytes + Immutable).