Open SkiFire13 opened 3 months ago
The easy fix here might be to make Combine
an unsafe trait.
A more complex alternative which would however keep Combine
safe would be to change Combine
's parameters to a single one that allows running either A
or B
, and returns a handle to call the other one.
It would look something like this (name totally up for discussions):
struct RunBoth<'a, A, B> {
world: UnsafeWorldCell<'a>,
a: &'a mut A,
b: &'a mut A,
}
impl<'a, A; System, B: System> RunBoth<'a, A, B> {
fn run_a(self, input: A::In) -> (A::Out, RunOne<'a, B>) {
let output = unsafe { self.a.run_unsafe(input, self.world) };
(output, RunBoth { world: self.world, s: self.b })
}
fn run_b(self, input: B::In) -> (B::Out, RunOne<'a, A>) {
let output = unsafe { self.b.run_unsafe(input, self.world) };
(output, RunBoth { world: self.world, s: self.a })
}
}
struct RunOne<'a, S> {
world: UnsafeWorldCell<'a>,
s: &'a mut S
}
impl<'a, S: System> RunOne<'a, S> {
fn run(self, input: S::In) -> S::Out {
unsafe { self.s.run_unsafe(input, self.world) }
}
}
pub trait Combine<A: System, B: System> {
type In;
type Out;
fn combine(
input: Self::In,
systems: RunBoth<'_, A, B>,
) -> Self::Out;
}
Bevy version
0.14.1
What you did
What went wrong
This should either fail to compile or throw an error at runtime. Instead, this produced UB due to
b
holding a reference to a resource that has been removed. MIRI also flags this as UB:Additional information
The issue is that
CombinatorSystem
gives to theFunc::combine
implementation two closures, each of which holds anUnsafeWorldCell
that assumes is valid for calling that system:https://github.com/bevyengine/bevy/blob/91fa4bb64905121a18b40df0062dbd85714aa3ce/crates/bevy_ecs/src/system/combinator.rs#L185-L198
The safety comment assumes that since the functions are
!Send + !Sync + !'static
then they cannot possibly be called in a parallel way. However this fails to see that they can still be called in a reentrant way, that is one function calling the other while it's running. This effectively also creates overlapping references to the world or its contents and thus leads to UB.Actually exploiting this issue is not that easy though, as can be seen in the code above, since one closure cannot be smuggled directly to the other, thus having to go through some global state. This might seem that it requires
'static
(and in fact using only the stdlib is does), but there are libraries that allow doing this likescoped-tls-hkt
, which allows to store non-'static
data in a thread local, ensuring it will be overwritten before it goes out of scope. With this one closure can actually be smuggled into the other and lead to the overlapping access to the world and thus UB.