Rust reborrow mutable reference

Why can the following two functions:

struct S;

fn f2sr<'a, 'b>(rb: &'b &'a S) -> &'a S
{
    *rb
}

fn f2sr_mut<'a, 'b>(rb: &'b &'a mut S) -> &'a S
{
    *rb
}

The first function compiles normally, but the second one gives a compilation error.
The error message for the second function is:

error: lifetime may not live long enough
  --> src\main.rs:49:5
   |
41 | fn f2sr<'a, 'b>(rb: &'b &'a mut S) -> &'a S // err
   |         --  -- lifetime 'b defined here
   |         |
   |         lifetime 'a defined here
...
49 |     *rb
   |     ^^^ function was supposed to return data with lifetime 'a but it is returning data with lifetime 'b
   |
   = help: consider adding the following bound: 'b: 'a

But when I change the second function's signature to:
fn f2sr<'a, 'b: 'a>(rb: &'b &'a mut S) -> &'a S, the compiler no longer throws an error. Why is this?

1 Like

If this compiled:

fn f2sr_mut<'a, 'b>(rb: &'b &'a mut S) -> &'a S

Then after 'b "expired", the &'a mut S would be usable again, but so would the returned value, which is a &'a S. &mut _ are exclusive references -- it is undefined behavior to have an active &mut _ and an active &_ to the same place. So that is why it is a compiler error.

When you have a nested &'b &'a mut S, there is an implicit 'a: 'b bound. When you add the 'b: 'a bound, in combination, you're effectively saying 'a == 'b. So it's effectively the same as this signature:

fn f2sr<'a>(rb: &'a &'a mut S) -> &'a S,

And it's okay to shared-reborrow through a &'a _ for 'a.

1 Like

To illustrate what kind of trouble you can get into if this weren't forbidden (I'll creatively redefine struct S(Vec<i32>) for illustration):

fn oops() {
    let mut mr = &mut S(vec![10]);
    let r = f2sr_mut(&mr); // make overlapping references
    let ri = &r.0[0]; // refers to 10
    mr.0 = vec![4]; // deallocates that vector (10 is now gone)
    println!("{}", ri); // boom!
}

oops doesn't break any rules (it compiles if we assume f2sr_mut exists), but it clearly has UB, so f2sr_mut must be incorrect.

4 Likes

We can implement it with unsafe and it explodes in Miri where the // boom! comment is :slight_smile:

1 Like

Here, our understanding already differs:

rb: &'b &'a mut S

I believe that 'b does not represent the lifetime of rb itself, but rather the lifetime of the memory that rb points to. For example, in x: &'a T, it indicates the lifetime of the T value that x points to.

According to your previous description, 'b clearly refers to the lifetime(liveness scope) of rb itself. Consider the following code:

fn foo() {
    let xxx: &'static str = "Hello";
}

The liveness scope of the local variable xxx, which is a reference type, is not 'static. We can only say that the liveness scope (or lifetime) of the string literal that xxx points to is 'static.

'b is neither the actual lifetime of rb nor the actual lifetime of the memory rb points to. 'b represents some lifetime, a "region" of the program, that is big enough to encompass all the "uses" of the referent *rb, but small enough to fit "inside" 'a.

Lifetime parameters are not concrete lifetimes; they are constraints that the compiler can fill in however it wants.

'static on the other hand is not a lifetime parameter, it is a concrete lifetime (the only one, in fact), so let xxx: &'static str is a different case than rb: &'b &'a mut S (where 'b and 'a are parameters, filled in by the compiler at the call site).

If 'static in let xxx: &'static str is a concrete lifetime, does it represent the liveness scope of the variable xxx, or the liveness scope of the string literal?

It, again, represents the constraint:

  • variable xxx must not live longer than 'static (this is trivially true for any variable);
  • str, i.e. bytes it refers to, must not live shorter that 'static (i.e. must be available until the program termination).
1 Like

The way I like to describe this is that the lifetime is of the borrow — not the reference, but the specific operation that obtained it (use of the & operator, or equivalent implicit borrows). This is important when considering mutable borrows, which must be unique — it's not just that the borrow must not outlive the referent, but it also must not overlap with any other borrow.

3 Likes

This is probably an echo of what the other replies said by now.

The name "lifetime" unfortunately implies that it's about the liveness scope of values, but that is not the case. Rust lifetimes -- those '_ things -- are generally about the duration of borrows. The connection between liveness scopes and Rust lifetimes is primarily that going out of scope is incompatible with being borrowed.[1]

This is why lifetimes of reference types typically end before the referent goes out of scope (but the analysis is sophisticated enough for exceptions, as the borrow durations actually calculated can be shorter than the associated lifetime in the types).

Consider the following counterexample:

fn foo<'not_static>(_proof_if_you_want_it: &'not_static str) {
    let xxx: &'static str = "Hello";
    let yyy: &'not_static str = &*xxx;
}

fn prover() { let local = String::new(); foo(&*local); }

That shows that the lifetime of a reference type can be less than the referent, which is probably not too surprising. A recent other thread has an example of a move happening with the "gap" of a lifetime.

You can conclude that the referent will never be moved or destructed or (as there is no shared mutability or generics here) mutated.[2] The referent could be some static memory, or it could be some leaked memory.[3]


  1. as is being moved or having a &mut _ taken ↩︎

  2. or that someone has committed unsoundness elsewhere, by e.g. unsafely transmuting a lifetime ↩︎

  3. or it could be empty -- no memory ↩︎

1 Like