From e8078a545e2b8cdc3097b864d0425ebae72f17af Mon Sep 17 00:00:00 2001 From: Frank Emrich Date: Thu, 15 Feb 2024 14:27:15 +0000 Subject: [PATCH] Backtrace support, part 2: Shadow VMRuntimeLimits for each stack (#99) As of PR #98, each `ContinuationObject` contains an object of type `StackLimits`. In addition, there exists such an object for the main stack, it is stored in the `VMContext` and the `StackChain::MainStack` variant at the list of currently active continuations points to it. These `StackLimits` objects contain counterparts of the stack-related fields in `VMRuntimeLimits`, namely `stack_limit` and the various `last_wasm_*` fields. As of PR #98, the actual contents of these `StackLimits` are unused (and not updated). This changes in this PR: While the `VMRuntimeLimits` continue to contain the stack-related information for the currently executing stack (either the main stack or a continuation), we ensure that for stacks that are *not* currently running, their corresponding `StackLimits` object now contains accurate values about their stack limits. The doc comment on `wasmtime_continuations::StackChain` describes the exact invariants that we maintain. To ensure that these invariants hold, we need to copy some fields between the `VMRuntimeLimits` and `StackLimits` objects when stack switching occurs. In particular, the `tc_resume` libcall now takes an additional argument: A pointer to the `StackLimits` object of the *parent* of the continuation that is about to be resume. Note that this needs to happen in the libcall itself, in order to obtain accurate values for the `last_wasm_exit_*` values in the `VMRuntimeLimits`. --- crates/continuations/src/lib.rs | 36 +++++- crates/cranelift/src/func_environ.rs | 12 +- crates/cranelift/src/wasmfx/optimized.rs | 136 ++++++++++++++++++++++- crates/environ/src/builtin.rs | 2 +- crates/runtime/src/continuation.rs | 17 +++ crates/runtime/src/libcalls.rs | 7 +- 6 files changed, 202 insertions(+), 8 deletions(-) diff --git a/crates/continuations/src/lib.rs b/crates/continuations/src/lib.rs index 7f28e8e7d073..b4baeea377ed 100644 --- a/crates/continuations/src/lib.rs +++ b/crates/continuations/src/lib.rs @@ -92,8 +92,40 @@ const STACK_CHAIN_CONTINUATION_DISCRIMINANT: usize = 2; /// fields of the corresponding `ContinuationObject`. For the main stack, the /// `MainStack` variant contains a pointer to the /// `typed_continuations_main_stack_limits` field of the VMContext. -// FIXME(frank-emrich) Note that the data within the StackLimits objects is -// currently not used or updated in any way. +/// +/// The following invariants hold for these `StackLimits` objects, and the data +/// in `VMRuntimeLimits`. +/// +/// Currently executing stack: +/// For the currently executing stack (i.e., the stack that is at the head of +/// the VMContext's `typed_continuations_chain` list), the associated +/// `StackLimits` object contains stale/undefined data. Instead, the live data +/// describing the limits for the currently executing stack is always maintained +/// in `VMRuntimeLimits`. Note that as a general rule independently from any +/// execution of continuations, the `last_wasm_exit*` fields in the +/// `VMRuntimeLimits` contain undefined values while executing wasm. +/// +/// Parents of currently executing stack: +/// For stacks that appear in the tail of the VMContext's +/// `typed_continuations_chain` list (i.e., stacks that are not currently +/// executing themselves, but are a parent of the currently executing stack), we +/// have the following: All the fields in the stack's StackLimits are valid, +/// describing the stack's stack limit, and pointers where executing for that +/// stack entered and exited WASM. +/// +/// Suspended continuations: +/// For suspended continuations (including their parents), we have the +/// following. Note that the main stack can never be in this state. The +/// `stack_limit` and `last_enter_wasm_sp` fields of the corresponding +/// `StackLimits` object contain valid data, while the `last_exit_wasm_*` fields +/// contain arbitrary values. +/// There is only one exception to this: Note that a continuation that has been +/// created with cont.new, but never been resumed so far, is considered +/// "suspended". However, its `last_enter_wasm_sp` field contains undefined +/// data. This is justified, because when resume-ing a continuation for the +/// first time, a native-to-wasm trampoline is called, which sets up the +/// `last_wasm_entry_sp` in the `VMRuntimeLimits` with the correct value, thus +/// restoring the necessary invariant. #[derive(Debug, Clone, PartialEq)] #[repr(usize, C)] pub enum StackChain { diff --git a/crates/cranelift/src/func_environ.rs b/crates/cranelift/src/func_environ.rs index 96ec7caaf423..3b22b3dc7019 100644 --- a/crates/cranelift/src/func_environ.rs +++ b/crates/cranelift/src/func_environ.rs @@ -158,7 +158,8 @@ pub struct FuncEnvironment<'module_environment> { /// VMRuntimeLimits` for this function's vmctx argument. This pointer is stored /// in the vmctx itself, but never changes for the lifetime of the function, /// so if we load it up front we can continue to use it throughout. - vmruntime_limits_ptr: cranelift_frontend::Variable, + /// NOTE(frank-emrich) pub for use in crate::wasmfx::* modules + pub(crate) vmruntime_limits_ptr: cranelift_frontend::Variable, /// A cached epoch deadline value, when performing epoch-based /// interruption. Loaded from `VMRuntimeLimits` and reloaded after @@ -2646,7 +2647,14 @@ impl<'module_environment> cranelift_wasm::FuncEnvironment for FuncEnvironment<'m ) -> WasmResult<()> { // If the `vmruntime_limits_ptr` variable will get used then we initialize // it here. - if self.tunables.consume_fuel || self.tunables.epoch_interruption { + if true || self.tunables.consume_fuel || self.tunables.epoch_interruption { + // TODO(frank-emrich) This is now done unconditionally because we + // need the `vmruntime_limits_ptr` variable when translating resume. + // This has no runtime overhead: We are adding a load to every + // function, but if it is not actually used, cranelift's DCE pass + // will remove it. However, it would be nicer to check if the + // function actually contains resume instructions, and only run + // `declare_vmruntime_limits_ptr` then. self.declare_vmruntime_limits_ptr(builder); } // Additionally we initialize `fuel_var` if it will get used. diff --git a/crates/cranelift/src/wasmfx/optimized.rs b/crates/cranelift/src/wasmfx/optimized.rs index 5cd961333406..e3c97937e2aa 100644 --- a/crates/cranelift/src/wasmfx/optimized.rs +++ b/crates/cranelift/src/wasmfx/optimized.rs @@ -24,6 +24,7 @@ pub(crate) mod typed_continuation_helpers { use cranelift_frontend::FunctionBuilder; use std::mem; use wasmtime_environ::BuiltinFunctionIndex; + use wasmtime_environ::PtrSize; // This is a reference to this very module. // We need it so that we can refer to the functions inside this module from @@ -927,6 +928,106 @@ pub(crate) mod typed_continuation_helpers { // the pointer right after the discriminant. self.payload } + + /// Must only be called if `self` represents a `MainStack` or `Continuation` variant. + /// Returns a pointer to the associated `StackLimits` object (i.e., in + /// the former case, the pointer directly stored in the variant, or in + /// the latter case a pointer to the `StackLimits` data within the + /// `ContinuationObject`. + pub fn get_stack_limits_ptr<'a>( + &self, + env: &mut crate::func_environ::FuncEnvironment<'a>, + builder: &mut FunctionBuilder, + ) -> ir::Value { + use wasmtime_continuations::offsets as o; + + self.assert_not_absent(env, builder); + + // `self` corresponds to a StackChain::MainStack or + // StackChain::Continuation. + // In both cases, the payload is a pointer. + let ptr = self.payload; + + // `obj` is now a pointer to the beginning of either + // 1. A ContinuationObject object (in the case of a + // StackChain::Continuation) + // 2. A StackLimits object (in the case of + // StackChain::MainStack) + // + // Since a ContinuationObject starts with an (inlined) StackLimits + // object at offset 0, we actually have in both cases that `ptr` is + // now the address of the beginning of a StackLimits object. + debug_assert_eq!(o::continuation_object::LIMITS, 0); + ptr + } + + /// Sets `last_wasm_entry_sp` and `stack_limit` fields in + /// `VMRuntimelimits` using the values from the `StackLimits` object + /// associated with this stack chain. + pub fn write_limits_to_vmcontext<'a>( + &self, + env: &mut crate::func_environ::FuncEnvironment<'a>, + builder: &mut FunctionBuilder, + vmruntime_limits: cranelift_frontend::Variable, + ) { + use wasmtime_continuations::offsets as o; + + let stack_limits_ptr = self.get_stack_limits_ptr(env, builder); + let vmruntime_limits_ptr = builder.use_var(vmruntime_limits); + + let memflags = ir::MemFlags::trusted(); + + let mut copy_to_vm_runtime_limits = |our_offset, their_offset| { + let our_value = + builder + .ins() + .load(self.pointer_type, memflags, stack_limits_ptr, our_offset); + builder + .ins() + .store(memflags, our_value, vmruntime_limits_ptr, their_offset); + }; + + let pointer_size = self.pointer_type.bytes() as u8; + copy_to_vm_runtime_limits( + o::stack_limits::STACK_LIMIT, + pointer_size.vmruntime_limits_stack_limit(), + ); + copy_to_vm_runtime_limits( + o::stack_limits::LAST_WASM_ENTRY_SP, + pointer_size.vmruntime_limits_last_wasm_entry_sp(), + ); + } + + /// Overwrites the `last_wasm_entry_sp` field of the `StackLimits` + /// object associated with this stack chain by loading the corresponding + /// field from the `VMRuntimeLimits`. + pub fn load_limits_from_vmcontext<'a>( + &self, + env: &mut crate::func_environ::FuncEnvironment<'a>, + builder: &mut FunctionBuilder, + vmruntime_limits: cranelift_frontend::Variable, + ) { + use wasmtime_continuations::offsets as o; + + let stack_limits_ptr = self.get_stack_limits_ptr(env, builder); + let vmruntime_limits_ptr = builder.use_var(vmruntime_limits); + + let memflags = ir::MemFlags::trusted(); + let pointer_size = self.pointer_type.bytes() as u8; + + let last_wasm_entry_sp = builder.ins().load( + self.pointer_type, + memflags, + vmruntime_limits_ptr, + pointer_size.vmruntime_limits_last_wasm_entry_sp(), + ); + builder.ins().store( + memflags, + last_wasm_entry_sp, + stack_limits_ptr, + o::stack_limits::LAST_WASM_ENTRY_SP, + ); + } } } @@ -1212,12 +1313,30 @@ pub(crate) fn translate_resume<'a>( let vmctx = tc::VMContext::new(vmctx, env.pointer_type()); vmctx.set_active_continuation(env, builder, resume_contobj); + // Note that the resume_contobj libcall a few lines further below + // manipulates the stack limits as follows: + // 1. Copy stack_limit, last_wasm_entry_sp and last_wasm_exit* values from + // VMRuntimeLimits into the currently active continuation (i.e., the + // one that will become the parent of the to-be-resumed one) + // + // 2. Copy `stack_limit` and `last_wasm_entry_sp` in the + // `StackLimits` of `resume_contobj` into the `VMRuntimeLimits`. + // + // See the comment on `wasmtime_continuations::StackChain` for a + // description of the invariants that we maintain for the various stack + // limits. + let parent_stacks_limit_pointer = parent_stack_chain.get_stack_limits_ptr(env, builder); + // We mark `resume_contobj` to be invoked let co = tc::ContinuationObject::new(resume_contobj, env.pointer_type()); co.set_state(builder, wasmtime_continuations::State::Invoked); - let (_vmctx, result) = - shared::generate_builtin_call!(env, builder, tc_resume, [resume_contobj]); + let (_vmctx, result) = shared::generate_builtin_call!( + env, + builder, + tc_resume, + [resume_contobj, parent_stacks_limit_pointer] + ); // Now the parent contobj (or main stack) is active again vmctx.store_stack_chain(env, builder, &parent_stack_chain); @@ -1258,6 +1377,15 @@ pub(crate) fn translate_resume<'a>( builder.switch_to_block(suspend_block); builder.seal_block(suspend_block); + // We store parts of the VMRuntimeLimits into the continuation that just suspended. + let suspended_chain = + tc::StackChain::from_continuation(builder, resume_contobj, env.pointer_type()); + suspended_chain.load_limits_from_vmcontext(env, builder, env.vmruntime_limits_ptr); + + // Afterwards (!), restore parts of the VMRuntimeLimits from the + // parent of the suspended continuation (which is now active). + parent_stack_chain.write_limits_to_vmcontext(env, builder, env.vmruntime_limits_ptr); + // We need to terminate this block before being allowed to switch to another one builder.ins().jump(switch_block, &[]); }; @@ -1378,6 +1506,10 @@ pub(crate) fn translate_resume<'a>( builder.switch_to_block(return_block); builder.seal_block(return_block); + // Restore parts of the VMRuntimeLimits from the + // parent of the returned continuation (which is now active). + parent_stack_chain.write_limits_to_vmcontext(env, builder, env.vmruntime_limits_ptr); + let co = tc::ContinuationObject::new(resume_contobj, env.pointer_type()); co.set_state(builder, wasmtime_continuations::State::Returned); diff --git a/crates/environ/src/builtin.rs b/crates/environ/src/builtin.rs index a04b27e3b0fa..5998c469575d 100644 --- a/crates/environ/src/builtin.rs +++ b/crates/environ/src/builtin.rs @@ -55,7 +55,7 @@ macro_rules! foreach_builtin_function { /// Creates a new continuation from a funcref. tc_cont_new(vmctx: vmctx, r: pointer, param_count: i64, result_count: i64) -> pointer; /// Resumes a continuation. - tc_resume(vmctx: vmctx, contobj: pointer) -> i32; + tc_resume(vmctx: vmctx, contobj: pointer, parent_stack_limits: pointer) -> i32; /// Suspends a continuation. tc_suspend(vmctx: vmctx, tag: i32); /// Returns the continuation object corresponding to the given continuation reference. diff --git a/crates/runtime/src/continuation.rs b/crates/runtime/src/continuation.rs index f89fb15be6a2..b8e7cf1351c7 100644 --- a/crates/runtime/src/continuation.rs +++ b/crates/runtime/src/continuation.rs @@ -164,6 +164,7 @@ pub fn cont_new( pub fn resume( instance: &mut Instance, contobj: *mut ContinuationObject, + parent_stack_limits: *mut StackLimits, ) -> Result { let cont = unsafe { contobj.as_ref().ok_or_else(|| { @@ -207,6 +208,22 @@ pub fn resume( } } + // See the comment on `wasmtime_continuations::StackChain` for a description + // of the invariants that we maintain for the various stack limits. + unsafe { + let runtime_limits = &**instance.runtime_limits(); + + (*parent_stack_limits).stack_limit = *runtime_limits.stack_limit.get(); + (*parent_stack_limits).last_wasm_entry_sp = *runtime_limits.last_wasm_entry_sp.get(); + // These last two values were only just updated in the `runtime_limits` + // because we entered the current libcall. + (*parent_stack_limits).last_wasm_exit_fp = *runtime_limits.last_wasm_exit_fp.get(); + (*parent_stack_limits).last_wasm_exit_pc = *runtime_limits.last_wasm_exit_pc.get(); + + *runtime_limits.stack_limit.get() = (*contobj).limits.stack_limit; + *runtime_limits.last_wasm_entry_sp.get() = (*contobj).limits.last_wasm_entry_sp; + } + unsafe { (*(*(*instance.store()).vmruntime_limits()) .stack_limit diff --git a/crates/runtime/src/libcalls.rs b/crates/runtime/src/libcalls.rs index d36771ea0a96..8deca03c9423 100644 --- a/crates/runtime/src/libcalls.rs +++ b/crates/runtime/src/libcalls.rs @@ -788,10 +788,15 @@ fn tc_cont_new( Ok(ans.cast::()) } -fn tc_resume(instance: &mut Instance, contobj: *mut u8) -> Result { +fn tc_resume( + instance: &mut Instance, + contobj: *mut u8, + parent_stack_limits: *mut u8, +) -> Result { crate::continuation::resume( instance, contobj.cast::(), + parent_stack_limits.cast::(), ) }