Slice, array and vector alignment (bug?)

Why does the following code work and not work depending on the length of the array or if I use a vector?

// useless_vec lint
let bytes = vec![0, 0, 0, 1, 0];

assert_eq!(std::mem::align_of::<[u8; 4]>(), 1);
assert_eq!(std::mem::align_of::<[u8; 5]>(), 1);

let slice = &bytes[0..4];
assert_eq!(std::mem::align_of_val(slice), 1);

// Passes for vec from 1 to 16,
// 4-long array from 1 to 4
// 5-long array from 1 to 2
for n in [1, 2, 4, 8, 16] {
    dbg!(n);
    assert_eq!((slice.as_ptr() as *const ()).align_offset(n), 0)
}

/// Copied from bytemuck
fn is_aligned_to(ptr: *const (), align: usize) -> bool {
    dbg!((ptr as usize) % align);

    // This is in a way better than `ptr as usize % align == 0`,
    // because casting a pointer to an integer has the side effect that it
    // exposes the pointer's provenance, which may theoretically inhibit
    // some compiler optimizations.
    dbg!(ptr.align_offset(align)) == 0
}

// Even when the slice's length is divisible by 4, this fails?
assert_eq!(slice.len() % 4, 0);
assert!(is_aligned_to(slice.as_ptr() as *const (), std::mem::align_of::<u32>()));

// As a consequence, this fails
let _: u32 = *bytemuck::from_bytes(slice);

Can I expect that all Vec's I get from the network are aligned for u32's? Or should I pack the struct or use pod_read_unaligned?

How does bytemuck::pod_read_unaligned work if alignment is required? i.e. I'm not using a packed struct

It is not required for pod_read_unaligned, as per the documentation, but you are not using that function. You are using from_bytes, which is just a ref-to-ref conversion, therefore it necessarily requires alignment.

1 Like

You can only expect it to be aligned to the value of std::mem::align_of::<T>(). If T is u8 that is 1, which is too small for being aligned for a u32.

It uses the read_unaligned method on raw pointers (or equivalently std::ptr::read_unaligned) which allows reading from unaligned pointers.

Specifically for reading a u32 from a slice of u8 you can just read a [u8; 4] from it and convert them to a u32 using u32::from_le_bytes/u32::from_be_bytes depending on the endianess used in the protocol. Also check out the byteorder crate which simplifies this process.

2 Likes

Thanks for the quick response! So read_unaligned copies the data to align it? Would this all work zero-copy if I made my struct packed?
And it's just coincidence that the slices of [u8; 4] and Vec<u8> happen to align to 4?
Is this code UB because it doesn't check alignment?

unsafe { &*(buffer.as_ptr() as *const TcpHeader) }

Yes, it performs a read which is technically a copy.

It would allow you to create a reference to it or to a slice of that type, so you will initially avoid a copy.

Yes, though it's not too unlikely. The way allocators work make it more likely for small allocations or allocations with size multiple of some power of 2 to be more aligned than requested, but this is just a coincidence and nothing guaranteed.

If the alignment of TcpHeader is not 1 and nothing guarantees the alignment of buffer to be bigger or equal to TcpHeader's then yes, it's unsound (it's UB if buffer happens to actually be less aligned than what TcpHeader requires)

3 Likes