Miri seems ok with returning "reinterpret" casting self, should it be?

I was pleasantly surprised this hairbrained idea seems ok to miri, but I want to double check that it's not just that it's missing something :slight_smile:

#![allow(dead_code)]

use std::mem::MaybeUninit;

struct Foo {
    x: i64,
    y: i64,
}

struct FooProjection1 {
    x: MaybeUninit<i64>,
    y: MaybeUninit<i64>,
}

struct FooProjection2 {
    x: i64,
    y: MaybeUninit<i64>,
}

impl FooProjection1 {
    fn set_x(&mut self, x: i64) -> &mut FooProjection2 {
        self.x = MaybeUninit::new(x);
        unsafe { &mut *(self as *mut Self as *mut FooProjection2) }
    }
}

impl FooProjection2 {
    fn set_y(&mut self, y: i64) -> &mut Foo {
        self.y = MaybeUninit::new(y);
        unsafe { &mut *(self as *mut Self as *mut Foo) }
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn is_this_allowed() {
        let mut foo_in_progress = FooProjection1 {
            x: MaybeUninit::uninit(),
            y: MaybeUninit::uninit(),
        };
        let finished_foo = foo_in_progress.set_x(10).set_y(42);
        finished_foo.x += 1;
        finished_foo.y += 1;
    }
}

I'm at least a little surprise that I didn't have to do something like:

impl FooProjection1 {
    fn set_x(&mut self, x: i64) -> &mut FooProjection2 {
        self.x = MaybeUninit::new(x);
        let p = self as *mut Self as *mut FooProjection2;
        drop(&mut self); // drop the self reference
        unsafe { &mut *p }
    }
}

In order to make it clear that there aren't two simultaneous &mut to the same object.

It's ok to Miri because it checks the actual layouts the compiler chose to use for your compilation, not whether what you're doing is stable to allowed layout changes.

You should put #[repr(C)] on the three structs to say that you're depending on them having consistent layouts.

rustc doesn't currently randomize field ordering for repr(Rust) structs, but it's allowed to. (Just like how it didn't used to reorder fields in repr(Rust) structs, but now it does.)

EDIT: bjorn3 points out below that there's a nightly flag that can randomize layout now!

4 Likes

That makes sense, will do, but why didn't I have to drop(&mut self)? Doesn't this technically mean two &mut to the same object coexist?

Well, &mut _ isn't needs_drop, so that drop doesn't really do anything.

And now that we have NLL, borrows end at the last use of the borrow, not at the end of the lexical block.

So I think it's ok, as the borrow is arguably gone after the as cast.

1 Like

Nit: Arguably the &mut is gone.

(The borrow lasts beyond the function, exercised by the return value... or at least, that's how I interpret the meaning of "the borrow".)

This function signature

which is identical with below signature due to the lifetime elision rules

fn set_x<'a>(&'a mut self, x: i64) -> &'a mut FooProjection2

means the self is uniquely borrowed by the returned &mut FooProjection2 reference. So the self is not usable while the returned reference is alive. You don't need to do anything more within the function body to prevent coexistent.

1 Like

This was my intention, I just wasn't sure because:

  • Normally you'd be returning a reference to some inner part of self, not a cast of self to a new type, and the borrow checker would enforce not using the outer &mut while inner still exists.

  • Lexically they both exist before the "transfer" happens while still inside the method.

Yup I was only thinking for satisfying the borrow checker.

This feels a little weird just because I'd expect self to stick around for the duration of a method (and it does from the POV of calling code), but it makes sense.

I think it's kinda hard to say -- the first borrow is arguably over, as &mut*p is technically a fresh borrow of unbound lifetime, which is then constrained to the same lifetime as the input by the function signature when it's returned.

I'm interpreting the borrows here as the lifetimes basically, so the &mut self borrow has to be the same as that of the returned reborrow, as per the function signature. The fact that self is dead doesn't mean the borrow wasn't for that long (though it may be crucial in avoiding aliasing). The fact that the borrowed-thing got reborrowed doesn't either. The returned borrow can be completely unrelated and your input borrow still has to be long enough.

Probably we just have a semantic mismatch, but I noted it due to a common misconception.

There is the -Zrandomize-layout flag which does randomize it. It is still rather limited in what layout changes it makes though.

2 Likes