How to connect global static variables to lifetimed objects

Hi, this is a new thread that follows-up on rc::Weak extending lifetime? - #5 by godmar as advised by rc::Weak extending lifetime? - #6 by alice

I have a global table that must refer to closures that I don't want to have a 'static lifetime. I do not want that because doing so would (in my opinion) make my entire application 'static. To that end, I was hoping to use weak references (as in the referenced thread). As was discussed there, however, weak references do not work - the recreated object will have the same lifetime as the original object.

I'm looking for a way to - perhaps an alternative abstraction - that allows me to accomplish my goal. In the code below (link to working playground; link to playground I'd love to see working); I'd like for Application::new to return a Application<'app> not a Application<'static> as it currently does.

For the example, I've wrapped Application in a Rc<RefCell<>> as per Ch. 15 Rust book since I need this for other parts of my (real) application, but it should not be related to the question at hand.

To make the example self-contained and motivated, I've included libc signal handling code modeled after vorner's signal handling crate but it is not related to my question.

use std::collections::HashMap;
use std::sync::Arc;
use std::os::raw::c_int;
use std::mem;
use std::{cell::RefCell, rc::Rc};

// 
// GlobalData follows in part 
// https://github.com/vorner/signal-hook/blob/master/signal-hook-registry/src/lib.rs
//
struct GlobalData {
    table: HashMap<c_int, Arc<dyn Fn()>>
}

impl GlobalData {
    fn get() -> &'static mut Self {
        unsafe { GLOBAL_DATA.as_mut().unwrap() }
    }
    fn ensure() -> &'static mut Self {
        unsafe {
            GLOBAL_DATA = Some(GlobalData{
                table: HashMap::new()
            });
        }
        Self::get()
    }
}

static mut GLOBAL_DATA: Option<GlobalData> = None;

fn install(signal: c_int, handler: Arc<dyn Fn()>)
{
    let gdata = &mut GlobalData::ensure();
    gdata.table.insert(signal, handler);
}

extern "C" fn handler(signal: c_int) {
    let actiontable = &GlobalData::get().table;
    match actiontable.get(&signal) {
        Some(action) => action(),
        None => panic!("no handler")
    }
}

struct Application<'app> {
    name: &'app str,  // just an example of state possibly carried by Application
}

impl <'app> Application<'app> {
    fn work(&self) {
        println!("Alarm went off in {}", self.name);
    }

    // want:
    fn new(name: &'app str) -> Rc<RefCell<Application<'app>>> {
    // have:
    //fn new(name: &'static str) -> Rc<RefCell<Application<'static>>> {
        let app = Rc::new(RefCell::new(Application{ 
            name 
        }));
        let weak = Rc::<RefCell<Application>>::downgrade(&app.clone());
        let signalhandler = move || {
            println!("signal handler called!");
            let appu = weak.upgrade();
            match appu {
                Some(uapp) => uapp.borrow().work(),
                None => panic!("user error, app no longer alive")
            }
        };

        install(libc::SIGALRM, Arc::from(signalhandler));
        app
    }
}

fn main() {
    // install a signal handler for SIGALRM
    unsafe {
        let mut action: libc::sigaction = mem::zeroed();
        action.sa_sigaction = handler as usize;
        action.sa_flags = libc::SA_RESTART;
        if libc::sigaction(libc::SIGALRM, &action, std::ptr::null_mut()) != 0 {
            panic!("cannot set signal handler");
        }
    }

    let _app = Application::new("my application");

    println!("Arming timer...");
    unsafe { 
        libc::alarm(1); 
        libc::sleep(2); 
    }
}

(Playground)

I believe the step you're missing is something along the lines of:

        let weak = unsafe {
            ::core::mem::transmute::<
                rc::Weak<RefCell<Application<'app>>>,
                rc::Weak<RefCell<Application<'static>>>,
            >(weak)
        };

Which is generally not sound, but in your case I believe it is fine: indeed, the rc::Weak gets moved into a closure, from where, in a single-threaded fashion (because Rc), the upgrades remain short-lived.

A possible "generalization"?

There may be a way to generalize over such an API, although correctly expressing the generalization requires HKT, since we are talking about an operation on a type constructor (erasing the lifetime parameter of a generic type). So the following example will not be generic over type-constructors/generic-types, but, instead, will hard-code the Application type (but without the RefCell, since it is inconsequential here):

//! Note, for the whole API to be _usable_, we need the generic type
//! to be covariant over its lifetime parameter.

pub
struct LifetimeErasedWeak /* = */ (
    rc::Weak<App<'static>>,
);

impl LifetimeErasedWeak {
    pub
    fn new<'app> (weak: rc::Weak<App<'app>>)
      -> Self
    {
        Self(unsafe { ::core::mem::transmute(weak) })
    }

