Is transmuting &[Box<T>] to &[&T] valid or UB?

Say you have a slice of Boxes. Practically speaking, in terms of memory layout, a Box is a pointer or a fat pointer. A ref is the same thing.

So if you take that initial slice, and convert it to a list of refs (because that's what a function wants), from a memory content perspective, you end up with the same thing.

That is:

use libc;

fn main() {
    let array = [Box::new(42u32), Box::new(21)];
    let array2: Vec<&u32> = array.iter().map(|x| &**x).collect();
    println!("{}", unsafe { libc::memcmp(array.as_ptr() as _, array2.as_ptr() as _, 8) });
}

prints 0 (no difference).

That intermediate Vec is, however, rather a waste of time and space. So, you'd theorize that you can probably transmute to get what you want... and it actually works:

fn main() {
    let array = [Box::new(42u32), Box::new(21)];
    let array: &[&u32] = unsafe { std::mem::transmute(&array[..]) };
    println!("{:?}", array);
}

miri is even happy about it. But here's the question: is it a shortcoming of miri or is it legitimately not UB?

Going even further, is it still valid for fat pointers?

fn main() {
    let array = [String::from("hello").into_boxed_str(), String::from("world").into_boxed_str()];
    let array: &[&str] = unsafe { std::mem::transmute(&array[..]) };
    println!("{:?}", array);
}

Again, miri is happy about it. But is it legitimate?

Edit: and if it's definitely not UB, should the standard library provide it as a supported thing? I've had to do the "collect refs in a temporary Vec" thing too many times.

2 Likes

It doesn't look like it could be UB for the cases provided.

edit: I had a bad counterexample that I removed.

The layout of wide pointers isn't guaranteed, so you could in principle have a different order between Box<Unsized> and &Unsized.

There's a safe transmute working group, which (despite the staleness of the repo) is apparently still active. This seems like it would fall under that.

6 Likes

Is that actually true? After all, the content of a Box is a (fat) pointer (via Unique via NonNull).

That's an implementation detail.

If there's not a documented guarantee, it's not guaranteed.

The trajectory is away from such layout guarantees, in favor of APIs like slice_from_raw_parts (and Box::from_raw/into_raw).

4 Likes

Using dubious transmutes for "performance" (presumed, un-benchmarked micro-optimizations) is a Very Bad Idea™.

Know your standard library.

    let array = [Box::new(42u32), Box::new(21)];
    let array2 = array.each_ref().map(Box::as_ref);

No additional allocations in sight.

2 Likes

Relevant issue: Is transmuting `&’a &’b T` to `&’a &’b mut T` sound or UB? · Issue #270 · rust-lang/unsafe-code-guidelines · GitHub

1 Like

Transmuting fat pointers is gray area. I would not do it.

But I would personally be happy about a method like this:

fn as_refs<T>(boxes: &[Box<T>]) -> &[&T] {
    let len = boxes.len();
    let ptr = boxes.as_ptr();
    unsafe { std::slice::from_raw_parts(ptr.cast(), len) }
}

Here we don't allow unsized T so the boxes are thin pointers, and we're using slice::from_raw_parts instead of transmute to convert the slice. So there are no transmutes of fat pointers involved.

7 Likes

That only works because my example is over-simplified. In the real world, you'd already start from a Vec or a slice, not an array. Also, your example relies on the optimizer getting rid of the intermediate arrays. But yes, of course your example doesn't allocate, since it's all arrays on the stack.

1 Like

I'm not sure it's all that relevant, as that issue involves transmuting to &mut.

The base issue is still mostly the same, both are about transmuting a &T to another pointer-like type (&mut T/Box<T>, both with stricter uniqueness requirements than &T), while being themselves behind another shared reference (the &'a part in the issue, or the &[...] in your case).

I think fn(&[Box<T>]) -> &[T] and fn(&mut [Box<T>]) -> &mut [&mut T] can be PRed to the standard library, they sound very useful, simple, and close to unspecced behavior

I guess you meant fn(&[Box<T>]) -> &[&T]?

This is unsound, as you could write a &mut T where a Box<T> was expected. This can only work if the outer reference is shared, i.e. fn(&[Box<T>]) -> &[&mut T], though I don't think this would be that useful.

2 Likes

You're definitely right on both accounts :slight_smile:

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.