Skip to content

Commit 6691006

Browse files
authored
Update the host ABI of Wasmtime to return failure (#9675)
* Update the host ABI of Wasmtime to return failure This commit updates the "array call" ABI of Wasmtime used to transition from wasm to the host to explicitly return a `bool` indicating whether the call succeeded or not. Previously functions would implicitly unwind via `longjmp` and thus no explicit checks were necessary. The burden of unwinding is now placed on Cranelift-compiled code itself instead of the caller. There are a few pieces of rationale for this change: * Primarily I was implementing initial integration of Pulley where the host `setjmp` and `longjmp` cannot be used to maintain the Pulley interpreter state. My initial plan for handling this was to handle traps a bit differently in Pulley where having direct access to whether a function trapped or not in the interpreter bytecode is easier to deal with. * Additionally it's generally not safe to call `longjmp` from Rust as it will not run on-stack destructors. This is ok today in the sense that we shouldn't have these in Wasmtime, but directly returning to compiled code improves our safety story here a bit where we just won't have to deal with the problem of skipping destructors. * In the future I'd like to move away from `setjmp` and `longjmp` entirely in the host to a Cranelift-native solution. This change would be required for such a migration anyway so it's my hope to make such a Cranelift-based implementation easier in the future. This might be necessary, for example, when implementing the `exception-handling` proposal for Wasmtime. Functionally this commit does not remove all usages of call-`longjmp`-from-Rust. Notably all libcalls and builtins still use this helper in the trampolines generated in Rust. I plan on going through the libcalls and updating their ABIs and signatures to reflect this in follow-up commits. As a result a few preexisting functions that should go away are marked `#[deprecated]` for internal use in this commit. I'll be cleaning that up as follow-ups. For now this commit handles the "hard part" of host functions by ensuring that the new `bool` return value is plumbed in all the locations correctly. prtest:full * Hack around Windows MinGW miscompile (?) * Run clang-format
1 parent 2b3fe80 commit 6691006

File tree

34 files changed

+428
-193
lines changed

34 files changed

+428
-193
lines changed

crates/cranelift/src/compiler.rs

Lines changed: 85 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,12 @@ impl wasmtime_environ::Compiler for Compiler {
400400
values_vec_len,
401401
);
402402

403-
builder.ins().return_(&[]);
403+
// At this time wasm functions always signal traps with longjmp or some
404+
// similar sort of routine, so if we got this far that means that the
405+
// function did not trap, so return a "true" value here to indicate that
406+
// to satisfy the ABI of the array-call signature.
407+
let true_return = builder.ins().iconst(ir::types::I8, 1);
408+
builder.ins().return_(&[true_return]);
404409
builder.finalize();
405410

406411
Ok(Box::new(compiler.finish()?))
@@ -459,13 +464,14 @@ impl wasmtime_environ::Compiler for Compiler {
459464

460465
// Do an indirect call to the callee.
461466
let callee_signature = builder.func.import_signature(array_call_sig);
462-
self.call_indirect_host(
467+
let call = self.call_indirect_host(
463468
&mut builder,
464469
callee_signature,
465470
callee,
466471
&[callee_vmctx, caller_vmctx, args_base, args_len],
467472
);
468-
473+
let succeeded = builder.func.dfg.inst_results(call)[0];
474+
self.raise_if_host_trapped(&mut builder, caller_vmctx, succeeded);
469475
let results =
470476
self.load_values_from_array(wasm_func_ty.returns(), &mut builder, args_base, args_len);
471477
builder.ins().return_(&results);
@@ -637,27 +643,11 @@ impl wasmtime_environ::Compiler for Compiler {
637643
);
638644
save_last_wasm_exit_fp_and_pc(&mut builder, pointer_type, &ptr_size, limits);
639645

640-
// Now it's time to delegate to the actual builtin. Builtins are stored
641-
// in an array in all `VMContext`s. First load the base pointer of the
642-
// array and then load the entry of the array that corresponds to this
643-
// builtin.
644-
let mem_flags = ir::MemFlags::trusted().with_readonly();
645-
let array_addr = builder.ins().load(
646-
pointer_type,
647-
mem_flags,
648-
vmctx,
649-
i32::from(ptr_size.vmcontext_builtin_functions()),
650-
);
651-
let body_offset = i32::try_from(index.index() * pointer_type.bytes()).unwrap();
652-
let func_addr = builder
653-
.ins()
654-
.load(pointer_type, mem_flags, array_addr, body_offset);
655-
656-
// Forward all our own arguments to the libcall itself, and then return
657-
// all the same results as the libcall.
658-
let block_params = builder.block_params(block0).to_vec();
659-
let host_sig = builder.func.import_signature(host_sig);
660-
let call = self.call_indirect_host(&mut builder, host_sig, func_addr, &block_params);
646+
// Now it's time to delegate to the actual builtin. Forward all our own
647+
// arguments to the libcall itself, and then return all the same results
648+
// as the libcall.
649+
let args = builder.block_params(block0).to_vec();
650+
let call = self.call_builtin(&mut builder, vmctx, &args, index, host_sig);
661651
let results = builder.func.dfg.inst_results(call).to_vec();
662652
builder.ins().return_(&results);
663653
builder.finalize();
@@ -877,6 +867,77 @@ impl Compiler {
877867
}),
878868
}
879869
}
870+
871+
/// Invokes the `raise` libcall in `vmctx` if the `succeeded` value
872+
/// indicates if a trap happened.
873+
///
874+
/// This helper is used when the host returns back to WebAssembly. The host
875+
/// returns a `bool` indicating whether the call succeeded. If the call
876+
/// failed then Cranelift needs to unwind back to the original invocation
877+
/// point. The unwind right now is then implemented in Wasmtime with a
878+
/// `longjmp`, but one day this might be implemented differently with an
879+
/// unwind inside of Cranelift.
880+
///
881+
/// Additionally in the future for pulley this will emit a special trap
882+
/// opcode for Pulley itself to cease interpretation and exit the
883+
/// interpreter.
884+
fn raise_if_host_trapped(
885+
&self,
886+
builder: &mut FunctionBuilder<'_>,
887+
vmctx: ir::Value,
888+
succeeded: ir::Value,
889+
) {
890+
let trapped_block = builder.create_block();
891+
let continuation_block = builder.create_block();
892+
builder.set_cold_block(trapped_block);
893+
builder
894+
.ins()
895+
.brif(succeeded, continuation_block, &[], trapped_block, &[]);
896+
897+
builder.seal_block(trapped_block);
898+
builder.seal_block(continuation_block);
899+
900+
builder.switch_to_block(trapped_block);
901+
let sigs = BuiltinFunctionSignatures::new(&*self.isa, &self.tunables);
902+
let sig = sigs.host_signature(BuiltinFunctionIndex::raise());
903+
self.call_builtin(builder, vmctx, &[vmctx], BuiltinFunctionIndex::raise(), sig);
904+
builder.ins().trap(TRAP_INTERNAL_ASSERT);
905+
906+
builder.switch_to_block(continuation_block);
907+
}
908+
909+
/// Helper to load the core `builtin` from `vmctx` and invoke it with
910+
/// `args`.
911+
fn call_builtin(
912+
&self,
913+
builder: &mut FunctionBuilder<'_>,
914+
vmctx: ir::Value,
915+
args: &[ir::Value],
916+
builtin: BuiltinFunctionIndex,
917+
sig: ir::Signature,
918+
) -> ir::Inst {
919+
let isa = &*self.isa;
920+
let ptr_size = isa.pointer_bytes();
921+
let pointer_type = isa.pointer_type();
922+
923+
// Builtins are stored in an array in all `VMContext`s. First load the
924+
// base pointer of the array and then load the entry of the array that
925+
// corresponds to this builtin.
926+
let mem_flags = ir::MemFlags::trusted().with_readonly();
927+
let array_addr = builder.ins().load(
928+
pointer_type,
929+
mem_flags,
930+
vmctx,
931+
i32::from(ptr_size.vmcontext_builtin_functions()),
932+
);
933+
let body_offset = i32::try_from(builtin.index() * pointer_type.bytes()).unwrap();
934+
let func_addr = builder
935+
.ins()
936+
.load(pointer_type, mem_flags, array_addr, body_offset);
937+
938+
let sig = builder.func.import_signature(sig);
939+
self.call_indirect_host(builder, sig, func_addr, args)
940+
}
880941
}
881942

