Is this slice of &mut [Box<T>] unsound?


#1

Here’s a tricky Rust question. Maybe you have an exampl ethat shows that this is unsound.

Here’s the pattern: Why don’t we allow borrowing &[Box<T>] as &[&T]? It seems useful and sound enough. What about the mutable version though?

The code is (playpen link)

#![feature(box_syntax)]

use std::mem;
use std::fmt::Debug;

/// This is convenient
fn sliceboxes<'a, T: ?Sized>(xs: &'a [Box<T>]) -> &'a [&'a T]
{
    unsafe {
        mem::transmute(xs)
    }
}

/// NOTE: Mutable version is unsound(?)
///
/// We might have an opportunity to swap a box for another non-boxed &mut T,
/// is that a disaster? Not sure.
fn sliceboxes_mut<'a, T: ?Sized>(xs: &'a mut [Box<T>]) -> &'a mut [&'a mut T]
{
    unsafe {
        mem::transmute(xs)
    }
}

fn main() {
    let xs = [box 1, box 2];

    println!("{:?}", sliceboxes(&xs));

    let ys: &[Box<Debug>] = &[box 1, box "hi"];

    println!("{:?}", sliceboxes(&ys));

    let zs: &mut [Box<Debug>] = &mut [box 1, box "hi"];
    {
        let zsl = sliceboxes_mut(zs);
        zsl.swap(0, 1)
    }

    println!("{:?}", sliceboxes(&zs));

    // If it is unsound, how do we demonstrate it?
    let (ws, mut string): (&mut [Box<Debug>], _) = (
        &mut [box 1, box 3],
        "hi".to_string());
    {
        let wsl = sliceboxes_mut(ws);

        wsl[1] = &mut string;
    }

    println!("{:?}", ws);
}

I wonder if the mutable version is sound. Intuition says it’s maybe not so good to swap an allocated-T box-T with a non-allocated-U box-U like swapping &mut 1 with Box<String>.

On the other hand, the type compatibility is very strict. We can only assign &mut T values of the exact same lifetime. Is this enough to make it sound?

  • Is the &[&T] version sound (looks easy – should be yes)

  • Is the mutable version sound?

  • Is it only sound, if we restrict the return value to -> &'a mut [&mut T]

  • Is the mutable version no good at all?


#2

Well, it doesn’t seem to like Drop:

#![feature(box_syntax)]

use std::mem;
use std::fmt::Debug;

fn sliceboxes_mut<'a, T: ?Sized>(xs: &'a mut [Box<T>]) -> &'a mut [&'a mut T]
{
    unsafe {
        mem::transmute(xs)
    }
}

#[derive(Debug)]
struct Test(&'static str);

impl Drop for Test {
    fn drop(&mut self) {
        println!("dropping {}", self.0)
    }
}

fn main() {
    let mut v = 1;
    let zs: &mut [Box<Debug>] = &mut [box 1, box Test("old value")];
    {
        let zsl = sliceboxes_mut(zs);
        zsl[0] = &mut v;
    }
}

Although I have no idea why making Test drop causes problems… I expected it to crash regardless when rust tries to deallocate the box but that doesn’t appear to be the case.


#3

I wonder if it would be possible to implement this with Deref:

impl<T> Deref for [Box<T>] {
    type Target = [&T];
    ...
}

I don’t think this would cause any coherence problems. To avoid coherence problems, this would need to be defined in libcore. However, Box is defined in liballoc which depends on libcore so we have a circular dependency… :frowning:


#4

libstd has a few tricky coherence problems like that. I think Add of str + String is the same.

I don’t think it’s possible to express it with Deref since the output type is [&T].

Either way, thank you for the crasher example! That’s always the best proof it’s unsound… unless something else is buggy.


#5

Good catch. There’s no way to get at the lifetime…