Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can I coerce a lifetime parameter to a shorter lifetime (soundly) even in the presence of `&mut T`?

Tags:

rust

lifetime

I'm trying to make a tree with parent pointers in Rust. A method on the node struct is giving me lifetime issues. Here's a minimal example, with lifetimes written explicitly so that I can understand them:

use core::mem::transmute;

pub struct LogNode<'n>(Option<&'n mut LogNode<'n>>);

impl<'n> LogNode<'n> {
    pub fn child<'a>(self: &'a mut LogNode<'n>) -> LogNode<'a> {
        LogNode(Some(self))
    }

    pub fn transmuted_child<'a>(self: &'a mut LogNode<'n>) -> LogNode<'a> {
        unsafe {
            LogNode(Some(
                transmute::<&'a mut LogNode<'n>, &'a mut LogNode<'a>>(self)
            ))
        }
    }
}

(Playground link)

Rust complains about child...

error[E0495]: cannot infer an appropriate lifetime for lifetime parameter 'n due to conflicting requirements

...but it's fine with transmuted_child.

I think I understand why child won't compile: the self parameter's type is &'a mut LogNode<'n> but the child node contains an &'a mut LogNode<'a>, and Rust doesn't want to coerce LogNode<'n> to LogNode<'a>. If I change the mutable references to shared references, it compiles fine, so it sounds like the mutable references are a problem specifically because &mut T is invariant over T (whereas &T is covariant). I guess the mutable reference in LogNode bubbles up to make LogNode itself invariant over its lifetime parameter.

But I don't understand why that's true—intuitively it feels like it's perfectly sound to take LogNode<'n> and shorten its contents' lifetimes by turning it into a LogNode<'a>. Since no lifetime is made longer, no value can be accessed past its lifetime, and I can't think of any other unsound behavior that could happen.

transmuted_child avoids the lifetime issue because it sidesteps the borrow checker, but I don't know if the use of unsafe Rust is sound, and even if it is, I'd prefer to use safe Rust if possible. Can I?

I can think of three possible answers to this question:

  1. child can be implemented entirely in safe Rust, and here's how.
  2. child cannot be implemented entirely in safe Rust, but transmuted_child is sound.
  3. child cannot be implemented entirely in safe Rust, and transmuted_child is unsound.

Edit 1: Fixed a claim that &mut T is invariant over the lifetime of the reference. (Wasn't reading the nomicon right.)

Edit 2: Fixed my first edit summary.

like image 579
ashtneoi Avatar asked Jan 01 '23 07:01

ashtneoi


1 Answers

The answer is #3: child cannot be implemented in safe Rust, and transmuted_child is unsound¹. Here's a program that uses transmuted_child (and no other unsafe code) to cause a segfault:

fn oops(arg: &mut LogNode<'static>) {
    let mut short = LogNode(None);
    let mut child = arg.transmuted_child();
    if let Some(ref mut arg) = child.0 {
        arg.0 = Some(&mut short);
    }
}

fn main() {
    let mut node = LogNode(None);
    oops(&mut node);
    println!("{:?}", node);
}

short is a short-lived local variable, but since you can use transmuted_child to shorten the lifetime parameter of the LogNode, you can stuff a reference to short inside a LogNode that should be 'static. When oops returns, the reference is no longer valid, and trying to access it causes undefined behavior (segfaulting, for me).


¹ There is some subtlety to this. It is true that transmuted_child itself does not have undefined behavior, but because it makes other code such as oops possible, calling or exposing it may make your interface unsound. To expose this function as part of a safe API, you must take great care to not expose other functionality that would let a user write something like oops. If you cannot do that, and you cannot avoid writing transmuted_child, it should be made an unsafe fn.

like image 96
trent Avatar answered Jan 02 '23 20:01

trent