Skip to content

Commit 3f7095b

Browse files
committed
Don't invoke the RustFutureContinuationCallback while holding the mutex
As described in the comments, this could lead to a deadlock if the foreign code immediately polled the future. This doesn't currently happen with any of the core bindings, but it might with other bindings and it seems like a footgun in general. To accomplish this, I made a separate class to handle the state and moved the mutex to only wrap that state. The state mutation happens with the mutex locked and the callback invocation happens after it's unlocked.
1 parent 4f4c989 commit 3f7095b

File tree

2 files changed

+117
-32
lines changed

2 files changed

+117
-32
lines changed

uniffi_core/src/ffi/rustfuture/future.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ where
202202
// This Mutex should never block if our code is working correctly, since there should not be
203203
// multiple threads calling [Self::poll] and/or [Self::complete] at the same time.
204204
future: Mutex<WrappedFuture<F, T, UT>>,
205-
scheduler: Mutex<Scheduler>,
205+
scheduler: Scheduler,
206206
// UT is used as the generic parameter for [LowerReturn].
207207
// Let's model this with PhantomData as a function that inputs a UT value.
208208
_phantom: PhantomData<fn(UT) -> ()>,
@@ -218,7 +218,7 @@ where
218218
pub(super) fn new(future: F, _tag: UT) -> Arc<Self> {
219219
Arc::new(Self {
220220
future: Mutex::new(WrappedFuture::new(future)),
221-
scheduler: Mutex::new(Scheduler::new()),
221+
scheduler: Scheduler::new(),
222222
_phantom: PhantomData,
223223
})
224224
}
@@ -232,20 +232,20 @@ where
232232
if ready {
233233
callback(data, RustFuturePoll::Ready)
234234
} else {
235-
self.scheduler.lock().unwrap().store(callback, data);
235+
self.scheduler.store(callback, data);
236236
}
237237
}
238238

239239
pub(super) fn is_cancelled(&self) -> bool {
240-
self.scheduler.lock().unwrap().is_cancelled()
240+
self.scheduler.is_cancelled()
241241
}
242242

243243
pub(super) fn wake(&self) {
244-
self.scheduler.lock().unwrap().wake();
244+
self.scheduler.wake();
245245
}
246246

247247
pub(super) fn cancel(&self) {
248-
self.scheduler.lock().unwrap().cancel();
248+
self.scheduler.cancel();
249249
}
250250