    pub
    fn with_upgrade<R, Scope> (
        self: &'_ Self,
        yield_: Scope,
    ) -> R
    where
        Scope : for<'scope> FnOnce(Option<Rc<App<'scope>>>) -> R,
        Scope : NoRcApp, // <- is this really needed?
    {
        yield_(self.0.upgrade())
    }
}

unsafe
trait NoRcApp {}

impl !NoRcApp for Rc<App<'_>> {}

I believe such an API to be sound; moreover, I believe the "NoRcApp captured" trait is un-necessary, since in order to capture an outer Rc<…'app> to drop it inside the closure, so as to drop the 'app-pointee, and thus cause the scope-yielded Rc to dangle, then we'd be capturing something with the 'app lifetime, which would thus necessarily have to outlive the scope.

I have still put the trait for good measure, since we could envision closure bodies becoming too smart for their own good, or we could envision a self-referential type with the 'app-pointee being the owner, and the Rc<…'app> being the "lender"; in those cases, it would be possible to write the malicious "move the borrower inside and drop both it and the 'app-pointee" pattern, and cause a UAF.

  • And obviously the whole thing relies on Rc<…> being single-threaded (!Send), since otherwise it would be easy to write a race-condition (spawn a thread which calls .with_upgrade(…), and inside, sleeps, before dereferencing the owned Rc; and from the the main thread, sleep shortly (so as to ensure with_upgrade() succeeds), and then drop the Arc and thus the 'app-pointee).

All in all, lifetime transmutation is a very tricky and dangerous thing to do, and this example is no exception; I actually wonder if the generalization I've attempted to write does more harm than good :sweat_smile:

This is incorrect. When you see T: 'static it means that T doesn't contain any non-'static data. That means once the value is created (e.g. as a variable on the stack) it can live for as long as your application and isn't tied to any particular scope.

It won't force your entire application to be 'static, just that all the closures in your table must own the values they close over instead of taking them by reference.

Thank you, nice!

This works as shown in this playground. It also works in my intended application.

:slightly_smiling_face: Btw, I need to warn against inlining the transmute, like I may have suggested with the let weak binding: since transmute is awfully strong, and since it is easy to have other lifetimes lurking around within type-annotations inside a function body (e.g., turbofish types or let type annotations), I highly recommend that such transmutes be contained within a function with types fully spelled out in its signature (where lifetimes may not lurk that easily):

unsafe
fn erase_weak_app_lifetime<'app> (
    it: rc::Weak<RefCell<Application<'app>>>
) ->    rc::Weak<RefCell<Application<'static>>>
{
    ::core::mem::transmute(it)
}

That way, if Application ends up having a second lifetime parameter, you'd be forced to mention it:

unsafe
fn erase_weak_app_lifetime<'app, '_2> (
    it: rc::Weak<RefCell<Application<'app, '_2>>>
) ->    rc::Weak<RefCell<Application<'static, '_2>>>
{
    ::core::mem::transmute(it)
}

Nice, this works, too. Is it possible to write this helper function in a way that abstracts the RefCell<Application<....>> type? Also, would it be better to write:

fn erase_weak_app_lifetime<'a> (
    it: std::rc::Weak<RefCell<Application<'a>>>
) ->    std::rc::Weak<RefCell<Application<'static>>>
{
    unsafe { ::core::mem::transmute(it) }
}

That would require Higher-Kinded Types, which do not really exist in Rust (directly), and in any case the added layer of abstraction is likely not to be a good w.r.t. a dangerous to use pattern. To see what I mean, here is the simplest possible example, using a macro, since they're the most convenient tool for ad-hoc HKTs:

macro_rules! erase_weak_lifetime {(for<$lt:lifetime> $T:ty) => ({
    type __T<$lt> = ::std::rc::Weak<$T>; // incidentllay ensures no other lifetime params
    ::core::mem::transmute::<__T<'_>, __T<'_>>
})
// Usage:
let weak = unsafe {
    erase_weak_lifetime!(for<'app> RefCell<Application<'app>>)(weak)
};

Careful, going from unsafe fn { … } to fn unsafe { … } means you are going from a function which is unsafe to call, i.e., where you trust the caller / require that it misuse not the function, to a function which anybody can call and use however they wish, wherein you promise that your dangerous (unsafe) implementation is fully fool-proof.

In this instance, the latter is clearly not the case, one can cause UB unless they use erase_weak_app_lifetime correctly, so it has to be marked unsafe.


The more cautious approach is to deny the unsafe_op_in_unsafe_fn lint, and then write:

/// Safety: while the returned `Weak` is upgraded, `'app` must not end.
unsafe
fn erase_weak_app_lifetime<'app> (
    it: ::std::rc::Weak<RefCell<Application<'app>>>
) ->    ::std::rc::Weak<RefCell<Application<'static>>>
{
    unsafe {
        // Safety:
        //   - Types which only differ in a lifetime have the same layout
        //   - Correctness of erasing the lifetime is guaranteed by the caller
        ::core::mem::transmute(it)
    }
}
1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.