882943
struct FunctionCompiler<'a> {

crates/cranelift/src/compiler/component.rs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ impl<'a> TrampolineCompiler<'a> {
120120
let pointer_type = self.isa.pointer_type();
121121
let args = self.builder.func.dfg.block_params(self.block0).to_vec();
122122
let vmctx = args[0];
123+
let caller_vmctx = args[1];
123124
let wasm_func_ty = self.types[self.signature].unwrap_func();
124125

125126
// Start off by spilling all the wasm arguments into a stack slot to be
@@ -228,6 +229,9 @@ impl<'a> TrampolineCompiler<'a> {
228229
host_sig.params.push(ir::AbiParam::new(pointer_type));
229230
callee_args.push(values_vec_len);
230231

232+
// return value is a bool whether a trap was raised or not
233+
host_sig.returns.push(ir::AbiParam::new(ir::types::I8));
234+
231235
// Load host function pointer from the vmcontext and then call that
232236
// indirect function pointer with the list of arguments.
233237
let host_fn = self.builder.ins().load(
@@ -237,11 +241,15 @@ impl<'a> TrampolineCompiler<'a> {
237241
i32::try_from(self.offsets.lowering_callee(index)).unwrap(),
238242
);
239243
let host_sig = self.builder.import_signature(host_sig);
240-
self.compiler
241-
.call_indirect_host(&mut self.builder, host_sig, host_fn, &callee_args);
244+
let call =
245+
self.compiler
246+
.call_indirect_host(&mut self.builder, host_sig, host_fn, &callee_args);
247+
let succeeded = self.builder.func.dfg.inst_results(call)[0];
242248

243249
match self.abi {
244250
Abi::Wasm => {
251+
self.compiler
252+
.raise_if_host_trapped(&mut self.builder, caller_vmctx, succeeded);
245253
// After the host function has returned the results are loaded from
246254
// `values_vec_ptr` and then returned.
247255
let results = self.compiler.load_values_from_array(
@@ -253,7 +261,7 @@ impl<'a> TrampolineCompiler<'a> {
253261
self.builder.ins().return_(&results);
254262
}
255263
Abi::Array => {
256-
self.builder.ins().return_(&[]);
264+
self.builder.ins().return_(&[succeeded]);
257265
}
258266
}
259267
}
@@ -522,8 +530,8 @@ impl<'a> TrampolineCompiler<'a> {
522530
self.builder.seal_block(run_destructor_block);
523531

524532
self.builder.switch_to_block(return_block);
525-
self.builder.ins().return_(&[]);
526533
self.builder.seal_block(return_block);
534+
self.abi_store_results(&[]);
527535
}
528536

529537
/// Invokes a host libcall and returns the result.
@@ -624,7 +632,8 @@ impl<'a> TrampolineCompiler<'a> {
624632
ptr,
625633
len,
626634
);
627-
self.builder.ins().return_(&[]);
635+
let true_value = self.builder.ins().iconst(ir::types::I8, 1);
636+
self.builder.ins().return_(&[true_value]);
628637
}
629638
}
630639
}

crates/cranelift/src/func_environ.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,10 @@ impl<'module_environment> FuncEnvironment<'module_environment> {
171171
) -> Self {
172172
let builtin_functions = BuiltinFunctions::new(isa, tunables);
173173

174+
// This isn't used during translation, so squash the warning about this
175+
// being unused from the compiler.
176+
let _ = BuiltinFunctions::raise;
177+
174178
// Avoid unused warning in default build.
175179
#[cfg(not(feature = "wmemcheck"))]
176180
let _ = wmemcheck;

crates/cranelift/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,8 @@ fn array_call_signature(isa: &dyn TargetIsa) -> ir::Signature {
151151
// of `ValRaw`.
152152
sig.params.push(ir::AbiParam::new(isa.pointer_type()));
153153
sig.params.push(ir::AbiParam::new(isa.pointer_type()));
154+
// boolean return value of whether this function trapped
155+
sig.returns.push(ir::AbiParam::new(ir::types::I8));
154156
sig
155157
}
156158

crates/environ/src/builtin.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,8 +196,12 @@ macro_rules! foreach_builtin_function {
196196
#[cfg(feature = "gc")]
197197
table_fill_gc_ref(vmctx: vmctx, table: i32, dst: i64, val: reference, len: i64);
198198

199-
// Raises an unconditional trap.
199+
// Raises an unconditional trap with the specified code.
200200
trap(vmctx: vmctx, code: u8);
201+
202+
// Raises an unconditional trap where the trap information must have
203+
// been previously filled in.
204+
raise(vmctx: vmctx);
201205
}
202206
};
203207
}

