rustlanguage-lawyerunsaferefcell

Why doesn't std::cell::Ref use a reference instead of NonNull?


The std::cell::Ref struct in Rust is defined as follows:

pub struct Ref<'b, T: ?Sized + 'b> {
    // NB: we use a pointer instead of `&'b T` to avoid `noalias` violations, because a
    // `Ref` argument doesn't hold immutability for its whole scope, only until it drops.
    // `NonNull` is also covariant over `T`, just like we would have with `&T`.
    value: NonNull<T>,
    borrow: BorrowRef<'b>,
}

The // NB comment (I assume Nota bene / Nasty Bug or something?) implies that the following definition would not work, because it would be a noalias violation (do they mean the LLVM attributes in the backend?):

pub struct Ref2<'b, T: ?Sized + 'b> {
    value: &'b T,
    borrow: BorrowRef<'b>,
}

I don't understand this point, as I was under the impression that the non lexical lifetime semantics were properly preserved in code generation. Otherwise the following simple example (which of course compiles) would also be illegal, right?:

struct Foo<'a> {
    v: &'a i32,
}
fn foo(x: &mut i32) {
    let f = Foo { v: x };
    *x = 5; // value modified while the `noalias` f.v pointer is still in scope
}

Could somebody with more knowledge about the internals shed some light on this for me? I fear that I am misunderstanding something critical here, leading to potential issues in my own unsafe code.


Solution

  • Non lexical lifetimes were never a property of code generation. They were purely a borrow checker property, and borrow checking never impacts code generation.

    The example you provided is not illegal according to LLVM. The problem with noalias only manifests with function parameter, because only they get a noalias attribute (at least currently). So, the only way to write a problematic code without unsafe code will be:

    fn foo(reference: &String, mut data: String) {
        // Do not use `reference` anymore here.
        
        // Write to data.
        data = String::new();
    }
    
    fn main() {
        let mut v = String::new();
        foo(&v, v);
    }
    

    Except it doesn't compile, because you're moving a borrowed v. So there was actually no way to trigger a miscompilation.

    With RefCell however, we can do that:

    use std::cell::{RefCell, Ref};
    
    fn foo<'a>(reference: Ref<'a, i32>, data: &'a RefCell<i32>) {
        drop(reference);
        // Do not use `reference` anymore here.
        
        *data.borrow_mut() = 0;
    }
    
    fn main() {
        let v = RefCell::new(0);
        foo(v.borrow(), &v);
    }
    

    Which would be LLVM UB if Ref would use a reference.