From 8495c8d91e6c15c2d8317bd43ba3840512db5e18 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Wed, 19 Aug 2026 08:07:47 -0700 Subject: [PATCH] Unconditionally setup tasks for resource destructors This commit updates the translation of resource destructors in the component model to unconditionally setup a sync task as the spec specifies. This resolves an issue where context slots were leaking across boundaries when a component destroyed its own resource. While here this updates the translation to use the inline fast path that is present in sync-to-sync adapters by juggling some code to make it sharable by the two locations. --- crates/cranelift/src/alias_region.rs | 11 + crates/cranelift/src/compiler/component.rs | 145 ++++---- crates/cranelift/src/component_sync_call.rs | 194 +++++++++++ crates/cranelift/src/func_environ.rs | 176 ++-------- crates/cranelift/src/lib.rs | 1 + .../async/context-in-resource-drop.wast | 328 ++++++++++++++++++ 6 files changed, 645 insertions(+), 210 deletions(-) create mode 100644 crates/cranelift/src/component_sync_call.rs create mode 100644 tests/misc_testsuite/component-model/async/context-in-resource-drop.wast diff --git a/crates/cranelift/src/alias_region.rs b/crates/cranelift/src/alias_region.rs index 9963f5ef22d9..319b3be89dd5 100644 --- a/crates/cranelift/src/alias_region.rs +++ b/crates/cranelift/src/alias_region.rs @@ -932,6 +932,17 @@ where } } + /// The Cranelift type of a pointer on the target being compiled for. + pub fn pointer_type(&self) -> ir::Type { + self.pointer_type + } + + /// The pointer size of the target being compiled for, used to compute the + /// layout of Wasmtime's vmctx types. + pub fn ptr_size(&self) -> &Offsets::Ptr { + self.offsets.get_ptr_size() + } + /// Get the alias region for accesses into the GC heap. pub fn gc_heap_region(&mut self, func: &mut ir::Function) -> ir::AliasRegion { self.region(func, AliasRegionKey::GcHeap) diff --git a/crates/cranelift/src/compiler/component.rs b/crates/cranelift/src/compiler/component.rs index 4580e60113bf..2fd2f207c4e8 100644 --- a/crates/cranelift/src/compiler/component.rs +++ b/crates/cranelift/src/compiler/component.rs @@ -13,11 +13,10 @@ use cranelift_codegen::ir::condcodes::IntCC; use cranelift_codegen::ir::{self, InstBuilder, Value}; use cranelift_codegen::isa::{CallConv, TargetIsa}; use cranelift_frontend::FunctionBuilder; -use wasmtime_environ::GetPtrSize; use wasmtime_environ::error::{Result, bail}; use wasmtime_environ::{ - Abi, BuiltinFunctionIndex, CompiledFunctionBody, EntityRef, FuncKey, HostCall, PanicOnOom as _, - TrapSentinel, Tunables, WasmFuncType, WasmValType, component::*, + Abi, BuiltinFunctionIndex, CompiledFunctionBody, EntityRef, FuncKey, GetPtrSize, HostCall, + PanicOnOom as _, TrapSentinel, Tunables, WasmFuncType, WasmValType, component::*, fact::PREPARE_CALL_FIXED_PARAMS, }; @@ -1136,8 +1135,10 @@ impl<'a> TrampolineCompiler<'a> { // old_may_block = load.i32 vmctx+$may_block_offset // store 0, vmctx+$may_block_offset // - // ;; call enter_sync_call, but only if the component instances - // ;; differ and concurrency is enabled + // ;; enter a sync call, but only if the component instances + // ;; differ and concurrency is enabled. This pushes an on-stack + // ;; `VMDeferredThread` and zeroes the live context slots; see + // ;; `enter_sync_call` below. // ... // // ;; ============================================================ @@ -1149,16 +1150,17 @@ impl<'a> TrampolineCompiler<'a> { // dtor = load.ptr vmctx+$offset // func_addr = load.ptr dtor+$offset // callee_vmctx = load.ptr dtor+$offset - // call_indirect func_addr, callee_vmctx, vmctx, rep - // ;; ============================================================ // - // ;; restore old value of may_block - // store old_may_block, vmctx+$may_block_offset + // call_indirect func_addr, callee_vmctx, vmctx, rep // - // ;; if needed, call exit_sync_call + // ;; and restore the caller's slots afterwards + // store saved0, vmstore+$context_slot0 // ... + // ;; ============================================================ // - // ;; if needed, restore the old value of may_block + // ;; if needed, exit the sync call entered above and restore the + // ;; old value of may_block + // ... // store old_may_block, vmctx+$may_block_offset // // jump return_block @@ -1191,51 +1193,32 @@ impl<'a> TrampolineCompiler<'a> { self.builder.switch_to_block(run_destructor_block); // If this is a component-defined resource, the `may_leave` flag must be - // checked. Additionally, if concurrency is enabled, the `may_block` - // field must be updated and `enter_sync_call` called. Note though that - // all of that may be elided if the resource table resides in the same - // component instance that defined the resource as the component is - // calling itself. - let old_may_block = if let Some(def) = resource_def { + // checked. Additionally, if concurrency is enabled, the `may_block` + // field must be updated and a sync call entered. + let entered_sync_call = if has_destructor && let Some(def) = resource_def { + // Skip the may-leave check for self-owned resources. if self.types[resource].unwrap_concrete_instance() != def.instance { self.check_may_leave_instance(self.types[resource].unwrap_concrete_instance()); + } - if self.compiler.tunables.concurrency_support { - // Stash the old value of `may_block` and then set it to false. - let old_may_block = self - .alias_regions - .vmcomponent() - .task_may_block() - .readonly() - .load(&mut self.builder.cursor(), vmctx); - let zero = self.builder.ins().iconst(ir::types::I32, i64::from(0)); - self.alias_regions.vmcomponent().task_may_block().store( - &mut self.builder.cursor(), - vmctx, - zero, - ); + if self.compiler.tunables.concurrency_support { + // Stash the old value of `may_block` and then set it to false. + let old_may_block = self + .alias_regions + .vmcomponent() + .task_may_block() + .readonly() + .load(&mut self.builder.cursor(), vmctx); + let zero = self.builder.ins().iconst(ir::types::I32, i64::from(0)); + self.alias_regions.vmcomponent().task_may_block().store( + &mut self.builder.cursor(), + vmctx, + zero, + ); - // Call `enter_sync_call` - // - // FIXME: Apply the optimizations described in #12311. - let host_args = vec![ - vmctx, - self.builder - .ins() - .iconst(ir::types::I32, i64::from(instance.as_u32())), - self.builder.ins().iconst(ir::types::I32, i64::from(0)), - self.builder - .ins() - .iconst(ir::types::I32, i64::from(def.instance.as_u32())), - ]; - let call = self.call_libcall(vmctx, host::enter_sync_call, &host_args); - let result = self.builder.func.dfg.inst_results(call).get(0).copied(); - self.raise_if_host_trapped(result.unwrap()); - - Some(old_may_block) - } else { - None - } + let slot = self.enter_sync_call_inline(instance, def.instance); + + Some((old_may_block, slot)) } else { None } @@ -1315,13 +1298,8 @@ impl<'a> TrampolineCompiler<'a> { self.builder.seal_block(continuation); } - if let Some(old_may_block) = old_may_block { - // Call `exit_sync_call` - // - // FIXME: Apply the optimizations described in #12311. - let call = self.call_libcall(vmctx, host::exit_sync_call, &[vmctx]); - let result = self.builder.func.dfg.inst_results(call).get(0).copied(); - self.raise_if_host_trapped(result.unwrap()); + if let Some((old_may_block, slot)) = entered_sync_call { + self.exit_sync_call_inline(vmctx, slot); // Restore the old value of `may_block` self.alias_regions.vmcomponent().task_may_block().store( @@ -1339,6 +1317,55 @@ impl<'a> TrampolineCompiler<'a> { self.abi_store_results(&[]); } + /// Translates `enter-sync-call` around a resource destructor, deferring the + /// heavyweight task bookkeeping the `enter_sync_call` libcall would + /// otherwise do eagerly. + fn enter_sync_call_inline( + &mut self, + caller_instance: RuntimeComponentInstanceIndex, + callee_instance: RuntimeComponentInstanceIndex, + ) -> ir::StackSlot { + let vmctx = self.caller_vmctx(); + let caller_instance = self + .builder + .ins() + .iconst(ir::types::I32, i64::from(caller_instance.as_u32())); + let callee_async = self.builder.ins().iconst(ir::types::I32, 0); + let callee_instance = self + .builder + .ins() + .iconst(ir::types::I32, i64::from(callee_instance.as_u32())); + crate::component_sync_call::enter( + &mut self.builder, + &mut self.alias_regions, + vmctx, + crate::component_sync_call::EnterArgs { + caller_instance, + callee_async, + callee_instance, + }, + ) + } + + /// Translates `exit-sync-call`, the counterpart to `enter_sync_call` above. + fn exit_sync_call_inline(&mut self, vmctx: ir::Value, slot: ir::StackSlot) { + // Note that the helper here wants a core wasm vmctx, not a component + // one like `vmctx` is in this function. + let caller_vmctx = self.caller_vmctx(); + let slow = crate::component_sync_call::exit( + &mut self.builder, + &mut self.alias_regions, + caller_vmctx, + slot, + ); + + let call = self.call_libcall(vmctx, host::exit_sync_call, &[vmctx]); + let result = self.builder.func.dfg.inst_results(call).get(0).copied(); + self.raise_if_host_trapped(result.unwrap()); + + slow.finish(&mut self.builder); + } + fn load_optional_memory( &mut self, vmctx: ir::Value, diff --git a/crates/cranelift/src/component_sync_call.rs b/crates/cranelift/src/component_sync_call.rs new file mode 100644 index 000000000000..6f2d7581f6a2 --- /dev/null +++ b/crates/cranelift/src/component_sync_call.rs @@ -0,0 +1,194 @@ +//! Shared translation of the component model's `{enter,exit}-sync-call` +//! intrinsics. +//! +//! Two different places wrap a synchronous call into another component +//! instance with these intrinsics: fused adapters, whose calls are translated +//! in `func_environ.rs`, and the `resource.drop` trampoline's call to a +//! resource destructor, translated in `compiler/component.rs`. +//! +//! Rather than call out to the host to do the task bookkeeping that entering a +//! sync call entails, both push a `VMDeferredThread` into their own stack frame +//! and publish it as the store's current thread. The host then only promotes it +//! into a real `GuestTask` if it ever actually needs one; see +//! `StoreOpaque::force_current_thread` and +//! `StoreOpaque::force_deferred_current_thread`. +//! +//! The two callers differ only in where the arguments recorded by `enter` come +//! from and in how `exit`'s out-of-line slow path is called. Everything else +//! lives here: the `VMDeferredThread` layout and, in particular, the +//! `context.{get,set}` slot save/zero/restore, which has to match what the host +//! does for a non-deferred thread in `StoreOpaque::set_thread`. + +use crate::alias_region::AliasRegions; +use cranelift_codegen::ir::condcodes::IntCC; +use cranelift_codegen::ir::{self, InstBuilder}; +use cranelift_frontend::FunctionBuilder; +use wasmtime_environ::{GetPtrSize, NUM_COMPONENT_CONTEXT_SLOTS, PtrSize}; + +/// The `enter_sync_call` arguments recorded into the pushed +/// `VMDeferredThread`, to be replayed by the host if it ever has to promote the +/// deferred thread into a real one. +pub struct EnterArgs { + /// The component instance performing the call. + pub caller_instance: ir::Value, + /// Whether the callee is async-lifted, as an `i32` boolean. + pub callee_async: ir::Value, + /// The component instance being called into. + pub callee_instance: ir::Value, +} + +/// Translates `enter-sync-call`: allocates a `VMDeferredThread` in the current +/// function's frame, records `args` into it along with the caller's context +/// slots, zeroes the live context slots for the callee, and publishes the frame +/// as the store's current thread. +/// +/// `vmctx` must be a core wasm `*mut VMContext` for this store. The returned +/// stack slot must be handed to [`exit`] once the callee returns. +pub fn enter( + builder: &mut FunctionBuilder<'_>, + alias_regions: &mut AliasRegions, + vmctx: ir::Value, + args: EnterArgs, +) -> ir::StackSlot +where + O: GetPtrSize, +{ + let pointer_type = alias_regions.pointer_type(); + let ptr = alias_regions.ptr_size(); + + // Allocate the on-stack `VMDeferredThread`. + let slot = builder.func.create_sized_stack_slot(ir::StackSlotData::new( + ir::StackSlotKind::ExplicitSlot, + u32::from(ptr.vm_deferred_thread().size()), + u8::try_from(ptr.size().trailing_zeros()).unwrap(), + )); + let slot_addr = builder.ins().stack_addr(pointer_type, slot, 0); + let vmstore = alias_regions + .vmctx() + .store_context() + .load(&mut builder.cursor(), vmctx); + + // Link the previous current thread in as this frame's parent. + let parent = alias_regions.vmstore_context_current_thread(&mut builder.cursor(), vmstore); + alias_regions.store_vmdeferred_thread_parent(&mut builder.cursor(), slot_addr, parent); + + // Record the deferred `enter_sync_call` arguments. + alias_regions.store_vmdeferred_thread_caller_instance( + &mut builder.cursor(), + slot_addr, + args.caller_instance, + ); + alias_regions.store_vmdeferred_thread_callee_async( + &mut builder.cursor(), + slot_addr, + args.callee_async, + ); + alias_regions.store_vmdeferred_thread_callee_instance( + &mut builder.cursor(), + slot_addr, + args.callee_instance, + ); + + // Save the caller's context slots into the frame and reset the live values + // to 0 for the freshly-entered (deferred) thread. + for i in 0..u8::try_from(NUM_COMPONENT_CONTEXT_SLOTS).unwrap() { + let saved = alias_regions.vmstore_context_component_context_slot( + &mut builder.cursor(), + ir::types::I32, + vmstore, + i, + ); + alias_regions.store_vmdeferred_thread_saved_context( + &mut builder.cursor(), + slot_addr, + i, + saved, + ); + let zero = builder.ins().iconst(ir::types::I32, 0); + alias_regions.store_vmstore_context_component_context_slot( + &mut builder.cursor(), + vmstore, + i, + zero, + ); + } + + // Publish the deferred thread as the store's current thread. + alias_regions.store_vmstore_context_current_thread(&mut builder.cursor(), vmstore, slot_addr); + + slot +} + +/// The unfinished slow path of an [`exit`], which the caller must fill in with +/// an out-of-line call to the `exit_sync_call` intrinsic before finishing it +/// with [`SlowExit::finish`]. +#[must_use = "the slow path of an exit-sync-call must be emitted and finished"] +pub struct SlowExit { + cont_block: ir::Block, +} + +impl SlowExit { + pub fn finish(self, builder: &mut FunctionBuilder<'_>) { + builder.ins().jump(self.cont_block, &[]); + builder.seal_block(self.cont_block); + builder.switch_to_block(self.cont_block); + } +} + +/// Translates `exit-sync-call`, the counterpart to [`enter`], where `slot` is +/// the stack slot that `enter` returned. +/// +/// If the deferred thread pushed by `enter` is still the store's current +/// thread, meaning nothing ever forced it into a real one, then it is popped +/// and the caller's context slots restored inline. Otherwise the host has state +/// to tear down, which only it can do, so the caller must emit an out-of-line +/// call to the `exit_sync_call` intrinsic: on return the builder is positioned +/// in that slow path, which is finished with [`SlowExit::finish`]. +pub fn exit( + builder: &mut FunctionBuilder<'_>, + alias_regions: &mut AliasRegions, + vmctx: ir::Value, + slot: ir::StackSlot, +) -> SlowExit +where + O: GetPtrSize, +{ + let pointer_type = alias_regions.pointer_type(); + let slot_addr = builder.ins().stack_addr(pointer_type, slot, 0); + let vmstore = alias_regions + .vmctx() + .store_context() + .load(&mut builder.cursor(), vmctx); + let cur = alias_regions.vmstore_context_current_thread(&mut builder.cursor(), vmstore); + let is_fast = builder.ins().icmp(IntCC::Equal, cur, slot_addr); + + let fast_block = builder.create_block(); + let slow_block = builder.create_block(); + let cont_block = builder.create_block(); + builder + .ins() + .brif(is_fast, fast_block, &[], slow_block, &[]); + builder.seal_block(fast_block); + builder.seal_block(slow_block); + + // Fast path: pop the deferred thread and restore the caller's context. + builder.switch_to_block(fast_block); + let parent = alias_regions.vmdeferred_thread_parent(&mut builder.cursor(), slot_addr); + alias_regions.store_vmstore_context_current_thread(&mut builder.cursor(), vmstore, parent); + for i in 0..u8::try_from(NUM_COMPONENT_CONTEXT_SLOTS).unwrap() { + let saved = + alias_regions.vmdeferred_thread_saved_context(&mut builder.cursor(), slot_addr, i); + alias_regions.store_vmstore_context_component_context_slot( + &mut builder.cursor(), + vmstore, + i, + saved, + ); + } + builder.ins().jump(cont_block, &[]); + + // Slow path: the thread was promoted to a real one, so the caller does the + // equivalent teardown out-of-line. + builder.switch_to_block(slow_block); + SlowExit { cont_block } +} diff --git a/crates/cranelift/src/func_environ.rs b/crates/cranelift/src/func_environ.rs index 71b07ef66604..17aa7b303a7b 100644 --- a/crates/cranelift/src/func_environ.rs +++ b/crates/cranelift/src/func_environ.rs @@ -38,11 +38,10 @@ use wasmtime_environ::{ DefinedGlobalIndex, DefinedTableIndex, ElemIndex, EngineOrModuleTypeIndex, FactInlineIntrinsic, FrameStateSlotBuilder, FrameValType, FuncIndex, FuncKey, GlobalConstValue, GlobalIndex, IndexType, KnownFunc, Memory, MemoryIndex, MemoryInit, MemorySegmentOffset, MemoryTunables, - Module, ModuleInternedTypeIndex, ModuleTranslation, ModuleTypesBuilder, - NUM_COMPONENT_CONTEXT_SLOTS, PassiveElemIndex, PtrSize, RuntimeDataIndex, Table, TableIndex, - TableInitialValue, TableSegment, TableSegmentElements, TagIndex, Tunables, TypeConvert, - TypeIndex, VMOffsets, WasmCompositeInnerType, WasmFuncType, WasmHeapTopType, WasmHeapType, - WasmRefType, WasmResult, WasmStorageType, WasmValType, + Module, ModuleInternedTypeIndex, ModuleTranslation, ModuleTypesBuilder, PassiveElemIndex, + RuntimeDataIndex, Table, TableIndex, TableInitialValue, TableSegment, TableSegmentElements, + TagIndex, Tunables, TypeConvert, TypeIndex, VMOffsets, WasmCompositeInnerType, WasmFuncType, + WasmHeapTopType, WasmHeapType, WasmRefType, WasmResult, WasmStorageType, WasmValType, }; use wasmtime_environ::{FUNCREF_INIT_BIT, FUNCREF_MASK}; @@ -2007,100 +2006,23 @@ impl<'a, 'func, 'module_env> Call<'a, 'func, 'module_env> { abi == wasmtime_environ::Abi::Wasm && !self.tail && !self.env.tunables.debug_guest } - /// Inline lowering of a FACT adapter's `enter-sync-call` intrinsic: push a - /// `VMDeferredThread` onto an explicit stack slot and publish it as the - /// store's current thread, deferring the heavyweight task bookkeeping the - /// `enter_sync_call` libcall would otherwise do eagerly. + /// Inline lowering of a FACT adapter's `enter-sync-call` intrinsic, which + /// defers the heavyweight task bookkeeping the `enter_sync_call` libcall + /// would otherwise do eagerly. /// /// `real_call_args` is `[callee_vmctx, caller_vmctx, caller_instance, /// callee_async, callee_instance]`. fn lower_fact_enter_sync_call(&mut self, real_call_args: &[ir::Value]) -> CallRets { - let ptr_ty = self.env.pointer_type(); - let ptr = self.env.offsets.ptr; - - // Allocate the on-stack `VMDeferredThread`. - let size = u32::from(ptr.vm_deferred_thread().size()); - let align_shift = u8::try_from(ptr.size().trailing_zeros()).unwrap(); - let slot = self - .builder - .func - .create_sized_stack_slot(ir::StackSlotData::new( - ir::StackSlotKind::ExplicitSlot, - size, - align_shift, - )); - let slot_addr = self.builder.ins().stack_addr(ptr_ty, slot, 0); - - let vmstore = self.env.get_vmstore_context_ptr(self.builder); - - // Link the previous current thread in as this frame's parent. - let parent = self - .env - .alias_regions - .vmstore_context_current_thread(&mut self.builder.cursor(), vmstore); - self.env.alias_regions.store_vmdeferred_thread_parent( - &mut self.builder.cursor(), - slot_addr, - parent, - ); - - // Record the deferred `enter_sync_call` arguments. - self.env - .alias_regions - .store_vmdeferred_thread_caller_instance( - &mut self.builder.cursor(), - slot_addr, - real_call_args[2], - ); - self.env.alias_regions.store_vmdeferred_thread_callee_async( - &mut self.builder.cursor(), - slot_addr, - real_call_args[3], - ); - self.env - .alias_regions - .store_vmdeferred_thread_callee_instance( - &mut self.builder.cursor(), - slot_addr, - real_call_args[4], - ); - - // Save the caller's context slots into the frame and reset the live - // values to 0 for the freshly-entered (deferred) thread. - for i in 0..u8::try_from(NUM_COMPONENT_CONTEXT_SLOTS).unwrap() { - let saved = self - .env - .alias_regions - .vmstore_context_component_context_slot( - &mut self.builder.cursor(), - ir::types::I32, - vmstore, - i, - ); - self.env - .alias_regions - .store_vmdeferred_thread_saved_context( - &mut self.builder.cursor(), - slot_addr, - i, - saved, - ); - let zero = self.builder.ins().iconst(ir::types::I32, 0); - self.env - .alias_regions - .store_vmstore_context_component_context_slot( - &mut self.builder.cursor(), - vmstore, - i, - zero, - ); - } - - // Publish the deferred thread as the store's current thread. - self.env.alias_regions.store_vmstore_context_current_thread( - &mut self.builder.cursor(), - vmstore, - slot_addr, + let vmctx = self.env.vmctx_val(&mut self.builder.cursor()); + let slot = crate::component_sync_call::enter( + self.builder, + &mut self.env.alias_regions, + vmctx, + crate::component_sync_call::EnterArgs { + caller_instance: real_call_args[2], + callee_async: real_call_args[3], + callee_instance: real_call_args[4], + }, ); debug_assert!(self.env.fact_sync_call_slot.is_none()); @@ -2109,72 +2031,26 @@ impl<'a, 'func, 'module_env> Call<'a, 'func, 'module_env> { } /// Inline lowering of a FACT adapter's `exit-sync-call` intrinsic, the - /// counterpart to `lower_fact_enter_sync_call`. If our deferred thread is - /// still current (nothing forced it) we pop it and restore the caller's - /// context inline; otherwise we fall back to the out-of-line - /// `exit_sync_call` libcall. + /// counterpart to `lower_fact_enter_sync_call`. fn lower_fact_exit_sync_call( &mut self, callee_index: FuncIndex, sig_ref: ir::SigRef, real_call_args: &[ir::Value], ) -> CallRets { - let ptr_ty = self.env.pointer_type(); - let slot = self .env .fact_sync_call_slot .take() .expect("inline exit-sync-call without a matching enter-sync-call"); - let slot_addr = self.builder.ins().stack_addr(ptr_ty, slot, 0); - let vmstore = self.env.get_vmstore_context_ptr(self.builder); - let cur = self - .env - .alias_regions - .vmstore_context_current_thread(&mut self.builder.cursor(), vmstore); - let is_fast = self.builder.ins().icmp(IntCC::Equal, cur, slot_addr); - - let fast_block = self.builder.create_block(); - let slow_block = self.builder.create_block(); - let cont_block = self.builder.create_block(); - self.builder - .ins() - .brif(is_fast, fast_block, &[], slow_block, &[]); - self.builder.seal_block(fast_block); - self.builder.seal_block(slow_block); - - // Fast path: pop the deferred thread and restore the caller's context. - self.builder.switch_to_block(fast_block); - let parent = self - .env - .alias_regions - .vmdeferred_thread_parent(&mut self.builder.cursor(), slot_addr); - self.env.alias_regions.store_vmstore_context_current_thread( - &mut self.builder.cursor(), - vmstore, - parent, + let vmctx = self.env.vmctx_val(&mut self.builder.cursor()); + let slow = crate::component_sync_call::exit( + self.builder, + &mut self.env.alias_regions, + vmctx, + slot, ); - for i in 0..u8::try_from(NUM_COMPONENT_CONTEXT_SLOTS).unwrap() { - let saved = self.env.alias_regions.vmdeferred_thread_saved_context( - &mut self.builder.cursor(), - slot_addr, - i, - ); - self.env - .alias_regions - .store_vmstore_context_component_context_slot( - &mut self.builder.cursor(), - vmstore, - i, - saved, - ); - } - self.builder.ins().jump(cont_block, &[]); - // Slow path: the thread was promoted to a real one, so do the - // equivalent out-of-line teardown via the `exit_sync_call` libcall. - self.builder.switch_to_block(slow_block); - let vmctx = self.env.vmctx_val(&mut self.builder.cursor()); let import_off = self.env.offsets.imported_functions().at(callee_index); let func_addr = self .env @@ -2184,10 +2060,8 @@ impl<'a, 'func, 'module_env> Call<'a, 'func, 'module_env> { .relative_to(import_off) .load(&mut self.builder.cursor(), vmctx); self.indirect_call_inst(sig_ref, func_addr, real_call_args); - self.builder.ins().jump(cont_block, &[]); - self.builder.seal_block(cont_block); - self.builder.switch_to_block(cont_block); + slow.finish(self.builder); CallRets::new() } diff --git a/crates/cranelift/src/lib.rs b/crates/cranelift/src/lib.rs index aa9e448e9c79..fabf37bdecda 100644 --- a/crates/cranelift/src/lib.rs +++ b/crates/cranelift/src/lib.rs @@ -41,6 +41,7 @@ mod alias_region; mod bounds_checks; mod builder; mod compiler; +mod component_sync_call; mod debug; mod func_environ; mod translate; diff --git a/tests/misc_testsuite/component-model/async/context-in-resource-drop.wast b/tests/misc_testsuite/component-model/async/context-in-resource-drop.wast new file mode 100644 index 000000000000..87f57bf62b50 --- /dev/null +++ b/tests/misc_testsuite/component-model/async/context-in-resource-drop.wast @@ -0,0 +1,328 @@ +;;! component_model_async = true + +;; A destructor run by `resource.drop` gets fresh context slots, and what it +;; stores there doesn't leak back out to whoever performed the drop. +(component + (core func $get (canon context.get i32 0)) + (core func $set (canon context.set i32 0)) + + (core module $Dtor + (import "" "get" (func $get (result i32))) + (import "" "set" (func $set (param i32))) + (global $ran (mut i32) (i32.const 0)) + (func (export "dtor") (param i32) + (if (i32.ne (call $get) (i32.const 0)) (then unreachable)) + (call $set (i32.const 0xdead)) + (global.set $ran (i32.add (global.get $ran) (i32.const 1)))) + (func (export "ran") (result i32) (global.get $ran)) + ) + (core instance $dtor (instantiate $Dtor (with "" (instance + (export "get" (func $get)) + (export "set" (func $set)))))) + + (type $r (resource (rep i32) (dtor (core func $dtor "dtor")))) + (core func $new (canon resource.new $r)) + (core func $drop (canon resource.drop $r)) + + (core module $M + (import "" "get" (func $get (result i32))) + (import "" "set" (func $set (param i32))) + (import "" "new" (func $new (param i32) (result i32))) + (import "" "drop" (func $drop (param i32))) + (import "" "ran" (func $ran (result i32))) + (func (export "f") + (call $set (i32.const 0x1234)) + (call $drop (call $new (i32.const 100))) + + ;; the destructor must have run, and must not have disturbed our slot + (if (i32.ne (call $ran) (i32.const 1)) (then unreachable)) + (if (i32.ne (call $get) (i32.const 0x1234)) (then unreachable))) + ) + (core instance $m (instantiate $M (with "" (instance + (export "get" (func $get)) + (export "set" (func $set)) + (export "new" (func $new)) + (export "drop" (func $drop)) + (export "ran" (func $dtor "ran")))))) + + (func (export "f") (canon lift (core func $m "f"))) +) +(assert_return (invoke "f")) + +;; Same as above, but for a resource with no destructor at all: nothing runs, so +;; nothing may perturb the caller's context slots either. +(component + (core func $get (canon context.get i32 0)) + (core func $set (canon context.set i32 0)) + + (type $r (resource (rep i32))) + (core func $new (canon resource.new $r)) + (core func $drop (canon resource.drop $r)) + + (core module $M + (import "" "get" (func $get (result i32))) + (import "" "set" (func $set (param i32))) + (import "" "new" (func $new (param i32) (result i32))) + (import "" "drop" (func $drop (param i32))) + (func (export "f") + (call $set (i32.const 0x1234)) + (call $drop (call $new (i32.const 100))) + (if (i32.ne (call $get) (i32.const 0x1234)) (then unreachable))) + ) + (core instance $m (instantiate $M (with "" (instance + (export "get" (func $get)) + (export "set" (func $set)) + (export "new" (func $new)) + (export "drop" (func $drop)))))) + + (func (export "f") (canon lift (core func $m "f"))) +) +(assert_return (invoke "f")) + +;; Destructors nest: a destructor which itself drops a resource sees the inner +;; destructor start from zeroed slots and gets its own slots back afterwards. +(component + (core func $get (canon context.get i32 0)) + (core func $set (canon context.set i32 0)) + + ;; The innermost destructor. + (core module $Dtor2 + (import "" "get" (func $get (result i32))) + (import "" "set" (func $set (param i32))) + (global $ran (mut i32) (i32.const 0)) + (func (export "dtor") (param i32) + (if (i32.ne (call $get) (i32.const 0)) (then unreachable)) + (call $set (i32.const 0x3333)) + (global.set $ran (i32.add (global.get $ran) (i32.const 1)))) + (func (export "ran") (result i32) (global.get $ran)) + ) + (core instance $dtor2 (instantiate $Dtor2 (with "" (instance + (export "get" (func $get)) + (export "set" (func $set)))))) + (type $r2 (resource (rep i32) (dtor (core func $dtor2 "dtor")))) + (core func $new2 (canon resource.new $r2)) + (core func $drop2 (canon resource.drop $r2)) + + ;; The outer destructor, which itself drops an `$r2`. + (core module $Dtor1 + (import "" "get" (func $get (result i32))) + (import "" "set" (func $set (param i32))) + (import "" "new2" (func $new2 (param i32) (result i32))) + (import "" "drop2" (func $drop2 (param i32))) + (global $ran (mut i32) (i32.const 0)) + (func (export "dtor") (param i32) + (if (i32.ne (call $get) (i32.const 0)) (then unreachable)) + (call $set (i32.const 0x2222)) + (call $drop2 (call $new2 (i32.const 200))) + (if (i32.ne (call $get) (i32.const 0x2222)) (then unreachable)) + (global.set $ran (i32.add (global.get $ran) (i32.const 1)))) + (func (export "ran") (result i32) (global.get $ran)) + ) + (core instance $dtor1 (instantiate $Dtor1 (with "" (instance + (export "get" (func $get)) + (export "set" (func $set)) + (export "new2" (func $new2)) + (export "drop2" (func $drop2)))))) + (type $r1 (resource (rep i32) (dtor (core func $dtor1 "dtor")))) + (core func $new1 (canon resource.new $r1)) + (core func $drop1 (canon resource.drop $r1)) + + (core module $M + (import "" "get" (func $get (result i32))) + (import "" "set" (func $set (param i32))) + (import "" "new1" (func $new1 (param i32) (result i32))) + (import "" "drop1" (func $drop1 (param i32))) + (import "" "ran1" (func $ran1 (result i32))) + (import "" "ran2" (func $ran2 (result i32))) + (func (export "f") + (call $set (i32.const 0x1111)) + (call $drop1 (call $new1 (i32.const 100))) + (if (i32.ne (call $ran1) (i32.const 1)) (then unreachable)) + (if (i32.ne (call $ran2) (i32.const 1)) (then unreachable)) + (if (i32.ne (call $get) (i32.const 0x1111)) (then unreachable))) + ) + (core instance $m (instantiate $M (with "" (instance + (export "get" (func $get)) + (export "set" (func $set)) + (export "new1" (func $new1)) + (export "drop1" (func $drop1)) + (export "ran1" (func $dtor1 "ran")) + (export "ran2" (func $dtor2 "ran")))))) + + (func (export "f") (canon lift (core func $m "f"))) +) +(assert_return (invoke "f")) + +;; Component composition case: `$B` drops a handle owned by `$A`, and `$A`'s +;; destructor must still start with zeroed slots without perturbing `$B`'s. +(component + (component $A + (core func $get (canon context.get i32 0)) + (core func $set (canon context.set i32 0)) + + (core module $Dtor + (import "" "get" (func $get (result i32))) + (import "" "set" (func $set (param i32))) + (global $ran (mut i32) (i32.const 0)) + (func (export "dtor") (param i32) + (if (i32.ne (call $get) (i32.const 0)) (then unreachable)) + (call $set (i32.const 0xdead)) + (global.set $ran (i32.add (global.get $ran) (i32.const 1)))) + (func (export "ran") (result i32) (global.get $ran)) + ) + (core instance $dtor (instantiate $Dtor (with "" (instance + (export "get" (func $get)) + (export "set" (func $set)))))) + + (type $t (resource (rep i32) (dtor (core func $dtor "dtor")))) + (core func $new (canon resource.new $t)) + + (core module $M + (import "" "get" (func $get (result i32))) + (import "" "set" (func $set (param i32))) + (import "" "new" (func $new (param i32) (result i32))) + ;; note that this scribbles over its own task's slots on the way out to + ;; make sure that nothing of it survives into the destructor's task + (func (export "make") (result i32) (local $r i32) + (if (i32.ne (call $get) (i32.const 0)) (then unreachable)) + (local.set $r (call $new (i32.const 100))) + (call $set (i32.const 0x5555)) + (local.get $r)) + ) + (core instance $m (instantiate $M (with "" (instance + (export "get" (func $get)) + (export "set" (func $set)) + (export "new" (func $new)))))) + + (export $t' "t" (type $t)) + (func (export "make") (result (own $t')) (canon lift (core func $m "make"))) + (func (export "ran") (result u32) (canon lift (core func $dtor "ran"))) + ) + + (component $B + (import "a" (instance $a + (export "t" (type $t (sub resource))) + (export "make" (func (result (own $t)))) + (export "ran" (func (result u32))))) + + (core func $make (canon lower (func $a "make"))) + (core func $ran (canon lower (func $a "ran"))) + (alias export $a "t" (type $t)) + (core func $drop (canon resource.drop $t)) + (core func $get (canon context.get i32 0)) + (core func $set (canon context.set i32 0)) + + (core module $N + (import "" "make" (func $make (result i32))) + (import "" "ran" (func $ran (result i32))) + (import "" "drop" (func $drop (param i32))) + (import "" "get" (func $get (result i32))) + (import "" "set" (func $set (param i32))) + (func (export "run") (local $r i32) + (local.set $r (call $make)) + (call $set (i32.const 0x1234)) + (call $drop (local.get $r)) + (if (i32.ne (call $ran) (i32.const 1)) (then unreachable)) + (if (i32.ne (call $get) (i32.const 0x1234)) (then unreachable))) + ) + (core instance $n (instantiate $N (with "" (instance + (export "make" (func $make)) + (export "ran" (func $ran)) + (export "drop" (func $drop)) + (export "get" (func $get)) + (export "set" (func $set)))))) + + (func (export "run") (canon lift (core func $n "run"))) + ) + + (instance $a (instantiate $A)) + (instance $b (instantiate $B (with "a" (instance $a)))) + (export "run" (func $b "run")) +) +(assert_return (invoke "run")) + +;; As above, but the destructor forces its deferred thread into a real one with +;; a guest->host call partway through, so `resource.drop` takes the out-of-line +;; exit path. The destructor's own slots must survive that, and `$B`'s must +;; still come back afterwards. +(component + (import "wasmtime" (instance $wasmtime (export "gc" (func)))) + + (component $A + (import "poke" (func $poke)) + (core func $poke' (canon lower (func $poke))) + (core func $get (canon context.get i32 0)) + (core func $set (canon context.set i32 0)) + + (core module $Dtor + (import "" "poke" (func $poke')) + (import "" "get" (func $get (result i32))) + (import "" "set" (func $set (param i32))) + (global $ran (mut i32) (i32.const 0)) + (func (export "dtor") (param i32) + (if (i32.ne (call $get) (i32.const 0)) (then unreachable)) + (call $set (i32.const 0x0dead000)) + ;; Force the deferred thread pushed by `resource.drop`. + (call $poke') + ;; Our own slots survive the force. + (if (i32.ne (call $get) (i32.const 0x0dead000)) (then unreachable)) + (global.set $ran (i32.add (global.get $ran) (i32.const 1)))) + (func (export "ran") (result i32) (global.get $ran))) + (core instance $dtor (instantiate $Dtor (with "" (instance + (export "poke" (func $poke')) + (export "get" (func $get)) + (export "set" (func $set)))))) + + (type $t (resource (rep i32) (dtor (core func $dtor "dtor")))) + (core func $new (canon resource.new $t)) + (core module $M + (import "" "new" (func $new (param i32) (result i32))) + (func (export "make") (result i32) (call $new (i32.const 100)))) + (core instance $m (instantiate $M (with "" (instance + (export "new" (func $new)))))) + + (export $t' "t" (type $t)) + (func (export "make") (result (own $t')) (canon lift (core func $m "make"))) + (func (export "ran") (result u32) (canon lift (core func $dtor "ran")))) + + (component $B + (import "a" (instance $a + (export "t" (type $t (sub resource))) + (export "make" (func (result (own $t)))) + (export "ran" (func (result u32))))) + (core func $make (canon lower (func $a "make"))) + (core func $ran (canon lower (func $a "ran"))) + (alias export $a "t" (type $t)) + (core func $drop (canon resource.drop $t)) + (core func $get (canon context.get i32 0)) + (core func $set (canon context.set i32 0)) + (core module $N + (import "" "make" (func $make (result i32))) + (import "" "ran" (func $ran (result i32))) + (import "" "drop" (func $drop (param i32))) + (import "" "get" (func $get (result i32))) + (import "" "set" (func $set (param i32))) + (func (export "run") (result i32) (local $r i32) + (local.set $r (call $make)) + (call $set (i32.const 0x1234)) + (call $drop (local.get $r)) + ;; Restored after the destructor forced the slow exit path. + (if (i32.ne (call $get) (i32.const 0x1234)) (then unreachable)) + (call $ran))) + (core instance $n (instantiate $N (with "" (instance + (export "make" (func $make)) + (export "ran" (func $ran)) + (export "drop" (func $drop)) + (export "get" (func $get)) + (export "set" (func $set)))))) + (func (export "run") (result u32) (canon lift (core func $n "run")))) + + (instance $a (instantiate $A (with "poke" (func $wasmtime "gc")))) + (instance $b (instantiate $B (with "a" (instance $a)))) + (export "run" (func $b "run")) +) + +(assert_return (invoke "run") (u32.const 1)) +(assert_return (invoke "run") (u32.const 2)) +(assert_return (invoke "run") (u32.const 3)) +(assert_return (invoke "run") (u32.const 4))