Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Method-based approach

Warning

This article contains an older approach that has been abandoned.

Today, exclusive references can be implemented in user-land using a method-based approach:

#![allow(unused)]
fn main() {
trait Reborrow {
    fn reborrow(&mut self) -> Self;
}
}

This captures the most important features of reborrowing: a source instance self has exclusive access asserted on it, and a new Self is produced from it (Some formalisations allow the method to choose its own result type using a Self::Target ADT: this is arguably mixing up reborrowing with a generalised Deref operation). However, this approach comes with downsides: the method requires an explicit &mut self reference which bounds the resulting Self’s lifetime to the calling function, and the method is user-overridable which leads to arguably non-idiomatic “spooky action” and a possibility of misuse.

Bounded lifetimes

When the fn reborrow method is called in some outer function fn outer_fn, The outer function must create a &mut T reference pointing to the value being reborrowed:

#![allow(unused)]
fn main() {
fn outer_fn<'a>(t: CustomMut<'a, u32>) -> &'a u32 {
    // This:
    inner_fn(t.reborrow());
    // ... is equivalent to this:
    let t_mut = &mut t;
    inner_fn(t_mut.reborrow())
}
}

This means that the fn reborrow method is given a reference pointing to a local value, effectively a pointer onto the stack. The compiler must make sure that this pointer does not outlive the stack, which then means that the lifetime of the resulting Self created by fn reborrow cannot outlive the function in which it was created in. In the above example, this means that trying to return the result of inner_fn will not compile because of the fn reborrow call, citing “returns a value referencing data owned by the current function”.

Compare this to Rust references: the compiler understands that the result of reborrowing a &mut produces a new reference that can be extended to the original reference’s lifetime. This function compiles despite an explicit reborrow being performed by the &mut *t code.

#![allow(unused)]
fn main() {
fn outer_fn<'a>(t: &'a mut u32) -> &'a u32 {
    inner_fn(&mut *t)
}
}

We can make the code not compile by explicitly creating a &mut t reference:

#![allow(unused)]
fn main() {
fn outer_fn<'a>(mut t: &'a mut u32) -> &'a u32 {
    // no longer compiles: returns a value referencing data owned by the current
    // function
    inner_fn(&mut t)
}
}

In user-land code bases that use the explicit fn reborrow method, there exists a way to fix this issue: by simply removing the fn reborrow method call, the original example code will compile. But knowing of this fix requires some deeper understanding of the fn reborrow method and the borrow checker: it is not an obvious or clean fix.

Most importantly, if the Rust compiler is in charge of automatically injecting fn reborrow calls at appropriate use-sites, then it may not be feasible for the compiler to perform the analysis to determine if a particular call should be removed or not. Furthermore, in a post-Polonius borrow checker world it will become possible for code like this to compile:

#![allow(unused)]
fn main() {
fn inner_fn<'a>(t: &'a mut u32) -> Result<&'a u32, &'a u32> {
    // ...
}

fn outer_fn<'a>(t: &'a mut u32) -> Result<&'a u32, &'a u32> {
    let result = inner_fn(t)?;
    if result > 100 {
        Ok(t)
    } else {
        Err(t)
    }
}
}

This means that a reborrow of t must always happen, yet the lifetime expansion of the result of inner_fn depends on whether the function returns Ok or Err. In the Err branch the result’s lifetime must expand to that of the source t, but in the Ok branch it must shrink to re-enable usage of the source t. The method-based approach to Reborrow traits will not work here, as the compiler cannot choose to call the method based on a result value that depends on the call’s result.

One could argue that the compiler could simply extend the lifetime of the method call’s result, as it should only do good deeds. This may well open a soundness hole that allows safe Rust code to perform use-after-free with stack references.

User-controlled code

The second downside of the method-based approach is that reborrowing is fully invisible in the source code, and having user-controlled code appear where there is none visible is not very idiomatic Rust.

Consider eg. the following code:

struct Bad;

impl Reborrow for Bad {
    fn reborrow(&mut self) -> Self {
        println!("I'm bad!");
        Self
    }
}

fn main() {
    let bad = Bad;
    let bad = bad;
}

Depending on the exact implementation choices, this might print out the “I’m bad” message. This is especially problematic if the compiler chooses to trust that fn reborrow methods never store their &mut self reference given to them and allows lifetime extension to happen:

#![allow(unused)]
fn main() {
struct Unsound<'a>(&'a mut &'a u32, u32);

// Note: lifetime for trait here to show that the compiler specially trusts that
// these lifetimes indeed are fully unifiable.
impl<'a> Reborrow<'a> for Unsound<'a> {
    fn reborrow(&'a mut self) -> Self {
        let data = &self.1;
        self.0 = data;
        Self(self.0, self.1)
    }
}
}

This would absolutely not compile today, but if the compiler did truly trust that reborrow methods can do no wrong then something like this might just pass through the compiler’s intentional blindspot and become a soundness hole.