From daa96e7885be20a51b6796745f92958147548977 Mon Sep 17 00:00:00 2001 From: Nick Fitzgerald Date: Thu, 22 Jan 2026 12:01:00 -0800 Subject: [PATCH 1/5] Document panics from using CM async machinery when CM async is not enabled --- .../concurrent/futures_and_streams.rs | 32 ++++++++++++++++--- .../src/runtime/component/func/typed.rs | 7 +++- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/concurrent/futures_and_streams.rs b/crates/wasmtime/src/runtime/component/concurrent/futures_and_streams.rs index a352e89f805a..290fb449dfba 100644 --- a/crates/wasmtime/src/runtime/component/concurrent/futures_and_streams.rs +++ b/crates/wasmtime/src/runtime/component/concurrent/futures_and_streams.rs @@ -1116,6 +1116,10 @@ pub struct FutureReader { impl FutureReader { /// Create a new future with the specified producer. + /// + /// # Panics + /// + /// Panics if component-model-async is not enabled in this store's config. pub fn new( mut store: S, producer: impl FutureProducer, @@ -1123,7 +1127,10 @@ impl FutureReader { where T: func::Lower + func::Lift + Send + Sync + 'static, { - assert!(store.as_context().0.cm_concurrency_enabled()); + assert!( + store.as_context().0.cm_concurrency_enabled(), + "cannot use `FutureReader::new` when component-model-async is not enabled on the config" + ); struct Producer

(P); @@ -1451,11 +1458,16 @@ where A: AsAccessor, { /// Create a new `GuardedFutureReader` with the specified `accessor` and `reader`. + /// + /// # Panics + /// + /// Panics if component-model-async is not enabled in this store's config. pub fn new(accessor: A, reader: FutureReader) -> Self { assert!( accessor .as_accessor() - .with(|a| a.as_context().0.cm_concurrency_enabled()) + .with(|a| a.as_context().0.cm_concurrency_enabled()), + "cannot use `GuardedFutureReader` when component-model-async is not enabled on the config" ); Self { reader: Some(reader), @@ -1503,6 +1515,10 @@ pub struct StreamReader { impl StreamReader { /// Create a new stream with the specified producer. + /// + /// # Panics + /// + /// Panics if component-model-async is not enabled in this store's config. pub fn new( mut store: S, producer: impl StreamProducer, @@ -1510,7 +1526,10 @@ impl StreamReader { where T: func::Lower + func::Lift + Send + Sync + 'static, { - assert!(store.as_context().0.cm_concurrency_enabled()); + assert!( + store.as_context().0.cm_concurrency_enabled(), + "cannot use `StreamReader` when component-model-async is not enabled on the config" + ); Self::new_( store .as_context_mut() @@ -1785,11 +1804,16 @@ where { /// Create a new `GuardedStreamReader` with the specified `accessor` and /// `reader`. + /// + /// # Panics + /// + /// Panics if component-model-async is not enabled in this store's config. pub fn new(accessor: A, reader: StreamReader) -> Self { assert!( accessor .as_accessor() - .with(|a| a.as_context().0.cm_concurrency_enabled()) + .with(|a| a.as_context().0.cm_concurrency_enabled()), + "cannot use `GuardedStreamReader` when component-model-async is not enabled on the config" ); Self { reader: Some(reader), diff --git a/crates/wasmtime/src/runtime/component/func/typed.rs b/crates/wasmtime/src/runtime/component/func/typed.rs index ed9b8a703a76..0ca36d9275ed 100644 --- a/crates/wasmtime/src/runtime/component/func/typed.rs +++ b/crates/wasmtime/src/runtime/component/func/typed.rs @@ -271,6 +271,8 @@ where /// Panics if the store that the [`Accessor`] is derived from does not own /// this function. /// + /// Panics if component-model-async is not enabled in this store's config. + /// /// # Example /// /// Using [`StoreContextMut::run_concurrent`] to get an [`Accessor`]: @@ -313,7 +315,10 @@ where { let result = accessor.as_accessor().with(|mut store| { let mut store = store.as_context_mut(); - assert!(store.0.cm_concurrency_enabled()); + assert!( + store.0.cm_concurrency_enabled(), + "cannot use `call_concurrent` when component-model-async is not enabled on the config" + ); let prepared = self.prepare_call(store.as_context_mut(), false, true, move |cx, ty, dst| { From 4dac66e15e089f06b58f33c05880c68e1da4156f Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Fri, 23 Jan 2026 10:51:01 -0800 Subject: [PATCH 2/5] Refactor how concurrency support is enabled in a `Store` This commit is an extension/refactor of #12377 and #12379. Notably this decouples the runtime behavior of Wasmtime from enabled/disabled WebAssembly proposals. This enables the `wasmtime serve` subcommand, for example, to continue to disallow component-model-async by default but continue to use `*_concurrent` under the hood. Specifically a new `Config::concurrency_support` knob is added. This is plumbed directly through to `Tunables` and takes over the preexisting `component_model_concurrency` field. This field configures whether tasks/etc are enabled at runtime for component-y things. The default value of this configuration option is the same as `cfg!(feature = "component-model-async")`, and this field is required if component-model-async wasm proposals are enabled. It's intended that eventually this'll affect on-by-default wasm features in Wasmtime depending if the support is compiled in. This results in a subtle shift in behavior where component-model-async concurrency is used by default now because the feature is turned on by default, even though the wasm features are off-by-default. This required adjusting a few indices expected in runtime tests due to tasks/threads being allocated in index spaces. Finally, this additionally denies access at runtime to `Linker::*_concurrent` when concurrent support is disabled as otherwise the various runtime data structures won't be initialized and panics will happen. Closes #12393 --- crates/cranelift/src/compiler/component.rs | 2 +- crates/environ/src/fact/trampoline.rs | 10 +-- crates/environ/src/tunables.rs | 4 +- crates/wasmtime/src/config.rs | 71 +++++++++++++++---- crates/wasmtime/src/engine/serialization.rs | 8 +-- .../src/runtime/component/concurrent.rs | 44 +++++------- .../concurrent/futures_and_streams.rs | 32 ++++----- .../runtime/component/concurrent_disabled.rs | 11 ++- crates/wasmtime/src/runtime/component/func.rs | 10 ++- .../src/runtime/component/func/typed.rs | 17 +++-- .../src/runtime/component/instance.rs | 7 +- .../wasmtime/src/runtime/component/linker.rs | 9 +++ crates/wasmtime/src/runtime/component/mod.rs | 35 --------- .../src/runtime/component/resources/any.rs | 7 +- .../wasmtime/src/runtime/component/store.rs | 17 +++++ crates/wasmtime/src/runtime/store.rs | 26 ++++--- src/commands/serve.rs | 1 - tests/all/component_model/async.rs | 45 ++++++++++++ tests/all/component_model/resources.rs | 6 +- 19 files changed, 227 insertions(+), 135 deletions(-) diff --git a/crates/cranelift/src/compiler/component.rs b/crates/cranelift/src/compiler/component.rs index 6a6c5b8224a7..93795cd6da89 100644 --- a/crates/cranelift/src/compiler/component.rs +++ b/crates/cranelift/src/compiler/component.rs @@ -1220,7 +1220,7 @@ impl<'a> TrampolineCompiler<'a> { .ins() .trapz(masked, TRAP_CANNOT_LEAVE_COMPONENT); - if self.compiler.tunables.component_model_concurrency { + if self.compiler.tunables.concurrency_support { // Stash the old value of `may_block` and then set it to false. let old_may_block = self.builder.ins().load( ir::types::I32, diff --git a/crates/environ/src/fact/trampoline.rs b/crates/environ/src/fact/trampoline.rs index 160949ae06b8..79cf0a49eb10 100644 --- a/crates/environ/src/fact/trampoline.rs +++ b/crates/environ/src/fact/trampoline.rs @@ -180,7 +180,7 @@ pub(super) fn compile(module: &mut Module<'_>, adapter: &AdapterData) { compiler.compile_sync_to_sync_adapter(adapter, &lower_sig, &lift_sig) } (true, true) => { - assert!(module.tunables.component_model_concurrency); + assert!(module.tunables.concurrency_support); // In the async->async case, we must compile a couple of helper functions: // @@ -209,7 +209,7 @@ pub(super) fn compile(module: &mut Module<'_>, adapter: &AdapterData) { ); } (false, true) => { - assert!(module.tunables.component_model_concurrency); + assert!(module.tunables.concurrency_support); // Like the async->async case above, for the sync->async case we // also need `async-start` and `async-return` helper functions to @@ -235,7 +235,7 @@ pub(super) fn compile(module: &mut Module<'_>, adapter: &AdapterData) { ); } (true, false) => { - assert!(module.tunables.component_model_concurrency); + assert!(module.tunables.concurrency_support); // As with the async->async and sync->async cases above, for the // async->sync case we use `async-start` and `async-return` helper @@ -759,7 +759,7 @@ impl<'a, 'b> Compiler<'a, 'b> { Trap::CannotLeaveComponent, ); - let old_task_may_block = if self.module.tunables.component_model_concurrency { + let old_task_may_block = if self.module.tunables.concurrency_support { // Save, clear, and later restore the `may_block` field. let task_may_block = self.module.import_task_may_block(); let old_task_may_block = if self.types[adapter.lift.ty].async_ { @@ -871,7 +871,7 @@ impl<'a, 'b> Compiler<'a, 'b> { self.instruction(Call(exit.as_u32())); } - if self.module.tunables.component_model_concurrency { + if self.module.tunables.concurrency_support { // Pop the task we pushed earlier off of the current task stack. // // FIXME: Apply the optimizations described in #12311. diff --git a/crates/environ/src/tunables.rs b/crates/environ/src/tunables.rs index 5a958a313a8b..fc9b4c3ce18a 100644 --- a/crates/environ/src/tunables.rs +++ b/crates/environ/src/tunables.rs @@ -143,7 +143,7 @@ define_tunables! { /// Whether any component model feature related to concurrency is /// enabled. - pub component_model_concurrency: bool, + pub concurrency_support: bool, } pub struct ConfigTunables { @@ -219,7 +219,7 @@ impl Tunables { inlining_small_callee_size: 50, inlining_sum_size_threshold: 2000, debug_guest: false, - component_model_concurrency: true, + concurrency_support: true, } } diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs index d500ed9c372f..08d146052e92 100644 --- a/crates/wasmtime/src/config.rs +++ b/crates/wasmtime/src/config.rs @@ -2358,6 +2358,10 @@ impl Config { let mut tunables = Tunables::default_for_target(&self.compiler_target())?; + // By default this is enabled with the Cargo feature, and if the feature + // is missing this is disabled. + tunables.concurrency_support = cfg!(feature = "component-model-async"); + // If no target is explicitly specified then further refine `tunables` // for the configuration of this host depending on what platform // features were found available at compile time. This means that anyone @@ -2430,9 +2434,23 @@ impl Config { ); } - #[cfg(feature = "component-model")] - { - tunables.component_model_concurrency = self.cm_concurrency_enabled(); + // Concurrency support is required for some component model features. + let requires_concurrency = WasmFeatures::CM_ASYNC + | WasmFeatures::CM_ASYNC_BUILTINS + | WasmFeatures::CM_ASYNC_STACKFUL + | WasmFeatures::CM_THREADING + | WasmFeatures::CM_ERROR_CONTEXT; + if tunables.concurrency_support && !cfg!(feature = "component-model-async") { + bail!( + "concurrency support was requested but was not \ + compiled into this build of Wasmtime" + ) + } + if !tunables.concurrency_support && features.intersects(requires_concurrency) { + bail!( + "concurrency support must be enabled to use the component \ + model async or threading features" + ) } Ok((tunables, features)) @@ -2923,17 +2941,42 @@ impl Config { self } - #[cfg(feature = "component-model")] - #[inline] - pub(crate) fn cm_concurrency_enabled(&self) -> bool { - cfg!(feature = "component-model-async") - && self.enabled_features.intersects( - WasmFeatures::CM_ASYNC - | WasmFeatures::CM_ASYNC_BUILTINS - | WasmFeatures::CM_ASYNC_STACKFUL - | WasmFeatures::CM_THREADING - | WasmFeatures::CM_ERROR_CONTEXT, - ) + /// Specifies whether support for concurrent execution of WebAssembly is + /// supported within this store. + /// + /// This configuration option affects whether runtime data structures are + /// initialized within a `Store` on creation to support concurrent execution + /// of WebAssembly guests. This is primarily applicable to the + /// [`Config::wasm_component_model_async`] configuration which is the first + /// time Wasmtime has supported concurrent execution of guests. This + /// configuration option, for example, enables usage of + /// [`Store::run_concurrent`], [`Func::call_concurrent`], [`StreamReader`], + /// etc. + /// + /// This configuration option can be manually disabled to avoid initializing + /// data structures in the [`Store`] related to concurrent execution. When + /// this option is disabled then APIs related to concurrency will all fail + /// with a panic. For example [`Store::run_concurrent`] will panic, creating + /// a [`StreamReader`] will panic, etc. + /// + /// The value of this option additionally affects whether a [`Config`] is + /// valid and the default set of enabled WebAssembly features. If this + /// option is disabled then component-model features related to concurrency + /// will all be disabled. If this option is enabled, then the options will + /// retain their normal defaults. It is not valid to create a [`Config`] + /// with component-model-async explicitly enabled and this option explicitly + /// disabled, however. + /// + /// This option defaults to `true`. + /// + /// [`Store`]: crate::Store + /// [`Store::run_concurrent`]: crate::Store::run_concurrent + /// [`Func::call_concurrent`]: crate::component::Func::call_concurrent + /// [`StreamReader`]: crate::component::StreamReader + #[cfg(feature = "component-model-async")] + pub fn concurrency_support(&mut self, enable: bool) -> &mut Self { + self.tunables.concurrency_support = Some(enable); + self } } diff --git a/crates/wasmtime/src/engine/serialization.rs b/crates/wasmtime/src/engine/serialization.rs index 190183971248..c014cb86116b 100644 --- a/crates/wasmtime/src/engine/serialization.rs +++ b/crates/wasmtime/src/engine/serialization.rs @@ -285,7 +285,7 @@ impl Metadata<'_> { inlining_intra_module, inlining_small_callee_size, inlining_sum_size_threshold, - component_model_concurrency, + concurrency_support, // This doesn't affect compilation, it's just a runtime setting. memory_reservation_for_growth: _, @@ -367,9 +367,9 @@ impl Metadata<'_> { "function inlining sum-size threshold", )?; Self::check_bool( - component_model_concurrency, - other.component_model_concurrency, - "component model concurrency", + concurrency_support, + other.concurrency_support, + "concurrency support", )?; Self::check_intra_module_inlining(inlining_intra_module, other.inlining_intra_module)?; diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index fcff9d596511..a2f934ad2692 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -886,8 +886,8 @@ impl Store { T: Send + 'static, { ensure!( - self.as_context().0.cm_concurrency_enabled(), - "cannot use `run_concurrent` without enabling component-model async" + self.as_context().0.concurrency_support(), + "cannot use `run_concurrent` Config::concurrency_support disabled", ); self.as_context_mut().run_concurrent(fun).await } @@ -983,6 +983,11 @@ impl StoreContextMut<'_, T> { /// This function can be used to invoke [`Func::call_concurrent`] for /// example within the async closure provided here. /// + /// This function will unconditionally return an error if + /// [`Config::concurrency_support]` is disabled. + /// + /// [`Config::concurrency_support`]: crate::Config::concurrency_support + /// /// # Store-blocking behavior /// /// At this time there are certain situations in which the `Future` returned @@ -1056,8 +1061,8 @@ impl StoreContextMut<'_, T> { T: Send + 'static, { ensure!( - self.0.cm_concurrency_enabled(), - "cannot use `run_concurrent` without enabling component-model async" + self.0.concurrency_support(), + "cannot use `run_concurrent` Config::concurrency_support disabled", ); self.do_run_concurrent(fun, false).await } @@ -1080,7 +1085,7 @@ impl StoreContextMut<'_, T> { where T: Send + 'static, { - debug_assert!(self.0.cm_concurrency_enabled()); + debug_assert!(self.0.concurrency_support()); check_recursive_run(); let token = StoreToken::new(self.as_context_mut()); @@ -1513,8 +1518,10 @@ impl StoreOpaque { /// - The top-level instance is not already on the current task's call stack. /// - The instance is not in need of a post-return function call. /// - `self` has not been poisoned due to a trap. - pub(crate) fn may_enter_concurrent(&mut self, instance: RuntimeInstance) -> bool { - debug_assert!(self.cm_concurrency_enabled()); + pub(crate) fn may_enter(&mut self, instance: RuntimeInstance) -> bool { + if !self.concurrency_support() { + return self.may_enter_at_all(instance); + } let state = self.concurrent_state_mut(); if let Some(caller) = state.guest_thread { instance != state.get_mut(caller.task).unwrap().instance @@ -1524,23 +1531,6 @@ impl StoreOpaque { } } - /// Returns `false` if the specified instance may not be entered, regardless - /// of what's on a task's call stack. - /// - /// If this returns `true`, the instance may be entered as long as it isn't - /// on the task's call stack, if applicable. - fn may_enter_at_all(&self, instance: RuntimeInstance) -> bool { - if self.trapped() { - return false; - } - - let flags = self - .component_instance(instance.instance) - .instance_flags(instance.index); - - unsafe { !flags.needs_post_return() } - } - /// Variation of `may_enter` which takes a `TableId` representing /// the callee. fn may_enter_task(&mut self, task: TableId) -> bool { @@ -1641,8 +1631,10 @@ impl StoreOpaque { .set_task_may_block(may_block) } - pub(crate) fn check_blocking_concurrent(&mut self) -> Result<()> { - debug_assert!(self.cm_concurrency_enabled()); + pub(crate) fn check_blocking(&mut self) -> Result<()> { + if !self.concurrency_support() { + return Ok(()); + } let state = self.concurrent_state_mut(); let task = state.guest_thread.unwrap().task; let instance = state.get_mut(task).unwrap().instance.instance; diff --git a/crates/wasmtime/src/runtime/component/concurrent/futures_and_streams.rs b/crates/wasmtime/src/runtime/component/concurrent/futures_and_streams.rs index 290fb449dfba..82c7ab33703b 100644 --- a/crates/wasmtime/src/runtime/component/concurrent/futures_and_streams.rs +++ b/crates/wasmtime/src/runtime/component/concurrent/futures_and_streams.rs @@ -1119,7 +1119,9 @@ impl FutureReader { /// /// # Panics /// - /// Panics if component-model-async is not enabled in this store's config. + /// Panics if [`Config::concurrency_support`] is not enabled. + /// + /// [`Config::concurrency_support`]: crate::Config::concurrency_support pub fn new( mut store: S, producer: impl FutureProducer, @@ -1127,10 +1129,7 @@ impl FutureReader { where T: func::Lower + func::Lift + Send + Sync + 'static, { - assert!( - store.as_context().0.cm_concurrency_enabled(), - "cannot use `FutureReader::new` when component-model-async is not enabled on the config" - ); + assert!(store.as_context().0.concurrency_support()); struct Producer

(P); @@ -1461,13 +1460,14 @@ where /// /// # Panics /// - /// Panics if component-model-async is not enabled in this store's config. + /// Panics if [`Config::concurrency_support`] is not enabled. + /// + /// [`Config::concurrency_support`]: crate::Config::concurrency_support pub fn new(accessor: A, reader: FutureReader) -> Self { assert!( accessor .as_accessor() - .with(|a| a.as_context().0.cm_concurrency_enabled()), - "cannot use `GuardedFutureReader` when component-model-async is not enabled on the config" + .with(|a| a.as_context().0.concurrency_support()) ); Self { reader: Some(reader), @@ -1518,7 +1518,9 @@ impl StreamReader { /// /// # Panics /// - /// Panics if component-model-async is not enabled in this store's config. + /// Panics if [`Config::concurrency_support`] is not enabled. + /// + /// [`Config::concurrency_support`]: crate::Config::concurrency_support pub fn new( mut store: S, producer: impl StreamProducer, @@ -1526,10 +1528,7 @@ impl StreamReader { where T: func::Lower + func::Lift + Send + Sync + 'static, { - assert!( - store.as_context().0.cm_concurrency_enabled(), - "cannot use `StreamReader` when component-model-async is not enabled on the config" - ); + assert!(store.as_context().0.concurrency_support()); Self::new_( store .as_context_mut() @@ -1807,13 +1806,14 @@ where /// /// # Panics /// - /// Panics if component-model-async is not enabled in this store's config. + /// Panics if [`Config::concurrency_support`] is not enabled. + /// + /// [`Config::concurrency_support`]: crate::Config::concurrency_support pub fn new(accessor: A, reader: StreamReader) -> Self { assert!( accessor .as_accessor() - .with(|a| a.as_context().0.cm_concurrency_enabled()), - "cannot use `GuardedStreamReader` when component-model-async is not enabled on the config" + .with(|a| a.as_context().0.concurrency_support()) ); Self { reader: Some(reader), diff --git a/crates/wasmtime/src/runtime/component/concurrent_disabled.rs b/crates/wasmtime/src/runtime/component/concurrent_disabled.rs index fa8ea16708f1..5bfd79f762f6 100644 --- a/crates/wasmtime/src/runtime/component/concurrent_disabled.rs +++ b/crates/wasmtime/src/runtime/component/concurrent_disabled.rs @@ -7,8 +7,7 @@ use core::convert::Infallible; use core::mem::MaybeUninit; use wasmtime_environ::component::{CanonicalAbiInfo, InterfaceType}; -#[derive(Default)] -pub struct ConcurrentState; +pub enum ConcurrentState {} fn should_have_failed_validation(what: &str) -> Result { // This should be unreachable; if we trap here, it indicates a @@ -168,4 +167,12 @@ impl StoreOpaque { // See comment in `enter_sync_call` unreachable!() } + + pub(crate) fn check_blocking(&mut self) -> crate::Result<()> { + Ok(()) + } + + pub(crate) fn may_enter(&mut self, instance: RuntimeInstance) -> bool { + self.may_enter_at_all(instance) + } } diff --git a/crates/wasmtime/src/runtime/component/func.rs b/crates/wasmtime/src/runtime/component/func.rs index e56d933ab000..4bfad04bbc25 100644 --- a/crates/wasmtime/src/runtime/component/func.rs +++ b/crates/wasmtime/src/runtime/component/func.rs @@ -258,7 +258,7 @@ impl Func { let store = store.as_context_mut(); #[cfg(feature = "component-model-async")] - if store.0.cm_concurrency_enabled() { + if store.0.concurrency_support() { return store .run_concurrent_trap_on_idle(async |store| { self.call_concurrent_dynamic(store, params, results, false) @@ -353,6 +353,10 @@ impl Func { /// but the task will still progress and invoke callbacks and such until /// completion. /// + /// This function will return an error if [`Config::concurrency_support`] is + /// disabled. + /// + /// [`Config::concurrency_support`]: crate::Config::concurrency_support /// [`run_concurrent`]: crate::Store::run_concurrent /// [#11833]: https://github.com/bytecodealliance/wasmtime/issues/11833 /// [`Accessor`]: crate::component::Accessor @@ -610,7 +614,7 @@ impl Func { bail!(crate::Trap::CannotEnterComponent); } - if store.engine().config().cm_concurrency_enabled() { + if store.0.concurrency_support() { let async_type = self.abi_async(store.0); store.0.enter_sync_call(None, async_type, instance)?; } @@ -812,7 +816,7 @@ impl Func { } .exit_call()?; - if !async_ && store.engine().config().cm_concurrency_enabled() { + if !async_ && store.0.concurrency_support() { store.0.exit_sync_call(false)?; } } diff --git a/crates/wasmtime/src/runtime/component/func/typed.rs b/crates/wasmtime/src/runtime/component/func/typed.rs index 0ca36d9275ed..bc81692c38db 100644 --- a/crates/wasmtime/src/runtime/component/func/typed.rs +++ b/crates/wasmtime/src/runtime/component/func/typed.rs @@ -181,7 +181,7 @@ where let mut store = store.as_context_mut(); #[cfg(feature = "component-model-async")] - if store.0.cm_concurrency_enabled() { + if store.0.concurrency_support() { use crate::component::concurrent::TaskId; use crate::runtime::vm::SendSyncPtr; use core::ptr::NonNull; @@ -258,6 +258,11 @@ where /// representing the completion of the guest task and any transitive /// subtasks it might create. /// + /// This function will return an error if [`Config::concurrency_support`] is + /// disabled. + /// + /// [`Config::concurrency_support`]: crate::Config::concurrency_support + /// /// # Progress and Cancellation /// /// For more information about how to make progress on the wasm task or how @@ -271,8 +276,6 @@ where /// Panics if the store that the [`Accessor`] is derived from does not own /// this function. /// - /// Panics if component-model-async is not enabled in this store's config. - /// /// # Example /// /// Using [`StoreContextMut::run_concurrent`] to get an [`Accessor`]: @@ -315,9 +318,9 @@ where { let result = accessor.as_accessor().with(|mut store| { let mut store = store.as_context_mut(); - assert!( - store.0.cm_concurrency_enabled(), - "cannot use `call_concurrent` when component-model-async is not enabled on the config" + ensure!( + store.0.concurrency_support(), + "cannot use `call_concurrent` Config::concurrency_support disabled", ); let prepared = @@ -373,7 +376,7 @@ where Return: 'static, { use crate::component::storage::slice_to_storage; - debug_assert!(store.0.cm_concurrency_enabled()); + debug_assert!(store.0.concurrency_support()); let param_count = if Params::flatten_count() <= MAX_FLAT_PARAMS { Params::flatten_count() diff --git a/crates/wasmtime/src/runtime/component/instance.rs b/crates/wasmtime/src/runtime/component/instance.rs index 887313e78802..271a4d2f5632 100644 --- a/crates/wasmtime/src/runtime/component/instance.rs +++ b/crates/wasmtime/src/runtime/component/instance.rs @@ -869,10 +869,9 @@ impl<'a> Instantiator<'a> { } }; - let exit = if let (&Some(component_instance), true) = ( - component_instance, - store.engine().config().cm_concurrency_enabled(), - ) { + let exit = if let Some(component_instance) = *component_instance + && store.0.concurrency_support() + { store.0.enter_sync_call( None, false, diff --git a/crates/wasmtime/src/runtime/component/linker.rs b/crates/wasmtime/src/runtime/component/linker.rs index 5ef3820d7195..9e3cf42c2f06 100644 --- a/crates/wasmtime/src/runtime/component/linker.rs +++ b/crates/wasmtime/src/runtime/component/linker.rs @@ -531,6 +531,9 @@ impl LinkerInstance<'_, T> { Params: ComponentNamedList + Lift + 'static, Return: ComponentNamedList + Lower + 'static, { + if !self.engine.tunables().concurrency_support { + bail!("concurrent host functions require `Config::concurrency_support`"); + } self.insert(name, Definition::Func(HostFunc::func_wrap_concurrent(f)))?; Ok(()) } @@ -690,6 +693,9 @@ impl LinkerInstance<'_, T> { + Sync + 'static, { + if !self.engine.tunables().concurrency_support { + bail!("concurrent host functions require `Config::concurrency_support`"); + } self.insert(name, Definition::Func(HostFunc::func_new_concurrent(f)))?; Ok(()) } @@ -768,6 +774,9 @@ impl LinkerInstance<'_, T> { + Sync + 'static, { + if !self.engine.tunables().concurrency_support { + bail!("concurrent host functions require `Config::concurrency_support`"); + } // TODO: This isn't really concurrent -- it requires exclusive access to // the store for the duration of the call, preventing guest code from // running until it completes. We should make it concurrent and clean diff --git a/crates/wasmtime/src/runtime/component/mod.rs b/crates/wasmtime/src/runtime/component/mod.rs index b06907125b2c..16d93b12ee28 100644 --- a/crates/wasmtime/src/runtime/component/mod.rs +++ b/crates/wasmtime/src/runtime/component/mod.rs @@ -764,38 +764,3 @@ pub(crate) mod concurrent_disabled; #[cfg(not(feature = "component-model-async"))] pub(crate) use concurrent_disabled as concurrent; - -impl crate::runtime::store::StoreOpaque { - #[cfg(feature = "component-model-async")] - pub(crate) fn cm_concurrency_enabled(&self) -> bool { - let enabled = self.concurrent_state().is_some(); - debug_assert_eq!(enabled, self.engine().config().cm_concurrency_enabled()); - enabled - } - - pub(crate) fn check_blocking(&mut self) -> crate::Result<()> { - #[cfg(feature = "component-model-async")] - if self.cm_concurrency_enabled() { - return self.check_blocking_concurrent(); - } - - Ok(()) - } - - pub(crate) fn may_enter(&mut self, instance: RuntimeInstance) -> bool { - #[cfg(feature = "component-model-async")] - if self.cm_concurrency_enabled() { - return self.may_enter_concurrent(instance); - } - - if self.trapped() { - return false; - } - - let flags = self - .component_instance(instance.instance) - .instance_flags(instance.index); - - unsafe { !flags.needs_post_return() } - } -} diff --git a/crates/wasmtime/src/runtime/component/resources/any.rs b/crates/wasmtime/src/runtime/component/resources/any.rs index 78c417b6728f..27cb760c9e40 100644 --- a/crates/wasmtime/src/runtime/component/resources/any.rs +++ b/crates/wasmtime/src/runtime/component/resources/any.rs @@ -197,10 +197,9 @@ impl ResourceAny { }; let mut args = [ValRaw::u32(rep)]; - let exit = if let (Some(instance), true) = ( - slot.instance, - store.engine().config().cm_concurrency_enabled(), - ) { + let exit = if let Some(instance) = slot.instance + && store.0.concurrency_support() + { store.0.enter_sync_call(None, false, instance)?; true } else { diff --git a/crates/wasmtime/src/runtime/component/store.rs b/crates/wasmtime/src/runtime/component/store.rs index 0b2d571aa52d..9402c209e144 100644 --- a/crates/wasmtime/src/runtime/component/store.rs +++ b/crates/wasmtime/src/runtime/component/store.rs @@ -34,6 +34,23 @@ impl StoreOpaque { pub(crate) fn set_trapped(&mut self) { self.store_data_mut().components.trapped = true; } + + /// Returns `false` if the specified instance may not be entered, regardless + /// of what's on a task's call stack. + /// + /// If this returns `true`, the instance may be entered as long as it isn't + /// on the task's call stack, if applicable. + pub(crate) fn may_enter_at_all(&mut self, instance: RuntimeInstance) -> bool { + if self.trapped() { + return false; + } + + let flags = self + .component_instance(instance.instance) + .instance_flags(instance.index); + + unsafe { !flags.needs_post_return() } + } } impl StoreData { diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs index 4671bc8f7bee..307459b68130 100644 --- a/crates/wasmtime/src/runtime/store.rs +++ b/crates/wasmtime/src/runtime/store.rs @@ -777,8 +777,15 @@ impl Store { host_resource_data: Default::default(), executor: Executor::new(engine), #[cfg(feature = "component-model")] - concurrent_state: if engine.config().cm_concurrency_enabled() { - Some(Default::default()) + concurrent_state: if engine.tunables().concurrency_support { + #[cfg(feature = "component-model-async")] + { + Some(Default::default()) + } + #[cfg(not(feature = "component-model-async"))] + { + unreachable!() + } } else { None }, @@ -2603,17 +2610,20 @@ at https://bytecodealliance.org/security. &mut self.async_state } - #[cfg(feature = "component-model-async")] - pub(crate) fn concurrent_state(&self) -> Option<&concurrent::ConcurrentState> { - self.concurrent_state.as_ref() - } - #[cfg(feature = "component-model-async")] pub(crate) fn concurrent_state_mut(&mut self) -> &mut concurrent::ConcurrentState { - debug_assert!(self.engine().config().cm_concurrency_enabled()); + debug_assert!(self.concurrency_support()); self.concurrent_state.as_mut().unwrap() } + #[inline] + #[cfg(feature = "component-model")] + pub(crate) fn concurrency_support(&self) -> bool { + let support = self.concurrent_state.is_some(); + debug_assert_eq!(support, self.engine().tunables().concurrency_support); + support + } + #[cfg(feature = "async")] pub(crate) fn has_pkey(&self) -> bool { self.pkey.is_some() diff --git a/src/commands/serve.rs b/src/commands/serve.rs index 4d7ca7ff5985..4b87cb893081 100644 --- a/src/commands/serve.rs +++ b/src/commands/serve.rs @@ -403,7 +403,6 @@ impl ServeCommand { .common .config(use_pooling_allocator_by_default().unwrap_or(None))?; config.wasm_component_model(true); - config.wasm_component_model_async(true); if self.run.common.wasm.timeout.is_some() { config.epoch_interruption(true); diff --git a/tests/all/component_model/async.rs b/tests/all/component_model/async.rs index da6cde8b4c7d..7e71ccd38eab 100644 --- a/tests/all/component_model/async.rs +++ b/tests/all/component_model/async.rs @@ -832,3 +832,48 @@ async fn run_wasm_in_call_async() -> Result<()> { run.call_async(&mut store, ()).await?; Ok(()) } + +#[tokio::test] +#[cfg_attr(miri, ignore)] +async fn require_concurrency_support() -> Result<()> { + let mut config = Config::new(); + config.concurrency_support(false); + let engine = Engine::new(&config)?; + + let mut store = Store::new(&engine, ()); + + assert!( + store + .run_concurrent(async |_| wasmtime::error::Ok(())) + .await + .is_err() + ); + + let ok = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + StreamReader::::new(&mut store, Vec::new()); + })); + assert!(ok.is_err()); + + let ok = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + FutureReader::new(&mut store, async { wasmtime::error::Ok(0) }) + })); + assert!(ok.is_err()); + + let mut linker = Linker::<()>::new(&engine); + let mut root = linker.root(); + + assert!( + root.func_wrap_concurrent::<(), (), _>("f1", |_, _| { todo!() }) + .is_err() + ); + assert!( + root.func_new_concurrent("f2", |_, _, _, _| { todo!() }) + .is_err() + ); + assert!( + root.resource_concurrent("f3", ResourceType::host::(), |_, _| { todo!() }) + .is_err() + ); + + Ok(()) +} diff --git a/tests/all/component_model/resources.rs b/tests/all/component_model/resources.rs index 821553fca49b..f40ab07f2526 100644 --- a/tests/all/component_model/resources.rs +++ b/tests/all/component_model/resources.rs @@ -233,7 +233,7 @@ fn mismatch_intrinsics() -> Result<()> { let ctor = i.get_typed_func::<(u32,), (ResourceAny,)>(&mut store, "ctor")?; assert_eq!( ctor.call(&mut store, (100,)).unwrap_err().to_string(), - "handle index 1 used with the wrong type, expected guest-defined \ + "handle index 2 used with the wrong type, expected guest-defined \ resource but found a different guest-defined resource", ); @@ -1411,8 +1411,8 @@ fn guest_different_host_same() -> Result<()> { (func (export "f") (param i32 i32) ;; different types, but everything goes into the same ;; handle index namespace - (if (i32.ne (local.get 0) (i32.const 1)) (then (unreachable))) - (if (i32.ne (local.get 1) (i32.const 2)) (then (unreachable))) + (if (i32.ne (local.get 0) (i32.const 2)) (then (unreachable))) + (if (i32.ne (local.get 1) (i32.const 3)) (then (unreachable))) ;; host should end up getting the same resource (call $f (local.get 0) (local.get 1)) From b02e4addfbee908c485607484bbc05d57e971e2b Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Fri, 23 Jan 2026 13:18:38 -0800 Subject: [PATCH 3/5] Add a `-Wconcurrency-support` CLI flag Used to update disas tests to show that, when disabled, old codegen quality is preserved --- crates/cli-flags/src/lib.rs | 7 +++++++ src/commands/run.rs | 2 +- .../component-model/direct-adapter-calls-inlining.wat | 2 +- tests/disas/component-model/direct-adapter-calls-x64.wat | 2 +- tests/disas/component-model/direct-adapter-calls.wat | 2 +- 5 files changed, 11 insertions(+), 4 deletions(-) diff --git a/crates/cli-flags/src/lib.rs b/crates/cli-flags/src/lib.rs index 6cc61e6cfb07..aad803fe729c 100644 --- a/crates/cli-flags/src/lib.rs +++ b/crates/cli-flags/src/lib.rs @@ -412,6 +412,9 @@ wasmtime_option_group! { /// Component model support for fixed-length lists: this corresponds /// to the 🔧 emoji in the component model specification pub component_model_fixed_length_lists: Option, + /// Whether or not any concurrency infrastructure in Wasmtime is + /// enabled or not. + pub concurrency_support: Option, } enum Wasm { @@ -1006,6 +1009,10 @@ impl CommonOptions { config.gc_support(enable); } + if let Some(enable) = self.wasm.concurrency_support { + config.concurrency_support(enable); + } + if let Some(enable) = self.wasm.shared_memory { config.shared_memory(enable); } diff --git a/src/commands/run.rs b/src/commands/run.rs index edcca310fffd..e3ce0e9a0abe 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -633,7 +633,7 @@ impl RunCommand { results: &mut Vec, ) -> Result<(), Error> { #[cfg(feature = "component-model-async")] - if self.run.common.wasm.component_model_async.unwrap_or(false) { + if self.run.common.wasm.concurrency_support.unwrap_or(true) { store .run_concurrent(async |store| { let task = func.call_concurrent(store, params, results).await?; diff --git a/tests/disas/component-model/direct-adapter-calls-inlining.wat b/tests/disas/component-model/direct-adapter-calls-inlining.wat index 82c1aae8b16f..41bfa66c1d02 100644 --- a/tests/disas/component-model/direct-adapter-calls-inlining.wat +++ b/tests/disas/component-model/direct-adapter-calls-inlining.wat @@ -1,7 +1,7 @@ ;;! target = "x86_64" ;;! test = "optimize" ;;! filter = "wasm[1]--function" -;;! flags = "-C inlining=y" +;;! flags = "-C inlining=y -Wconcurrency-support=n" ;; Same as `direct-adapter-calls.wat`, except we have enabled function inlining ;; so all the direct calls should get inlined. diff --git a/tests/disas/component-model/direct-adapter-calls-x64.wat b/tests/disas/component-model/direct-adapter-calls-x64.wat index 4d5bece74070..984198c73bc2 100644 --- a/tests/disas/component-model/direct-adapter-calls-x64.wat +++ b/tests/disas/component-model/direct-adapter-calls-x64.wat @@ -1,7 +1,7 @@ ;;! target = "x86_64" ;;! test = 'compile' ;;! filter = "function" -;;! flags = "-C inlining=n" +;;! flags = "-C inlining=n -Wconcurrency-support=n" ;; Same as `direct-adapter-calls.wat` but shows full compilation down to x86_64 ;; so that we can exercise our linker's ability to resolve relocations for diff --git a/tests/disas/component-model/direct-adapter-calls.wat b/tests/disas/component-model/direct-adapter-calls.wat index f8e71a7d98aa..8f935101d4d4 100644 --- a/tests/disas/component-model/direct-adapter-calls.wat +++ b/tests/disas/component-model/direct-adapter-calls.wat @@ -1,7 +1,7 @@ ;;! target = "x86_64" ;;! test = "optimize" ;;! filter = "function" -;;! flags = "-C inlining=n" +;;! flags = "-C inlining=n -Wconcurrency-support=n" ;; The following component links two sub-components together and each are only ;; instantiated the once, so we statically know what their core modules' From 04313d43edc032c456461d7e7c86959609a8e5ab Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Fri, 23 Jan 2026 13:26:08 -0800 Subject: [PATCH 4/5] Ungate `Config` flag --- crates/wasmtime/src/config.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs index 08d146052e92..45d01e007e51 100644 --- a/crates/wasmtime/src/config.rs +++ b/crates/wasmtime/src/config.rs @@ -2973,7 +2973,6 @@ impl Config { /// [`Store::run_concurrent`]: crate::Store::run_concurrent /// [`Func::call_concurrent`]: crate::component::Func::call_concurrent /// [`StreamReader`]: crate::component::StreamReader - #[cfg(feature = "component-model-async")] pub fn concurrency_support(&mut self, enable: bool) -> &mut Self { self.tunables.concurrency_support = Some(enable); self From 139e46343fbabe360a63c9a0aebed1c5d630acdf Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Fri, 23 Jan 2026 14:25:45 -0800 Subject: [PATCH 5/5] Review comments --- crates/wasmtime/src/runtime/component/concurrent.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index a2f934ad2692..395b967d3736 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -887,7 +887,7 @@ impl Store { { ensure!( self.as_context().0.concurrency_support(), - "cannot use `run_concurrent` Config::concurrency_support disabled", + "cannot use `run_concurrent` when Config::concurrency_support disabled", ); self.as_context_mut().run_concurrent(fun).await } @@ -984,7 +984,7 @@ impl StoreContextMut<'_, T> { /// example within the async closure provided here. /// /// This function will unconditionally return an error if - /// [`Config::concurrency_support]` is disabled. + /// [`Config::concurrency_support`] is disabled. /// /// [`Config::concurrency_support`]: crate::Config::concurrency_support /// @@ -1062,7 +1062,7 @@ impl StoreContextMut<'_, T> { { ensure!( self.0.concurrency_support(), - "cannot use `run_concurrent` Config::concurrency_support disabled", + "cannot use `run_concurrent` when Config::concurrency_support disabled", ); self.do_run_concurrent(fun, false).await }