Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to safely get an immutable byte slice from a `&mut [u32]`?

Tags:

rust

unsafe

In a rather low level part of a project of mine, a function receives a mutable slice of primitive data (&mut [u32] in this case). This data should be written to a writer in little endian.

Now, this alone wouldn't be a problem, but all of this has to be fast. I measured my application and identified this as one of the critical paths. In particular, if the endianness doesn't need to be changed (since we're already on a little endian system), there shouldn't be any overhead.

This is my code (Playground):

use std::{io, mem, slice};

fn write_data(mut w: impl io::Write, data: &mut [u32]) -> Result<(), io::Error> {
    adjust_endianness(data);

    // Is this safe?
    let bytes = unsafe {
        let len = data.len() * mem::size_of::<u32>();
        let ptr = data.as_ptr() as *const u8;
        slice::from_raw_parts(ptr, len)
    };

    w.write_all(bytes)
}

fn adjust_endianness(_: &mut [u32]) {
    // implementation omitted
}

adjust_endianness changes the endianness in place (which is fine, since a wrong-endian u32 is garbage, but still a valid u32).

This code works, but the critical question is: Is this safe? In particular, at some point, data and bytes both exist, being one mutable and one immutable slice to the same data. That sounds very bad, right?

On the other hand, I can do this:

let bytes = &data[..];

That way, I also have those two slices. The difference is just that data is now borrowed.

Is my code safe or does it exhibit UB? Why? If it's not safe, how to safely do what I want to do?

like image 870
Lukas Kalbertodt Avatar asked May 02 '19 10:05

Lukas Kalbertodt


1 Answers

In general, creation of slices that violate Rust's safety rules, even briefly, is unsafe. If you cheat the borrow checker and make independent slices borrowing the same data as & and &mut at the same time, it will make Rust specify incorrect aliasing information in LLVM, and this may lead to actually miscompiled code. Miri doesn't flag this case, because you're not using data afterwards, but the exact details of what is unsafe are still being worked out.

To be safe, you should to explain the sharing situation to the borrow checker:

let shared_data = &data[..];

data will be temporarily reborrowed as shared/read-only for the duration shared_data is used. In this case it shouldn't cause any limitations. The data will keep being mutable after exiting this scope.

Then you'll have &[u32], but you need &[u8]. Fortunately, this conversion is safe to do, because both are shared, and u8 has lesser alignment requirement than u32 (if it was the other way, you'd have to use align_to!).

let shared_data = &data[..];
let bytes = unsafe {
    let len = shared_data.len() * mem::size_of::<u32>();
    let ptr = data.as_ptr() as *const u8;
    slice::from_raw_parts(ptr, len)
};
like image 57
Kornel Avatar answered Nov 11 '22 10:11

Kornel