Skip to content

Commit 663111c

Browse files
committed
Fix sync imports calling code that calls async imports in an async context
1 parent a165cac commit 663111c

File tree

6 files changed

+215
-16
lines changed

6 files changed

+215
-16
lines changed

lib/api/src/backend/sys/entities/function/mod.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,7 @@ impl Function {
505505
) -> Result<(), RuntimeError> {
506506
// Call the trampoline.
507507
let result = {
508+
let store_id = store.objects_mut().id();
508509
// Safety: the store context is uninstalled before we return, and the
509510
// store mut is valid for the duration of the call.
510511
let store_install_guard =
@@ -516,9 +517,13 @@ impl Function {
516517
let storeref = store.as_store_ref();
517518
let vm_function = self.handle.get(storeref.objects().as_sys());
518519
let config = storeref.engine().tunables().vmconfig();
520+
let signal_handler = storeref.signal_handler();
519521
r = unsafe {
522+
// Safety: This is the intended use-case for StoreContext::pause, as
523+
// documented in the function's doc comments.
524+
let pause_guard = StoreContext::pause(store_id);
520525
wasmer_call_trampoline(
521-
store.as_store_ref().signal_handler(),
526+
signal_handler,
522527
config,
523528
vm_function.anyfunc.as_ptr().as_ref().vmctx,
524529
trampoline,

lib/api/src/backend/sys/entities/function/typed.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ macro_rules! impl_native_traits {
5252
rets_list.as_mut()
5353
};
5454

55+
let store_id = store.objects_mut().id();
5556
// Install the store into the store context
5657
let store_install_guard = unsafe {
5758
StoreContext::ensure_installed(store.as_store_mut().inner as *mut _)
@@ -62,6 +63,9 @@ macro_rules! impl_native_traits {
6263
let storeref = store.as_store_ref();
6364
let config = storeref.engine().tunables().vmconfig();
6465
r = unsafe {
66+
// Safety: This is the intended use-case for StoreContext::pause, as
67+
// documented in the function's doc comments.
68+
let pause_guard = StoreContext::pause(store_id);
6569
wasmer_vm::wasmer_call_trampoline(
6670
store.as_store_ref().signal_handler(),
6771
config,
@@ -190,6 +194,7 @@ macro_rules! impl_native_traits {
190194
rets_list.as_mut()
191195
};
192196

197+
let store_id = store.objects_mut().id();
193198
// Install the store into the store context
194199
let store_install_guard = unsafe {
195200
StoreContext::ensure_installed(store.as_store_mut().inner as *mut _)
@@ -200,6 +205,9 @@ macro_rules! impl_native_traits {
200205
let storeref = store.as_store_ref();
201206
let config = storeref.engine().tunables().vmconfig();
202207
r = unsafe {
208+
// Safety: This is the intended use-case for StoreContext::pause, as
209+
// documented in the function's doc comments.
210+
let pause_guard = StoreContext::pause(store_id);
203211
wasmer_vm::wasmer_call_trampoline(
204212
store.as_store_ref().signal_handler(),
205213
config,

lib/api/src/entities/store/context.rs

Lines changed: 120 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,12 @@ pub(crate) struct StoreAsyncGuardWrapper {
9696
pub(crate) guard: *mut LocalRwLockWriteGuard<Box<StoreInner>>,
9797
}
9898

99+
pub(crate) struct StorePtrPauseGuard {
100+
store_id: StoreId,
101+
ptr: *mut StoreInner,
102+
ref_count_decremented: bool,
103+
}
104+
99105
#[cfg(feature = "experimental-async")]
100106
pub(crate) enum GetStoreAsyncGuardResult {
101107
Ok(StoreAsyncGuardWrapper),
@@ -170,13 +176,56 @@ impl StoreContext {
170176
pub(crate) unsafe fn ensure_installed(store_ptr: *mut StoreInner) -> StoreInstallGuard {
171177
let store_id = unsafe { store_ptr.as_ref().unwrap().objects.id() };
172178
if Self::is_active(store_id) {
179+
let current_ptr = STORE_CONTEXT_STACK.with(|cell| {
180+
let stack = cell.borrow();
181+
unsafe { stack.last().unwrap().entry.get().as_ref().unwrap().as_ptr() }
182+
});
183+
assert_eq!(store_ptr, current_ptr, "Store context pointer mismatch");
173184
StoreInstallGuard::NotInstalled
174185
} else {
175186
Self::install(store_id, StoreContextEntry::Sync(store_ptr));
176187
StoreInstallGuard::Installed(store_id)
177188
}
178189
}
179190

191+
/// "Pause" one borrow of the store context.
192+
///
193+
/// # Safety
194+
/// Code must ensure it does not use the StorePtrWrapper or
195+
/// StoreAsyncGuardWrapper that it owns, or any StoreRef/StoreMut
196+
/// derived from them, while the store context is paused.
197+
///
198+
/// The safe, correct use-case for this method is to
199+
/// pause the store context while executing WASM code, which
200+
/// cannot use the store context directly. This allows an async
201+
/// context to uninstall the store context when suspending if it's
202+
/// called from a sync imported function. The imported function
203+
/// will have borrowed the store context in its trampoline, which
204+
/// will prevent the async context from uninstalling the store.
205+
/// However, since the imported function passes a mutable borrow
206+
/// of its store into `Function::call`, it will expect the store
207+
/// to change before the call returns.
208+
pub(crate) unsafe fn pause(id: StoreId) -> StorePtrPauseGuard {
209+
STORE_CONTEXT_STACK.with(|cell| {
210+
let mut stack = cell.borrow_mut();
211+
let top = stack
212+
.last_mut()
213+
.expect("No store context installed on this thread");
214+
assert_eq!(top.id, id, "Mismatched store context access");
215+
let ref_count_decremented = if top.borrow_count > 0 {
216+
top.borrow_count -= 1;
217+
true
218+
} else {
219+
false
220+
};
221+
StorePtrPauseGuard {
222+
store_id: id,
223+
ptr: unsafe { top.entry.get().as_ref().unwrap().as_ptr() },
224+
ref_count_decremented,
225+
}
226+
})
227+
}
228+
180229
/// Safety: This method lets you borrow multiple mutable references
181230
/// to the currently active store context. The caller must ensure that:
182231
/// * there is only one mutable reference alive, or
@@ -291,6 +340,9 @@ impl Clone for StorePtrWrapper {
291340

292341
impl Drop for StorePtrWrapper {
293342
fn drop(&mut self) {
343+
if std::thread::panicking() {
344+
return;
345+
}
294346
let id = self.as_mut().objects_mut().id();
295347
STORE_CONTEXT_STACK.with(|cell| {
296348
let mut stack = cell.borrow_mut();
@@ -306,6 +358,9 @@ impl Drop for StorePtrWrapper {
306358
#[cfg(feature = "experimental-async")]
307359
impl Drop for StoreAsyncGuardWrapper {
308360
fn drop(&mut self) {
361+
if std::thread::panicking() {
362+
return;
363+
}
309364
let id = unsafe { self.guard.as_ref().unwrap().objects.id() };
310365
STORE_CONTEXT_STACK.with(|cell| {
311366
let mut stack = cell.borrow_mut();
@@ -323,12 +378,28 @@ impl Drop for StoreInstallGuard {
323378
if let Self::Installed(store_id) = self {
324379
STORE_CONTEXT_STACK.with(|cell| {
325380
let mut stack = cell.borrow_mut();
326-
let top = stack.pop().expect("Store context stack underflow");
327-
assert_eq!(top.id, *store_id, "Mismatched store context uninstall");
328-
assert_eq!(
329-
top.borrow_count, 0,
330-
"Cannot uninstall store context while it is still borrowed"
331-
);
381+
match (stack.pop(), std::thread::panicking()) {
382+
(Some(top), false) => {
383+
assert_eq!(top.id, *store_id, "Mismatched store context uninstall");
384+
assert_eq!(
385+
top.borrow_count, 0,
386+
"Cannot uninstall store context while it is still borrowed"
387+
);
388+
}
389+
(Some(top), true) => {
390+
// If we're panicking and there's a store ID mismatch, just
391+
// put the store back in the hope that its own install guard
392+
// take care of uninstalling it later.
393+
if top.id != *store_id {
394+
stack.push(top);
395+
}
396+
}
397+
(None, false) => panic!("Store context stack underflow"),
398+
(None, true) => {
399+
// Nothing to do if we're panicking; panics can put the context
400+
// in an invalid state, and we don't to cause another panic here.
401+
}
402+
}
332403
})
333404
}
334405
}
@@ -338,12 +409,51 @@ impl Drop for ForcedStoreInstallGuard {
338409
fn drop(&mut self) {
339410
STORE_CONTEXT_STACK.with(|cell| {
340411
let mut stack = cell.borrow_mut();
341-
let top = stack.pop().expect("Store context stack underflow");
342-
assert_eq!(top.id, self.store_id, "Mismatched store context uninstall");
412+
match (stack.pop(), std::thread::panicking()) {
413+
(Some(top), false) => {
414+
assert_eq!(top.id, self.store_id, "Mismatched store context uninstall");
415+
assert_eq!(
416+
top.borrow_count, 0,
417+
"Cannot uninstall store context while it is still borrowed"
418+
);
419+
}
420+
(Some(top), true) => {
421+
// If we're panicking and there's a store ID mismatch, just
422+
// put the store back in the hope that its own install guard
423+
// take care of uninstalling it later.
424+
if top.id != self.store_id {
425+
stack.push(top);
426+
}
427+
}
428+
(None, false) => panic!("Store context stack underflow"),
429+
(None, true) => {
430+
// Nothing to do if we're panicking; panics can put the context
431+
// in an invalid state, and we don't to cause another panic here.
432+
}
433+
}
434+
})
435+
}
436+
}
437+
438+
impl Drop for StorePtrPauseGuard {
439+
fn drop(&mut self) {
440+
if std::thread::panicking() {
441+
return;
442+
}
443+
STORE_CONTEXT_STACK.with(|cell| {
444+
let mut stack = cell.borrow_mut();
445+
let top = stack
446+
.last_mut()
447+
.expect("No store context installed on this thread");
448+
assert_eq!(top.id, self.store_id, "Mismatched store context access");
343449
assert_eq!(
344-
top.borrow_count, 0,
345-
"Cannot uninstall store context while it is still borrowed"
450+
unsafe { top.entry.get().as_ref().unwrap() }.as_ptr(),
451+
self.ptr,
452+
"Mismatched store context access"
346453
);
454+
if self.ref_count_decremented {
455+
top.borrow_count += 1;
456+
}
347457
})
348458
}
349459
}

lib/api/src/error.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ impl RuntimeError {
269269
pub(crate) fn from_dyn(err: Box<dyn std::error::Error + Send + Sync>) -> Self {
270270
match err.downcast::<Self>() {
271271
Ok(runtime_error) => *runtime_error,
272-
Err(error) => Trap::user(error).into(),
272+
Err(error) => Trap::user(error),
273273
}
274274
}
275275
}

lib/api/tests/jspi_async.rs

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
#![cfg(feature = "experimental-async")]
22

3-
use std::sync::OnceLock;
3+
use std::{cell::RefCell, sync::OnceLock};
44

55
use anyhow::Result;
66
use futures::future;
77
use wasmer::{
8-
AsyncFunctionEnvMut, Function, FunctionEnv, FunctionType, Instance, Module, Store, StoreAsync,
9-
Type, TypedFunction, Value, imports,
8+
AsyncFunctionEnvMut, Function, FunctionEnv, FunctionEnvMut, FunctionType, Instance, Module,
9+
Store, StoreAsync, Type, TypedFunction, Value, imports,
1010
};
1111
use wasmer_vm::TrapCode;
1212

@@ -227,3 +227,79 @@ fn cannot_yield_when_not_in_async_context() -> Result<()> {
227227

228228
Ok(())
229229
}
230+
231+
#[test]
232+
fn nested_async_in_sync() -> Result<()> {
233+
const WAT: &str = r#"
234+
(module
235+
(import "env" "sync" (func $sync (result i32)))
236+
(import "env" "async" (func $async (result i32)))
237+
(func (export "entry") (result i32)
238+
call $sync
239+
)
240+
(func (export "inner_async") (result i32)
241+
call $async
242+
)
243+
)
244+
"#;
245+
let wasm = wat::parse_str(WAT).expect("valid WAT module");
246+
247+
let mut store = Store::default();
248+
let module = Module::new(&store, wasm)?;
249+
250+
struct Env {
251+
inner_async: RefCell<Option<wasmer::TypedFunction<(), i32>>>,
252+
}
253+
let env = FunctionEnv::new(
254+
&mut store,
255+
Env {
256+
inner_async: RefCell::new(None),
257+
},
258+
);
259+
260+
let sync = Function::new_typed_with_env(&mut store, &env, |mut env: FunctionEnvMut<Env>| {
261+
let (env, mut store) = env.data_and_store_mut();
262+
env.inner_async
263+
.borrow()
264+
.as_ref()
265+
.expect("inner_async function to be set")
266+
.call(&mut store)
267+
.expect("inner async call to succeed")
268+
});
269+
270+
let async_ = Function::new_typed_async(&mut store, async || {
271+
tokio::task::yield_now().await;
272+
42
273+
});
274+
275+
let imports = imports! {
276+
"env" => {
277+
"sync" => sync,
278+
"async" => async_,
279+
}
280+
};
281+
282+
let instance = Instance::new(&mut store, &module, &imports)?;
283+
284+
let inner_async = instance
285+
.exports
286+
.get_typed_function::<(), i32>(&mut store, "inner_async")
287+
.unwrap();
288+
env.as_mut(&mut store)
289+
.inner_async
290+
.borrow_mut()
291+
.replace(inner_async);
292+
293+
let entry = instance
294+
.exports
295+
.get_typed_function::<(), i32>(&mut store, "entry")?;
296+
let result = tokio::runtime::Builder::new_current_thread()
297+
.enable_all()
298+
.build()
299+
.unwrap()
300+
.block_on(entry.call_async(&store.into_async()))?;
301+
302+
assert_eq!(result, 42);
303+
304+
Ok(())
305+
}

tests/compilers/typed_functions.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ fn long_f(a: u32, b: u32, c: u32, d: u32, e: u32, f: u16, g: u64, h: u64, i: u16
2222
+ a as u64 * 1000000000
2323
}
2424

25-
fn long_f_dynamic(values: &[Value]) -> Result<Vec<Value>, RuntimeError> {
25+
fn long_f_dynamic(values: &[Value]) -> DynamicFunctionResult {
2626
Ok(vec![Value::I64(
2727
values[9].unwrap_i32() as i64
2828
+ values[8].unwrap_i32() as i64 * 10

0 commit comments

Comments
 (0)