Skip to content
This repository was archived by the owner on Jul 7, 2023. It is now read-only.
This repository was archived by the owner on Jul 7, 2023. It is now read-only.

On a wip branch, there are some subtle soundness issues with Lazy #30

Closed
@matklad

Description

@matklad

Just couldn't resist taking a look at a couple of safety issues that have burnt me in the past in this area :-)

This type

pub(super) struct Lazy<T, F> {

has two problems:

First, it misses unsafe impl<T: Sync + Send, F: Sync> Sync for Lazy<T, F> (the inferred impl misses + Send). This can be weaponized to turn Lazy into a channel for sending Sync+!Send types across threads:

fn sync_not_send<T: Sync + Default>() {
    let lazy = Lazy::new(move || T::default());
    std::thread::scope(|scope| {
        scope.spawn(|| {
            lazy.get(); // We create T in this thread
        });
    });
    // And drop in this thread.
    drop(lazy);
    // So we have send a !Send type over threads. (with some more legwork, its
    // possible to even sneak the value out of drop through thread local)
}

Second, the type owns T (it can drop it), but this isn't visible to compiler. That is, PhantomData<Box<T>> is missing. In principle, this should be weaponizable by subverting dropcheck. If the compiler believes that dropping Lazy can't drop T, then it might allow code where the T in lazy has some dangling pointers when the Lazy is dropped, something along the lines of

fn no_dropchk() {
    struct Sneaky<'a>(&'a str);
    impl<'a> Drop for Sneaky<'a> {
        fn drop(&mut self) {
            println!(".0 = {}", self.0);
        }
    }

    let lazy;
    let s = " hello".to_string();
    lazy = Lazy::new(|| Sneaky(s.as_str()));
}

No, this exact code compiles, because, although compiler doesn't see that we can drop T, it sees that we can drop F, and those share the problematic lifetime. I am maybe 70% confident that, because of this, there's no way too weaponize it, but still adding phantom data would seem like a good idea.

(newer versions of OnceCell crate also have this version of synchronization primitive: https://docs.rs/once_cell/latest/once_cell/race/struct.OnceBox.html. We don't have a spnning no-alloc version though, as I somewhat firmly believe that, for all code that could spin lock wihtout yielding to OS/blocking interrupts/etc, the caller should explicitly type out .spin somewhere)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions