kvcache added the bug label to Issue #10663.
kvcache opened issue #10663:
Test Case
Any rust at all compiled with
--target wasm32-wasip2Steps to Reproduce
Set up the host linker:
fn link(linker: &mut Linker) { my_own_interface::add_to_linker(linker, |s| s)?; only_wasi_time::add_to_linker(linker)?; }Now a guest compiled for
wasm32-wasip2will not link, because it is missing wasi::cli/environment and several other interfaces.So update the host linker:
After, to catch the imports that would be missing?
fn link(linker: &mut Linker, component: &Component) { my_own_interface::add_to_linker(linker, |s| s)?; only_wasi_time::add_to_linker(linker)?; linker.define_unknown_imports_as_traps(component); }This fails, due to defining duplicate imports for
my_own_interface- which seems like not the behavior I want. After all,my_own_interfaceis implemented and does things!
Before? But if I do that, I'll need to also enable shadowing :grimacing:.
fn link(linker: &mut Linker, component: &Component) { linker.define_unknown_imports_as_traps(component); linker.allow_shadowing(true); my_own_interface::add_to_linker(linker, |s| s); only_wasi_time::add_to_linker(linker); }This worked very briefly, but then I submitted the code to CI which used a newer rust version with 0.2.3 instead of 0.2.0 wasi.
define_unknown_imports_as_trapshad apparently defined the 0.2.3 import as a trap, and allowed the shadowed 0.2.0 import to coexist silently.I updated to 0.2.5, and it's the same error.
unknown import: wasi:clocks/monotonic-clock@0.2.3#now has not been defined)Expected Results
When I configure a Linker, only the unknown imports need are defined as traps when I use
define_unknown_imports_as_traps.I can call
define_unknown_imports_as_trapsafter setting up my linker to force any Component to still link, just with traps for other imports.I do not need to allow shadowing to
define_unknown_imports_as_traps.Actual Results
define_unknown_imports_as_trapsdefines all imports, and defines them as traps even when they are linkable to the Component. This imposes a strict equality constraint on semantic versions of WIT, which only works if you are exactly version aligned.Versions and Environment
Wasmtime version or commit: 0.32.0
Operating system: osx, AL2023
Architecture: aarch64
Extra Info
I'm building a functions as a service product, where users upload wasms. I can't support all of WASI, but I can support some things like time.
I worked around this by:
- removing
define_unknown_imports_as_traps- removing
allow_shadowing- manually implementing unsupported interfaces with
Err(wasmtime::Error::msg("unsupported wasi interface. Contact support@momentohq.com for more information"))This is going to be a better approach for my current project, but
define_unknown_imports_as_trapswas a similar functionality (can you support a custom message string?) without needing to write a massive amount of unimplemented boilerplate.for posterity, the interfaces to implement or stub for
wasm32-wasip2aretime environment error exit filesystem_preopens filesystem_types stderr stdin stdout streamsThanks for this awesome project!
alexcrichton commented on issue #10663:
Thanks for filing this report, and thanks for all the detail! I've been looking into this and I'm unfortunately not sure how best to resolve this. I'm going to write up my learnings here and see what other folks think about this as well.
Your first attempt:
fn link(linker: &mut Linker, component: &Component) { my_own_interface::add_to_linker(linker, |s| s)?; only_wasi_time::add_to_linker(linker)?; linker.define_unknown_imports_as_traps(component); }pretty much exactly follows the original issue motivating the
define_unknown_imports_as_trapsfunction, and thus the intention was that this would work. You say though that this fails due to duplicate imports formy_own_interface, which is basically a bug in the implementation. Here is where despitemy_own_interfacebeing defined we continue down below because we're dealing with an instance import. A few lines down though this fails because creating a newinstancerequires that the name is unique and not seen before, but in this case it was already define.This bug is, I believe, basically a showstopper. If you define _any_ instance in a linker and then also call
define_unknown_imports_as_trapsthen this won't work. This is just a mistake though and definitely not the originally intended behavior.Your second attempt:
fn link(linker: &mut Linker, component: &Component) { linker.define_unknown_imports_as_traps(component); linker.allow_shadowing(true); my_own_interface::add_to_linker(linker, |s| s); only_wasi_time::add_to_linker(linker); // 0.2.0 or 0.2.5 }fails regardless of whether you define WASI 0.2.0 or 0.2.5 in the linker when the guest module requests 0.2.3. This is an unfortunate interaction with how the
Linkerhandles multiple definitions of the same interface at different versions. What's happening is that the first call todefine_unknown_imports_as_trapsis defining trapping functions for 0.2.3. The second call toonly_wasi_time::add_to_linkerthen defines either 0.2.0 or 0.2.5, depending on your host. In both cases though the component in question uses the trapping versions, not the host versions. This is due to the fact thatLinkerwill provide an exact semver match if one is available, and otherwise fall back to a compatible version. Here an exact match is available due todefine_unknown_imports_as_trapshappening first (it defines 0.2.3) and the 0.2.0 and 0.2.5 versions never get used due to the component importing 0.2.3.
So ok I think that explains all the behavior you're seeing. The first one is definitely a bug and the second is an unfortunate and/or confusing interaction with semver requirements. Ideally though it shouldn't matter as we should just get the first iteration working.
Ok so how to get that working? That's what I don't know how to do. Right now
define_unknown_imports_as_trapsis defined in terms of mostly other public methods ofLinkerwhich is pretty nice from a maintainability point of view. The problem with this though is how to recurse into instances? We have a few cases:
- If a new name
foo:bar/baz@1.2.3has never been defined, at any version, it's obvious to just make a definition and define all internal pieces as traps.- A name
foo:bar/baz@1.2.3might have already been defined, but some components of the instance may be missing (e.g. you implement half of thewasi:clocks/monotonic-clockinterface)- A name
foo:bar/baz@1.2.3was defined at a different version. Ideally we want to use functions/resources from that since they're "compatible" and we only want to define new functions/resources as stubs.Above (1) is easy to do, but (2) and (3) raise unfortunate questions. Right now there's no concept of "reopening" an instance to add more definitions. Once you call
Linker::instance(...)for example you've locked that name forever and can't retroactively add more items into it. Such a behavior change would be required to implement (2) or (3). For (3) though we could be adding functions meant for one version of an interface to another version of an interface, which also feels a bit weird!
So finally, what I'm left with. My thoughts on how to approach this are:
- Redefine this function as
define_unknown_interfaces_as_traps. That's much clearer to me and we basically don't have to deal with cases (2) or (3) above. Instead the function is documented as wholly unknown interfaces get trapping functions. Partially implemented interfaces, though, are still a link-time error. This I don't think is a great experience because let's say your host provideswasi:clocks/monotonic-clock@0.2.0, but in 0.2.10 a new function is added. In theory you want that to be a trapping stub until the host implements it if you want the high-level goal of "just implement missing functions as traps".- Implement "reopening the instance" behavior. This means that redefining an instance is no longer an error for any
Linkermethods. Instead it means you'd add more items to existing instances. That would neatly solve (2) above, and otherwise this function would document in the case of (3) that new functions might be added to interfaces of different versions if one is matching. A bit odd from a purely theoretical point of view, but otherwise fits-the-bill from a practical point of view.- Completely reimplement
define_unknown_imports_as_traps. Instead of defining items individually instead just set a boolean inLinkeror something like that. This would affect the type-checking and pre-instantiation phase where they would both have to consult this boolean when dealing with non-present imports. Not great from a maintainability point of view (this feature leaks into the rest ofLinker) but perhaps the cleanest from a theoretical/conceptual point of view.Personally I'm sort of leaning towards (2). Does anyone else have particular thoughts on directions to take this though?
kvcache commented on issue #10663:
For my own part, I just needed a nudge to go ahead and hammer out the entire wasm32-wasip2 import set. I do not want to have unknown imports trap at runtime - I want them to fail linking, so my users know as soon as possible that their application is not runnable. That's not practical behavior to create for
std, unfortunately, because as far as I know making somewasm32-momentotarget would be a massive, fragmenting undertaking.I think there's a [4] which might be "remove define_unknown_interfaces_as_traps" :big_smile:. I'm sure there are some useful cases but it seems like a massive footgun. It was painful to stub out all the wasip2 imports, but it's much more maintainable and clear this way.
Maybe there's a [5] too, which would be sort of a combination of [2] and [4].
add_to_linker_and_trap_unknown_imports()would be great - in this way you could deal with problem_case2, without globally permitting every unknown import on every interface the linker touches.
I think you'd also want something like anUnknownModule::new("foo:bar/baz@1.2.4").add_to_linker_trap_unknown_imports()to let people opt in to other interfaces having a behavior like this.I do not dislike solution [1] or [2], but I'd love [5]!
Last updated: Dec 06 2025 at 06:05 UTC