Is this "scoped" reduction of lifetime inside of a mutable reference sound?

I have a recursive algorithm with a State<'a> struct containing references.

struct State<'a> {
    x: &'a i32
}

At some points in the recursive algorithm, I would like to store data temporarily in the state struct just for the current recursion and deeper. I can do this by value, but in this case involves cloning data to move it into the state (real value is much more complex than i32).

I'd like to continue to store references, but in this case they are only valid for some shorter lifetime 'scope for the current recursion and deeper, not the 'a lifetime of the full algorithm.

I am exploring a possibility of a pattern which casts the &mut State<'a> to &mut State<'scope> ('scope is strictly shorter than 'a) with a guard which writes the shorter &'scope i32 reference into the struct, and then resets the original value of the field on drop.

Something like

impl State<'_> {
    /// Set a field in `self`, projected by the function `P`, to the value `new_value`.
    ///
    /// The original value of the field will be restored when `ScopedSetState` drops.
    pub fn scoped_set<'scope, P, T>(&'scope mut self, projector: P, new_value: T) -> ScopedSetState<'scope, P, T>
    where
        P: for<'p> Fn(&'p mut State<'scope>) -> &'p mut T,
    {
        // ScopedSetState contains `&'scope mut State<'scope>`, which is cast from `self`.
        // See playground link at bottom of post for full details.
    }
}

As long as great care is taken to not write references into other fields in the state with the shorter 'scope lifetime, I think this can be set up so that it's impossible to produce a dangling reference. (Probably I would do this by having an inner MutableState struct which has pub fields and can only be accessed in the closure passed to scoped_set.)

Miri seems happy, though I wanted to cast this out in case others could find reasons this is unsound anyway?

(Or other concerns I need to protect against?)

Here's a link to the playground.

Thanks for all insight here :folded_hands:

This is unsound if your scope object gets leaked Rust Playground

1 Like

Ah yes of course, thanks :+1:

Hmm thinking further, it's possible to make the guard inaccessible outside of the scoped_set API by making it take a closure. This is the same lesson we learning with std::thread::scope about how to prevent leaking.

So perhaps that offers a way forward here? Rust Playground

    // inner has a shorter lifetime than state, we write it to state
    // only for the duration of the inner closure
    state.with_scoped_set(|s| &mut s.x, &inner, |state| {
        println!("scoped state x is: {}", state.x);
    })

I think giving a &mut reference to the closure probably still makes it unsound because that allows to swap States and hence move one from an inner closure to an outer closure. The lifetime bounds might make this tricky, I'll see if I can come up with an example that breaks it.

2 Likes

The idea is you have something like

struct State<'a> {
    x: &'a i32,
    inaccessible_we_hope: &'a str,
}

but only x gets stashed and restored, correct?

Then yeah, if you can have two States, it's still unsound.

fn with_scoped_state(state1: &mut State<'_>) {
    let inner = 2;
    let local = "Hello".to_owned();
    let state2 = &mut State { x: &0, inaccessible_we_hope: &*local };
    state1.with_scoped_set(|s| &mut s.x, &inner, move |state1| {
        std::mem::swap(state1, state2);
    })
}
1 Like

Aha yep that's the refutation I needed, thanks both!