251251
pub(super) fn complete(&self, call_status: &mut RustCallStatus) -> T::ReturnType {
@@ -254,7 +254,7 @@ where
254254

255255
pub(super) fn free(self: Arc<Self>) {
256256
// Call cancel() to send any leftover data to the continuation callback
257-
self.scheduler.lock().unwrap().cancel();
257+
self.scheduler.cancel();
258258
// Ensure we drop our inner future, releasing all held references
259259
self.future.lock().unwrap().free();
260260
}

uniffi_core/src/ffi/rustfuture/scheduler.rs

Lines changed: 110 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* License, v. 2.0. If a copy of the MPL was not distributed with this
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44

5-
use std::mem;
5+
use std::{mem, sync::Mutex};
66

77
use super::{RustFutureContinuationCallback, RustFuturePoll};
88

@@ -20,7 +20,12 @@ use super::{RustFutureContinuationCallback, RustFuturePoll};
2020
/// state, invoking any future callbacks as soon as they're stored.
2121
2222
#[derive(Debug)]
23-
pub(super) enum Scheduler {
23+
pub(super) struct Scheduler {
24+
state: Mutex<SchedulerState>,
25+
}
26+
27+
#[derive(Debug)]
28+
pub(super) enum SchedulerState {
2429
/// No continuations set, neither wake() nor cancel() called.
2530
Empty,
2631
/// `wake()` was called when there was no continuation set. The next time `store` is called,
@@ -33,58 +38,138 @@ pub(super) enum Scheduler {
3338
Set(RustFutureContinuationCallback, *const ()),
3439
}
3540

36-
impl Scheduler {
37-
pub(super) fn new() -> Self {
38-
Self::Empty
41+
/// Encapsulates a call to a RustFutureContinuationCallback
42+
struct CallbackCall {
43+
callback: RustFutureContinuationCallback,
44+
data: *const (),
45+
poll_data: RustFuturePoll,
46+
}
47+
48+
impl CallbackCall {
49+
fn new(
50+
callback: RustFutureContinuationCallback,
51+
data: *const (),
52+
poll_data: RustFuturePoll,
53+
) -> Self {
54+
Self {
55+
callback,
56+
data,
57+
poll_data,
58+
}
3959
}
4060

41-
/// Store new continuation data if we are in the `Empty` state. If we are in the `Waked` or
42-
/// `Cancelled` state, call the continuation immediately with the data.
43-
pub(super) fn store(&mut self, callback: RustFutureContinuationCallback, data: *const ()) {
61+
fn invoke(self) {
62+
(self.callback)(self.data, self.poll_data)
63+
}
64+
}
65+
66+
/// The SchedulerState impl contains all the ways to mutate the inner state field.
67+
///
68+
/// All methods return an `Option<CallbackCall>` rather than invoking the callback directly.
69+
/// This is important, since the Mutex is locked while inside these methods. If we called the
70+
/// callback directly, the foreign code could poll the future again, which would try to lock the
71+
/// mutex again and lead to a deadlock.
72+
impl SchedulerState {
73+
fn store(
74+
&mut self,
75+
callback: RustFutureContinuationCallback,
76+
data: *const (),
77+
) -> Option<CallbackCall> {
4478
match self {
45-
Self::Empty => *self = Self::Set(callback, data),
79+
Self::Empty => {
80+
*self = Self::Set(callback, data);
81+
None
82+
}
4683
Self::Set(old_callback, old_data) => {
4784
log::error!(
48-
"store: observed `Self::Set` state. Is poll() being called from multiple threads at once?"
85+
"store: observed `SchedulerState::Set` state. Is poll() being called from multiple threads at once?"
4986
);
50-
old_callback(*old_data, RustFuturePoll::Ready);
87+
let call = CallbackCall::new(*old_callback, *old_data, RustFuturePoll::Ready);
5188
*self = Self::Set(callback, data);
89+
Some(call)
5290
}
5391
Self::Waked => {
5492
*self = Self::Empty;
55-
callback(data, RustFuturePoll::MaybeReady);
56-
}
57-
Self::Cancelled => {
58-
callback(data, RustFuturePoll::Ready);
93+
Some(CallbackCall::new(
94+
callback,
95+
data,
96+
RustFuturePoll::MaybeReady,
97+
))
5998
}
99+
Self::Cancelled => Some(CallbackCall::new(callback, data, RustFuturePoll::Ready)),
60100
}
61101
}
62102

63-
pub(super) fn wake(&mut self) {
103+
fn wake(&mut self) -> Option<CallbackCall> {
64104
match self {
65105
// If we had a continuation set, then call it and transition to the `Empty` state.
66-
Self::Set(callback, old_data) => {
106+
SchedulerState::Set(callback, old_data) => {
67107
let old_data = *old_data;
68108
let callback = *callback;
69-
*self = Self::Empty;
70-
callback(old_data, RustFuturePoll::MaybeReady);
109+
*self = SchedulerState::Empty;
110+
Some(CallbackCall::new(
111+
callback,
112+
old_data,
113+
RustFuturePoll::MaybeReady,
114+
))
71115
}
72116
// If we were in the `Empty` state, then transition to `Waked`. The next time `store`
73117
// is called, we will immediately call the continuation.
74-
Self::Empty => *self = Self::Waked,
118+
SchedulerState::Empty => {
119+
*self = SchedulerState::Waked;
120+
None
121+
}
75122
// This is a no-op if we were in the `Cancelled` or `Waked` state.
76-
_ => (),
123+
_ => None,
124+
}
125+
}
126+
127+
fn cancel(&mut self) -> Option<CallbackCall> {
128+
if let SchedulerState::Set(callback, old_data) =
129+
mem::replace(self, SchedulerState::Cancelled)
130+
{
131+
Some(CallbackCall::new(callback, old_data, RustFuturePoll::Ready))
132+
} else {
133+
None
134+
}
135+
}
136+
}
137+
138+
impl Scheduler {
139+
pub(super) fn new() -> Self {
140+
Self {
141+
state: Mutex::new(SchedulerState::Empty),
77142
}
78143
}
79144

80-
pub(super) fn cancel(&mut self) {
81-
if let Self::Set(callback, old_data) = mem::replace(self, Self::Cancelled) {
82-
callback(old_data, RustFuturePoll::Ready);
145+
/// Call a method on the inner state field
146+
///
147+
/// If it returns a callback to invoke, then make the call after releasing the mutex.
148+
fn call_state_method(&self, f: impl Fn(&mut SchedulerState) -> Option<CallbackCall>) {
149+
let mut state = self.state.lock().unwrap();
150+
let callback_call = f(&mut state);
151+
drop(state);
152+
if let Some(callback_call) = callback_call {
153+
callback_call.invoke()
83154
}
84155
}
85156

157+
/// Store new continuation data if we are in the `Empty` state. If we are in the `Waked` or
158+
/// `Cancelled` state, call the continuation immediately with the data.
159+
pub(super) fn store(&self, callback: RustFutureContinuationCallback, data: *const ()) {
160+
self.call_state_method(|state| state.store(callback, data))
161+
}
162+
163+
pub(super) fn wake(&self) {
164+
self.call_state_method(SchedulerState::wake)
165+
}
166+
167+
pub(super) fn cancel(&self) {
168+
self.call_state_method(SchedulerState::cancel)
169+
}
170+
86171
pub(super) fn is_cancelled(&self) -> bool {
87-
matches!(self, Self::Cancelled)
172+
matches!(*self.state.lock().unwrap(), SchedulerState::Cancelled)
88173
}
89174
}
90175

0 commit comments

Comments
 (0)