From 43c66ffeff973cf65c0176a03dfffd67b498e896 Mon Sep 17 00:00:00 2001 From: Roman Volosatovs Date: Wed, 10 Jun 2026 14:21:40 +0200 Subject: [PATCH 1/2] add test for `substituted_component_type` panic `Linker::substituted_component_type` builds a `types::Component` whose resource substitution map is `Some(..)` but only covers the component's *imported* resources. Introspecting an exported function that references an *exported* (non-imported) resource then panics with an index-out-of-bounds, because `InstanceType::resource_type` hard-indexes that partial map (`matching.rs`) instead of falling back to treating an absent resource as uninstantiated. This adds a test that builds a component exporting a resource plus a constructor returning `own` and a method taking `borrow`, then walks the introspected function types via `substituted_component_type`. It panics today (index out of bounds) and will pass once the resolver falls back gracefully. Assisted-by: claude:claude-opus-4-8 --- tests/all/component_model/dynamic.rs | 76 ++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/tests/all/component_model/dynamic.rs b/tests/all/component_model/dynamic.rs index 95f6d6ba278f..e214410064d5 100644 --- a/tests/all/component_model/dynamic.rs +++ b/tests/all/component_model/dynamic.rs @@ -1238,6 +1238,82 @@ fn introspection() -> Result<()> { Ok(()) } +#[test] +fn substituted_component_type_with_exported_resource() -> Result<()> { + // Regression test: `Linker::substituted_component_type` builds a + // `types::Component` whose resource substitution map only covers *imported* + // resources, but is nonetheless `Some(..)`. Introspecting an exported + // function that references an *exported* (non-imported) resource used to + // panic with an index-out-of-bounds when resolving the resource type, + // because the resolver hard-indexed that partial map instead of falling + // back to treating the resource as uninstantiated (which is what + // `Component::component_type()` does via `resources: None`). + let engine = super::engine(); + + let component = Component::new( + &engine, + r#" + (component + (type $t' (resource (rep i32))) + (export $t "t" (type $t')) + + (core func $ctor (canon resource.new $t)) + (func (export "[constructor]t") (param "x" u32) (result (own $t)) + (canon lift (core func $ctor))) + + (core module $m + (func (export "f") (param i32)) + ) + (core instance $i (instantiate $m)) + (func (export "[method]t.use") (param "self" (borrow $t)) + (canon lift (core func $i "f"))) + ) + "#, + )?; + + let linker = Linker::<()>::new(&engine); + + // This must not panic, even though `t` is an exported resource that is + // absent from the linker's imported-resource substitution map. + let component_ty = linker.substituted_component_type(&component)?; + + let mut exports = component_ty.exports(linker.engine()); + + // The exported resource itself. + let (name, _t_ty) = exports.next().unwrap(); + assert_eq!(name, "t"); + + // `[constructor]t` returns `own`. + let (name, ctor_ty) = exports.next().unwrap(); + assert_eq!(name, "[constructor]t"); + let ComponentItem::ComponentFunc(ctor_ty) = ctor_ty.ty else { + panic!("`[constructor]t` export item of wrong type") + }; + assert_eq!(ctor_ty.params().len(), 1); + let mut ctor_results = ctor_ty.results(); + assert_eq!(ctor_results.len(), 1); + let types::Type::Own(_) = ctor_results.next().unwrap() else { + panic!("`[constructor]t` should return an `own` handle") + }; + + // `[method]t.use` takes `borrow`. + let (name, method_ty) = exports.next().unwrap(); + assert_eq!(name, "[method]t.use"); + let ComponentItem::ComponentFunc(method_ty) = method_ty.ty else { + panic!("`[method]t.use` export item of wrong type") + }; + let mut method_params = method_ty.params(); + assert_eq!(method_params.len(), 1); + let (name, param) = method_params.next().unwrap(); + assert_eq!(name, "self"); + let types::Type::Borrow(_) = param else { + panic!("`[method]t.use` should take a `borrow` handle") + }; + assert_eq!(method_ty.results().len(), 0); + + Ok(()) +} + #[test] fn flags_beyond_end() -> Result<()> { let engine = super::engine(); From 57fdb23fa7f9952b8c867347e2b18f59618c74ce Mon Sep 17 00:00:00 2001 From: Roman Volosatovs Date: Wed, 10 Jun 2026 14:22:49 +0200 Subject: [PATCH 2/2] fix panic on guest-exported resources `Linker::substituted_component_type` constructs the introspection type with `resources: Some(&cx.imported_resources)`, a map that only contains the component's *imported* resources. `InstanceType::resource_type` previously hard-indexed that map (`self.resources.map(|t| t[ty])`), so resolving a resource that is not in the map -- e.g. a resource the component *exports* -- panicked with an index-out-of-bounds whenever an exported function's params/results referenced it. Fix this by using a fallible lookup and falling back to `ResourceType::uninstantiated` when the resource is absent from the map, exactly as the `resources: None` path already does. This is the semantically correct behavior: a resource that has no substitution is "not (yet) instantiated", which is also what `Component::component_type()` relies on (it passes `resources: None`). With this fix the same safe, public introspection API no longer panics depending on which (also public) API produced the `types::Component`. Assisted-by: claude:claude-opus-4-8 --- crates/wasmtime/src/runtime/component/matching.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/wasmtime/src/runtime/component/matching.rs b/crates/wasmtime/src/runtime/component/matching.rs index 47aeeabaf980..9c2dccf3bae4 100644 --- a/crates/wasmtime/src/runtime/component/matching.rs +++ b/crates/wasmtime/src/runtime/component/matching.rs @@ -205,7 +205,7 @@ impl<'a> InstanceType<'a> { match self.types[index] { TypeResourceTable::Concrete { ty, .. } => self .resources - .map(|t| t[ty]) + .and_then(|t| t.get(ty).copied()) .unwrap_or_else(|| ResourceType::uninstantiated(&self.types, ty)), TypeResourceTable::Abstract(ty) => ResourceType::abstract_(&self.types, ty), }