WebAssembly Target Updates: How Undefined Symbols Are Now Handled

Apr 04, 2026 628 views

A breaking change is coming to Rust's WebAssembly targets: the --allow-undefined flag passed to wasm-ld is being removed. Here's what that means, why it matters, and what you may need to update.

What is --allow-undefined?

All WebAssembly binaries in Rust are produced by linking with wasm-ld, which serves the same role as tools like ld, lld, and mold — taking separately compiled crates and object files and combining them into a final binary. Since WebAssembly targets were first introduced in Rust, the --allow-undefined flag has always been passed to wasm-ld. Its documented behavior is:

 --allow-undefined Allow undefined symbols in linked binary. This options
 is equivalent to --import-undefined and
 --unresolved-symbols=ignore-all

"Undefined" here refers to symbol resolution within wasm-ld itself. Symbols in wasm-ld work similarly to those on native platforms — every Rust function has an associated symbol, and external symbols can be declared via extern "C" blocks, for example:

unsafe extern "C" {
 fn mylibrary_init();
}

fn init() {
 unsafe {
 mylibrary_init();
 }
}

Here, mylibrary_init is an undefined symbol — one normally provided by an external component such as a compiled C library. With --allow-undefined in effect, rather than raising a linker error, wasm-ld silently converts that unresolved symbol into a WebAssembly import:

(module
 (import "env" "mylibrary_init" (func $mylibrary_init))

 ;; ...
)

The unresolved symbol isn't flagged as an error — instead, it quietly becomes an import in the final WebAssembly module. The precise history of why this flag was introduced is somewhat murky, but it appears to have been a practical necessity in the very early days of wasm-ld support in Rust. That workaround has remained in place ever since.

What's wrong with --allow-undefined?

Passing --allow-undefined across all WebAssembly targets creates a meaningful behavioral divergence from every other platform Rust supports. On native targets, undefined symbols are a hard linker error by default. On WebAssembly, they've been silently swept under the rug — and that silence creates real problems.

When misconfiguration or mistakes produce broken WebAssembly modules instead of build errors, the failure surfaces far from its source. Some concrete examples:

  • If mylibrary_init is mistyped as mylibraryinit, the final binary imports the misspelled symbol rather than calling the correct C symbol — with no error at build time.

  • If mylibrary was accidentally omitted from the build, mylibrary_init becomes an import instead of triggering a linker error indicating an unresolved symbol.

  • External tools like wasm-bindgen or wasm-tools component new don't know how to handle "env" imports by default, and any errors they emit won't clearly trace back to the original undefined symbol in source code.

  • Web developers may have encountered errors like Uncaught TypeError: Failed to resolve module specifier "env". Relative references must start with either "/", "./", or "../". — a confusing message that's actually downstream of an unexpected "env" import caused by an unresolved symbol that should have been caught at link time.

Removing --allow-undefined brings WebAssembly targets in line with every other platform Rust supports: undefined symbols are an error, caught early, with a clear message pointing to the actual problem.

What is going to break, and how to fix it?

In practice, most projects should be unaffected. If a WebAssembly binary currently imports unexpected symbols, it almost certainly won't run in its target environment anyway — most runtimes reject modules with unresolved imports. For example, a wasm32-wasip1 binary that imports mylibrary_init will fail at runtime in most WASI runtimes. Removing --allow-undefined simply surfaces that failure earlier, at link time, with a more actionable error message.

That said, some projects may be intentionally relying on this behavior. For instance, your application might declare an external symbol like this:

unsafe extern "C" {
 fn js_log(n: u32);
}

// ...

And then perhaps some JS code that looks like:

let instance = await WebAssembly.instantiate(module, {
 env: {
 js_log: n => console.log(n),
 }
});

Some codebases explicitly depend on --allow-undefined generating an import in the final WebAssembly binary — and that's a legitimate pattern. If your project relies on this behavior, the recommended fix is to add a #[link] attribute that explicitly declares the wasm_import_module name:

#[link(wasm_import_module = "env")]
unsafe extern "C" {
 fn js_log(n: u32);
}

// ...

This preserves the original behavior, prevents wasm-ld from treating the symbol as undefined, and works correctly both before and after this change. As a quick workaround, you can also pass -Clink-arg=--allow-undefined at compile time to restore the previous behavior without modifying your source.

When is this change being made?

The removal of --allow-undefined on wasm targets is tracked in rust-lang/rust#149868. The patch is expected to land in nightly shortly, with a stable release shipping as part of Rust 1.96 on 2026-05-28. If you run into breakage as a result, please file an issue on rust-lang/rust.

Comments

Sign in to comment.
No comments yet. Be the first to comment.

Related Articles

Changes to WebAssembly targets and handling undefined sym...