On a wip branch, there are some subtle soundness issues with Lazy #30
Description
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
regex-automata/src/util/lazy.rs
Line 110 in 1d8f4d6
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)