Skip to content

'static reference to local variable that never goes out of scope? #565

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
joshlf opened this issue Apr 3, 2025 · 10 comments
Open

'static reference to local variable that never goes out of scope? #565

joshlf opened this issue Apr 3, 2025 · 10 comments

Comments

@joshlf
Copy link

joshlf commented Apr 3, 2025

Is the following sound? In particular, is it sound to synthesize a 'static reference to a local variable if we can prove that that variable will never go out of scope at runtime because the local scope will never finish executing?

fn with_static<T: 'static, F: FnOnce(&'static T)>(t: T, f: F) {
    let tp: *const T = &t;
    // SAFETY: The `loop {}` below ensures that `t` never goes
    // out of scope.
    let tr: &'static T = unsafe { &*tp };
    f(tr);
    loop {}
    drop(t)
}
@chorman0773
Copy link
Contributor

Lifetimes at the language level don't participate in the operational semantics, they only determines what code is well-formed.

Note that you'll also need to make sure that f doesn't stash the t in a variable and then unwind.

@joshlf
Copy link
Author

joshlf commented Apr 3, 2025

Ah good point.

@joshlf
Copy link
Author

joshlf commented Apr 3, 2025

Okay, do you think this is sound?

pub fn with_static<T: 'static, F: FnOnce(&'static mut T)>(t: T, f: F) -> ! {
    struct AbortOnDrop<T>(T);
    impl<T> Drop for AbortOnDrop<T> {
        fn drop(&mut self) {
            loop {}
        }
    }

    let mut t = AbortOnDrop(t);
    let tp: *mut T = &mut t.0;

    // SAFETY: Since we `drop(t)` after the `loop {}`, `t` will only go out of
    // scope if `f` unwinds. If this happens, `AbortOnDrop::drop` will `loop {}`
    // forever before its inner `T` is destructed. Thus, `t.0` will never be
    // destructed, and so it is sound to synthesize a `'static` reference to
    // `t`.
    f(unsafe { &mut *tp });

    loop {}
    drop(t);
}

@Lokathor
Copy link
Contributor

Lokathor commented Apr 3, 2025

if that drop works like how you seem to think it does, then you don't need to write the loop and drop call after the unsafe function call

@chorman0773
Copy link
Contributor

chorman0773 commented Apr 3, 2025

I wouldn't hold the T value in the guard, since the Drop will invalid any use of .0 (including concurrently).

In any case, from an opsem perspective, as mentioned, it's perfectly fine to "lie" about lifetimes, as long as you don't use the reference after the reference is invalidated (which happens when the storage is deallocated, probably on move, and definitely on a conflicting write). As I mentioned, the lifetimes don't exist at runtime, they're just static guides that make it so that things that aren't valid at runtime aren't valid at compile time.

@joshlf
Copy link
Author

joshlf commented Apr 3, 2025

I wouldn't hold the T value in the guard, since the Drop will invalid any use of .0 (including concurrently).

Interesting. I was assuming that the invalidation would happen after Drop::drop returned, at which point each field is destructed in turn. Since Drop::drop will never return, I assumed that wouldn't be an issue. Is my understanding incorrect?

@chorman0773
Copy link
Contributor

The move will probably invalidate it.
If you use drop_in_place instead, or the implicit end-of-scope drop, the mutable reference may invalidate the existing reference depending on the precise model

@joshlf
Copy link
Author

joshlf commented Apr 3, 2025

What guarantees are provided regarding drop order in the presence of unwinding? Is that something I could rely on?

@Lokathor
Copy link
Contributor

Lokathor commented Apr 3, 2025

I think drops have to be "bottom of the block upwards" regardless of if it's an unwind or a normal return. Otherwise you can trivially make situations that don't make sense.

@RalfJung
Copy link
Member

RalfJung commented Apr 4, 2025

is it sound to synthesize a 'static reference to a local variable if we can prove that that variable will never go out of scope at runtime because the local scope will never finish executing?

Yes. We even have a proof of that in RustBelt. :D You can even take in the t by reference and transmute &'a mut T to &'static mut T.

I would just remove the field from AbortOnDrop to avoid any kind of aliasing issues.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants