-
Notifications
You must be signed in to change notification settings - Fork 32
Description
Context: I'm building asterai, a WASM component registry & runtime (to bundle & run components on top of wasmtime). Hit this issue when resolving a dependency on a published component's interface.
The issue is with wasm-pkg-core's dependency resolution when a dependency is a compiled Component binary (rather than a WIT-only package). The component's runtime world (with all its WASI imports) gets merged into the dep resolution, which causes wit-parser to panic.
Versions
wasm-pkg-core0.15.0wit-parser0.244.0wit-component0.244.0
Reproduction
1. Publish a compiled component to an OCI registry
The component's WIT source is simple, no WASI deps:
package asterai:discord@0.1.0;
interface types {
record message {
content: string,
channel-id: string,
}
}
interface discord {
send-message: func(content: string, channel-id: string) -> string;
}
world component {
export discord;
export types;
}The compiled component.wasm (built from Rust with cargo component build) includes WASI runtime imports: wasi:io, wasi:cli, wasi:filesystem, wasi:http, etc.
Push the compiled component binary to an OCI registry at the version tag asterai/discord:0.1.0.
2. Build a WIT package that depends on it
package asterai:discord-message-listener@0.1.0;
interface incoming-message {
use asterai:discord/types@0.1.0.{message};
handle: func(msg: message);
}
world component {
export wasi:cli/run@0.2.0;
}Call resolve_dependencies followed by generate_resolve (or build_package / fetch_dependencies which call it internally).
3. Result
thread 'tokio-runtime-worker' panicked at wit-parser-0.244.0/src/resolve.rs:1807:25:
world import of wasi:filesystem/types@0.2.0 is missing transitive dep of wasi:io/streams@0.2.0
Without the asterai:discord dependency (i.e. only WIT-only deps from wasi.dev), everything works fine.
Root cause
In resolver.rs (L740), generate_resolve handles DecodedWasm::Component identically to DecodedWasm::WitPackage:
DecodedDependency::Wasm { resolution, decoded } => {
let resolve = match decoded {
DecodedWasm::WitPackage(resolve, _) => resolve,
// merges full runtime resolve!
DecodedWasm::Component(resolve, _) => resolve,
};
merged.merge(resolve).with_context(|| ...)?;
}For a Component, the resolve includes:
- The component's exported package interfaces (what consumers actually need)
- A runtime world with imports for all WASI interfaces (
wasi:io,wasi:cli,wasi:filesystem, etc.) - Full definitions of all those WASI packages
When this is merged, and then the separately-fetched wasi.dev WIT packages are also merged, assert_valid() iterates all worlds in the resolve, including the component's runtime world, and assert_world_elaborated finds broken cross-references from the duplicate WASI package merging.
I believe the runtime world is an implementation detail that shouldn't leak into dep resolution. Consumers only need the package's exported interfaces.
Suggested fix
When decoded is DecodedWasm::Component, sanitize before merging:
- Find the interface package by matching
resolve.packagesagainst the dependency name (note:resolve.worlds[world_id].packagepoints to a syntheticroot:componentpackage, not the actual interface package) - Strip worlds from that package
- Re-encode as WIT-only
- Decode the result (yields a
DecodedWasm::WitPackagewith a clean resolve) - Merge that clean resolve instead
This is what we're doing as a workaround on our side (reimplementing generate_resolve with the sanitization step) and it resolves the issue.
I'm happy to submit a PR if this approach looks right.
By the way: assert_world_elaborated in wit-parser (resolve.rs:1807) uses assert! instead of returning an Err. So the validation failure panics the process. In a server context, this crashes the tokio worker thread. This might warrant a separate issue on bytecodealliance/wasm-tools? Though maybe assert! is reasonable in this scenario.