Casting mutable lifetime to immutable

We have mutable refs and immutable refs in Rust, so let's define a mutable lifetime as some lifetime 'a where &'a mut T for some type T and an immutable lifetime as some lifetime 'a where &'a T for some type T. These lifetimes have the same rules as their ref counter-parts: immutable lifetimes can overlap with each other but mutable lifetimes aren't allowed to overlap with any other lifetimes. It makes sense that only mutable refs can have mutable lifetimes and only immutable refs can have immutable lifetimes... but surprisingly the code below does not compile:

fn main() {
    let mut a = 10;
    let b: &i32 = &*(&mut a);
    let c: &i32 = &*(&mut a);
    dbg!(b, c); // compile error
}

playground

error[E0499]: cannot borrow `a` as mutable more than once at a time
 --> src/main.rs:4:21
  |
3 |     let b: &i32 = &*(&mut a);
  |                     -------- first mutable borrow occurs here
4 |     let c: &i32 = &*(&mut a);
  |                     ^^^^^^^^ second mutable borrow occurs here
5 |     dbg!(b, c);
  |          - first borrow later used here

I'm borrowing a as mutable at first but then immediately and unconditionally re-borrowing it as immutable and assigning it to an immutable ref variable. The initial mutable borrow of a doesn't live past the expression it's used in. I would expect the immutable ref variables b and c to have immutable lifetimes but it seems they have mutable lifetimes, as the compiler complains when they are used simultaneously.

My questions are:

  1. When re-borrowing a mut ref as an immut ref why does the immut ref maintain the mut ref's original mut lifetime?
  2. If the answer to the above is "it would be a soundness issue otherwise" then can you please demonstrate this soundness issue with a minimal complete concrete example using only safe Rust?

Thank you!

2 Likes

The problem is that you are making a mutable reference while you have an immutable reference. If you only make one mutable reference it will compile.

fn main() {
    let mut a = 10;
    let mut_ref_a = &mut a;
    let b: &i32 = &*(mut_ref_a);
    let c: &i32 = &*(mut_ref_a);
    dbg!(b, c);
}

playground

3 Likes

Note that even if the first mutable borrow ends before the second one begins, this is still not allowed because the second mutable borrow can't overlap with the first immutable borrow. For example, this also fails to compile (playground):

let mut a = 10;
let b: &i32 = &a;
let c: &i32 = &*(&mut a);
dbg!(b, c); // compile error
error[E0502]: cannot borrow `a` as mutable because it is also borrowed as immutable
 --> src/main.rs:4:17
  |
3 | let b: &i32 = &a;
  |               -- immutable borrow occurs here
4 | let c: &i32 = &*(&mut a);
  |                 ^^^^^^^^ mutable borrow occurs here
5 | dbg!(b, c); // compile error
  |      - immutable borrow later used here

I think it's clear why this can't be allowed under the aliasing rules, and neither can your original example. However, there are some cases that are not so clear. For example, the following code (playground) could be accepted if it were possible for the first mutable borrow to end while keeping its longer-lived immutable reborrow around:

let mut a = 10;
let b: &i32 = &*(&mut a);
let c: &i32 = &a;
dbg!(b, c); // compile error

However, this doesn't work with the current borrow checker, or with the Stacked Borrows model that is being developed to formalize Rust's aliasing rules. This limitation is discussed briefly in the Rustonomicon.

2 Likes

That is interesting. So you can create two immutable references from a single mutable reference but you can't create one immutable reference from the mutable reference and then reference the original value.

fn main() {
    let mut a = 10;
    let mut_ref_a = &mut a;
    let b: &i32 = &*(mut_ref_a);
    let c: &i32 = &*(mut_ref_a);
    dbg!(b, c); // compiles
}

This example kinda side-steps the issue. There's no coercion of mut_ref_a's mut lifetime into distinct immut lifetimes for b and c but b and c just extend the same mut lifetime provided by mut_ref_a so there's no overlap of mut lifetimes.

I tried to get @mbrubeck's second example to compile by putting the &mut a borrow into its own scoped block but it didn't work:

fn main() {
    let mut a = 10;
    let b: &i32;
    let c: &i32;
    {
        b = &*(&mut a);
        c = &a;
    }
    dbg!(b, c); // compile error
}

It seems like when you re-borrow a mut ref that mut lifetime never disappears, even if the scope it was originally borrowed inside ends, but it gets extended to cover the entire immut lifetime of the new immut ref. So b is stuck carrying &mut a's mut lifetime as well as its own immut lifetime, which is kinda the same thing as just having a mut lifetime. The worst of both worlds: an immut ref with a mut lifetime.

Okay, lesson learned, never re-borrow a mut ref as an immut ref.

I finally found an unsafe example for this, thanks to this post on the Rust internals forum:

use std::sync::Mutex;

#[derive(Debug)]
struct Struct {
    mutex: Mutex<String>
}

impl Struct {
    // downgrades mut self to shared str
    fn get_string(&mut self) -> &str {
        self.mutex.get_mut().unwrap()
    }
    fn mutate_string(&self) {
        // if Rust allowed downgrading mut refs to shared refs then
        // then the following line would invalidate any shared refs
        // returned from the get_string method
        *self.mutex.lock().unwrap() = "surprise!".to_owned();
    }
}

fn main() {
    let mut s = Struct {
        mutex: Mutex::new("string".to_owned())
    };
    let str_ref = s.get_string(); // mut ref downgraded to shared ref
    s.mutate_string(); // str_ref invalidated
    dbg!(str_ref); // compile error as expected
}

playground

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.