crates/wasmtime/src/runtime/component/func/host.rs

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ impl HostFunc {
4848
string_encoding: StringEncoding,
4949
storage: *mut MaybeUninit<ValRaw>,
5050
storage_len: usize,
51-
) where
51+
) -> bool
52+
where
5253
F: Fn(StoreContextMut<T>, P) -> Result<R>,
5354
P: ComponentNamedList + Lift + 'static,
5455
R: ComponentNamedList + Lower + 'static,
@@ -295,24 +296,19 @@ unsafe fn call_host_and_handle_result<T>(
295296
&Arc<ComponentTypes>,
296297
StoreContextMut<'_, T>,
297298
) -> Result<()>,
298-
) {
299+
) -> bool {
299300
let cx = VMComponentContext::from_opaque(cx);
300301
let instance = (*cx).instance();
301302
let types = (*instance).component_types();
302303
let raw_store = (*instance).store();
303304
let mut store = StoreContextMut(&mut *raw_store.cast());
304305

305-
let res = crate::runtime::vm::catch_unwind_and_longjmp(|| {
306+
crate::runtime::vm::catch_unwind_and_record_trap(|| {
306307
store.0.call_hook(CallHook::CallingHost)?;
307308
let res = func(instance, types, store.as_context_mut());
308309
store.0.call_hook(CallHook::ReturningFromHost)?;
309310
res
310-
});
311-
312-
match res {
313-
Ok(()) => {}
314-
Err(e) => crate::runtime::vm::raise_user_trap(e),
315-
}
311+
})
316312
}
317313

318314
unsafe fn call_host_dynamic<T, F>(
@@ -435,7 +431,8 @@ extern "C" fn dynamic_entrypoint<T, F>(
435431
string_encoding: StringEncoding,
436432
storage: *mut MaybeUninit<ValRaw>,
437433
storage_len: usize,
438-
) where
434+
) -> bool
435+
where
439436
F: Fn(StoreContextMut<'_, T>, &[Val], &mut [Val]) -> Result<()> + Send + Sync + 'static,
440437
{
441438
let data = data as *const F;

crates/wasmtime/src/runtime/func.rs

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1592,7 +1592,7 @@ impl Func {
15921592
/// can pass to the called wasm function, if desired.
15931593
pub(crate) fn invoke_wasm_and_catch_traps<T>(
15941594
store: &mut StoreContextMut<'_, T>,
1595-
closure: impl FnMut(*mut VMContext),
1595+
closure: impl FnMut(*mut VMContext) -> bool,
15961596
) -> Result<()> {
15971597
unsafe {
15981598
let exit = enter_wasm(store);
@@ -2296,7 +2296,8 @@ impl HostContext {
22962296
caller_vmctx: *mut VMOpaqueContext,
22972297
args: *mut ValRaw,
22982298
args_len: usize,
2299-
) where
2299+
) -> bool
2300+
where
23002301
F: Fn(Caller<'_, T>, P) -> R + 'static,
23012302
P: WasmTyList,
23022303
R: WasmRet,
@@ -2356,15 +2357,10 @@ impl HostContext {
23562357

23572358
// With nothing else on the stack move `run` into this
23582359
// closure and then run it as part of `Caller::with`.
2359-
let result = crate::runtime::vm::catch_unwind_and_longjmp(move || {
2360+
crate::runtime::vm::catch_unwind_and_record_trap(move || {
23602361
let caller_vmctx = VMContext::from_opaque(caller_vmctx);
23612362
Caller::with(caller_vmctx, run)
2362-
});
2363-
2364-
match result {
2365-
Ok(val) => val,
2366-
Err(err) => crate::runtime::vm::raise_user_trap(err),
2367-
}
2363+
})
23682364
}
23692365
}
23702366

crates/wasmtime/src/runtime/func/typed.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ where
219219
let storage = core::ptr::slice_from_raw_parts_mut(storage, storage_len);
220220
func_ref
221221
.as_ref()
222-
.array_call(VMOpaqueContext::from_vmcontext(caller), storage);
222+
.array_call(VMOpaqueContext::from_vmcontext(caller), storage)
223223
});
224224

225225
let (_, storage) = captures;

crates/wasmtime/src/runtime/trampoline/func.rs

Lines changed: 6 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -25,23 +25,13 @@ unsafe extern "C" fn array_call_shim<F>(
2525
caller_vmctx: *mut VMOpaqueContext,
2626
values_vec: *mut ValRaw,
2727
values_vec_len: usize,
28-
) where
28+
) -> bool
29+
where
2930
F: Fn(*mut VMContext, &mut [ValRaw]) -> Result<()> + 'static,
3031
{
31-
// Here we are careful to use `catch_unwind` to ensure Rust panics don't
32-
// unwind past us. The primary reason for this is that Rust considers it UB
33-
// to unwind past an `extern "C"` function. Here we are in an `extern "C"`
34-
// function and the cross into wasm was through an `extern "C"` function at
35-
// the base of the stack as well. We'll need to wait for assorted RFCs and
36-
// language features to enable this to be done in a sound and stable fashion
37-
// before avoiding catching the panic here.
38-
//
39-
// Also note that there are intentionally no local variables on this stack
40-
// frame. The reason for that is that some of the "raise" functions we have
41-
// below will trigger a longjmp, which won't run local destructors if we
42-
// have any. To prevent leaks we avoid having any local destructors by
43-
// avoiding local variables.
44-
let result = crate::runtime::vm::catch_unwind_and_longjmp(|| {
32+
// Be sure to catch Rust panics to manually shepherd them across the wasm
33+
// boundary, and then otherwise delegate as normal.
34+
crate::runtime::vm::catch_unwind_and_record_trap(|| {
4535
let vmctx = VMArrayCallHostFuncContext::from_opaque(vmctx);
4636
// Double-check ourselves in debug mode, but we control
4737
// the `Any` here so an unsafe downcast should also
@@ -51,18 +41,7 @@ unsafe extern "C" fn array_call_shim<F>(
5141
let state = &*(state as *const _ as *const TrampolineState<F>);
5242
let values_vec = core::slice::from_raw_parts_mut(values_vec, values_vec_len);
5343
(state.func)(VMContext::from_opaque(caller_vmctx), values_vec)
54-
});
55-
56-
match result {
57-
Ok(()) => {}
58-
59-
// If a trap was raised (an error returned from the imported function)
60-
// then we smuggle the trap through `Box<dyn Error>` through to the
61-
// call-site, which gets unwrapped in `Trap::from_runtime` later on as we
62-
// convert from the internal `Trap` type to our own `Trap` type in this
63-
// crate.
64-
Err(err) => crate::runtime::vm::raise_user_trap(err),
65-
}
44+
})
6645
}
6746

6847
pub fn create_array_call_function<F>(

0 commit comments

Comments
 (0)