Apr 10, 2026·7 min read·3 visits
A use-after-free in Wasmtime 43.0.0 occurs when cloning a `Linker` due to shallow copying in the `StringPool`. Dropping the original `Linker` invalidates the clone's pointers, causing crashes in the host application.
Wasmtime version 43.0.0 contains a use-after-free vulnerability in the `StringPool` component of the embedding API. The flaw emerges from an unsound implementation of the `TryClone` trait, leading to dangling pointers when a `Linker` is cloned and the original instance is dropped. The vulnerability causes segmentation faults in the host process and requires specific API interactions to trigger.
The vulnerability exists within the Wasmtime embedding API, specifically affecting the wasmtime::Linker component. The Linker utilizes an internal StringPool to intern module and export names. This interning mechanism optimizes memory usage by storing unique strings once and issuing handles to reference them throughout the execution lifecycle.
The flaw is classified as a Use-After-Free (CWE-416) condition. The vulnerability was introduced in Wasmtime version 43.0.0 during a codebase refactoring effort. The refactoring aimed to implement more robust error handling for Out-Of-Memory (OOM) conditions by replacing infallible allocations with fallible ones.
The scope of this vulnerability is narrowly constrained. It does not affect users of the Wasmtime command-line interface (CLI) or guest WebAssembly programs. The issue only manifests in host applications written in Rust that explicitly interface with the Wasmtime embedding API and invoke specific lifecycle methods on the Linker object.
The primary consequence of triggering this vulnerability is a segmentation fault in the host process. Memory corruption occurs when the application attempts to resolve an interned string using an invalid memory address. The host operating system terminates the process abruptly upon detecting the invalid access.
The underlying technical flaw resides in the implementation of the TryClone trait for the StringPool struct within the wasmtime-environ crate. The TryClone trait serves as a fallible counterpart to standard cloning, allowing the system to handle allocation failures gracefully. The implementation provided for StringPool was structurally unsound.
The unsound implementation executed a shallow copy of the internal map structure. The internal map utilizes &'static str keys to reference interned strings. These string slices point directly into a contiguous heap-allocated memory buffer explicitly owned and managed by the original StringPool instance.
Executing the shallow clone duplicated the map and its &'static str keys without duplicating the underlying heap buffer. The newly instantiated StringPool map contained keys referencing memory owned entirely by the original instance. This design violates memory isolation principles between the two discrete objects.
When the host application drops the original Linker, the Rust drop semantics automatically deallocate the original StringPool and its associated memory buffer. The cloned Linker retains the map with pointers referencing the deallocated memory block. Subsequent operations requiring name resolution via the cloned Linker access these dangling pointers, resulting in a use-after-free condition.
The vulnerable code path existed in crates/environ/src/string_pool.rs. The try_clone function incorrectly duplicated the hash map containing references without properly duplicating the underlying string data. This resulted in two distinct structures sharing an undocumented memory dependency.
The remediation, introduced in PR #12906 and merged via commit 96dde3aa67a5c456e4091ed60a9e3e774f0efd85, completely rewrites the try_clone logic for StringPool. Instead of shallow copying the map, the patched implementation creates an empty StringPool and systematically iterates through the original pool's contents.
// Patched implementation of TryClone for StringPool
impl TryClone for StringPool {
fn try_clone(&self) -> Result<Self, OutOfMemory> {
let mut new_pool = StringPool::new();
// Re-intern strings in index order so that each Atom value is
// identical in the clone. Re-interning ensures the cloned map's
// keys point into the clone's own `strings`.
for s in self.strings.iter() {
new_pool.insert(s)?;
}
Ok(new_pool)
}
}The new implementation explicitly re-interns each string by calling insert(s). This operation allocates new memory within the cloned pool's localized buffer and generates updated pointers. Iterating in sequential order ensures that the integer handle values (Atoms) assigned to each string remain identical across both the original and cloned pools, preserving internal consistency while eliminating the shared memory dependency.
Exploitation of this vulnerability requires precise interaction with the Wasmtime host API. A guest WebAssembly module lacks the required capabilities to trigger the flaw, as it cannot invoke the clone() or drop() methods on the host's Linker object. The host application code must execute the exact sequence of lifecycle methods required to produce the dangling pointers.
The regression test provided by the Wasmtime maintainers demonstrates the exact exploit path. The application must first instantiate a Linker and define host variables or functions within it. The application then creates a clone of the Linker and immediately drops the original instance from memory.
// Proof of Concept Regression Test
#[test]
fn linker_clone_drop_original_then_instantiate() -> Result<()> {
let engine = Engine::default();
let original = {
let mut l: Linker<()> = Linker::new(&engine);
l.func_wrap("env", "answer", || -> i32 { 42 })?;
l
};
let clone = original.clone();
drop(original); // Dropping original invalidates clone's string pool
let mut store = Store::new(&engine, ());
let instance = clone.instantiate(&mut store, &module)?; // UAF/Segfault triggered here
Ok(())
}Invoking clone.instantiate(&mut store, &module) forces the Wasmtime engine to evaluate the interned strings required for module resolution. The StringPool attempts to read the name data from the deallocated buffer. The host operating system intercepts the invalid memory access and terminates the execution with a segmentation fault.
The vulnerability carries a CVSS 4.0 score of 1.0, reflecting a low severity impact. The low score is justified by the stringent prerequisites required for exploitation. The attack surface is entirely contained within the host application's explicit API usage patterns rather than exposed to untrusted external input or guest code execution.
The primary security consequence is a localized denial-of-service affecting the host application. The use-after-free generates a segmentation fault, terminating the process immediately. The vulnerability does not provide an attacker with a generic capability to execute arbitrary code or establish a persistent presence on the host machine.
While the advisory notes that data leakage and heap corruption are unproven, highly constrained environments using specialized allocators could theoretically allocate attacker-controlled data into the freed buffer before the clone is used. Achieving this precise timing and memory layout control requires circumstances typically unavailable in production implementations.
The direct remediation strategy requires upgrading the Wasmtime dependency to version 43.0.1. This release contains the requisite fix implementing the deep-copy and re-interning logic within the StringPool component. Embedders utilizing the Rust API must recompile their host applications against the updated dependency.
For environments where immediate dependency upgrades are unfeasible, a programmatic workaround exists. Developers must audit their codebases to ensure that .clone() is never called on instances of wasmtime::Linker. Removing the clone operation entirely circumvents the vulnerable code path.
Developers requiring clone-like functionality can implement a manual duplication routine. This involves creating a completely new Linker instance attached to the same Wasmtime engine, iterating over the items in the original Linker, and explicitly defining them in the new instance.
// Safe workaround for cloning a Linker
fn clone_linker<T>(linker: &Linker<T>, store: &mut Store<T>) -> Result<Linker<T>> {
let mut cloned = Linker::new(linker.engine());
for (module, name, item) in linker.iter(store) {
cloned.define(module, name, item)?;
}
Ok(cloned)
}This workaround leverages the public API to reconstruct the linker state safely. By relying on Linker::define(), the host application ensures that strings are correctly interned into a newly initialized StringPool, bypassing the flawed TryClone implementation.
CVSS:4.0/AV:P/AC:H/AT:P/PR:H/UI:A/VC:L/VI:L/VA:L/SC:N/SI:N/SA:N| Product | Affected Versions | Fixed Version |
|---|---|---|
Wasmtime Bytecode Alliance | = 43.0.0 | 43.0.1 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-416 |
| Attack Vector | Physical/Local API usage |
| CVSS Score | 1.0 (Low) |
| Impact | Denial of Service (Process Crash) |
| Exploit Status | PoC available (regression test) |
| KEV Status | Not Listed |
Referencing memory after it has been freed can cause a program to crash, use unexpected values, or execute code.