Stream: git-wasmtime

Topic: wasmtime / issue #10663 [Linker] `define_unknown_imports_...


view this post on Zulip Wasmtime GitHub notifications bot (Apr 24 2025 at 02:24):

kvcache added the bug label to Issue #10663.

view this post on Zulip Wasmtime GitHub notifications bot (Apr 24 2025 at 02:24):

kvcache opened issue #10663:

Test Case

Any rust at all compiled with --target wasm32-wasip2

Steps 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-wasip2 will 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_interface is 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_traps had 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_traps after 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_traps defines 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:

  1. removing define_unknown_imports_as_traps
  2. removing allow_shadowing
  3. 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_traps was 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-wasip2 are

time
environment
error
exit
filesystem_preopens
filesystem_types
stderr
stdin
stdout
streams

Thanks for this awesome project!

view this post on Zulip Wasmtime GitHub notifications bot (Apr 24 2025 at 21:09):

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_traps function, and thus the intention was that this would work. You say though that this fails due to duplicate imports for my_own_interface, which is basically a bug in the implementation. Here is where despite my_own_interface being defined we continue down below because we're dealing with an instance import. A few lines down though this fails because creating a new instance requires 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_traps then 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 Linker handles multiple definitions of the same interface at different versions. What's happening is that the first call to define_unknown_imports_as_traps is defining trapping functions for 0.2.3. The second call to only_wasi_time::add_to_linker then 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 that Linker will provide an exact semver match if one is available, and otherwise fall back to a compatible version. Here an exact match is available due to define_unknown_imports_as_traps happening 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_traps is defined in terms of mostly other public methods of Linker which 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:

  1. If a new name foo:bar/baz@1.2.3 has never been defined, at any version, it's obvious to just make a definition and define all internal pieces as traps.
  2. A name foo:bar/baz@1.2.3 might have already been defined, but some components of the instance may be missing (e.g. you implement half of the wasi:clocks/monotonic-clock interface)
  3. A name foo:bar/baz@1.2.3 was 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:

  1. 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 provides wasi: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".
  2. Implement "reopening the instance" behavior. This means that redefining an instance is no longer an error for any Linker methods. 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.
  3. Completely reimplement define_unknown_imports_as_traps. Instead of defining items individually instead just set a boolean in Linker or 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 of Linker) 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?

view this post on Zulip Wasmtime GitHub notifications bot (Apr 25 2025 at 17:06):

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 some wasm32-momento target 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 an UnknownModule::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