Stream: wasi

Topic: Trouble virtualizing wasi interfaces


view this post on Zulip Scott Andrews (Oct 20 2026 at 18:10):

I'm working on a suite of components that virtualize WASI interfaces (wrapping each interface to trace calls, apply access controls, etc.). I've hit a limitation in the 0.3 async model that I don't think wrappers can work around.

The concrete case is wasi:http/types request.new, a sync function that accepts trailers: future<result<option<trailers>, error-code>>. The virtualizer's exported trailers is a distinct resource type from the imported one, so that future can't be forwarded to the upstream request.new. Something has to await it, unwrap the resource, and write a new future. But a sync-lifted export has no task that outlives the call. Once it returns, the component can't run until it's called again. Blocking inside new would deadlock, because the caller only writes trailers after new returns.

wit-bindgen 0.62 papered over this: spawn_local from a sync export queued the work, and it ran during whatever async export came next. 0.63 now reasonably asserts there's an active task, which surfaced the problem: https://github.com/bytecodealliance/wit-bindgen/issues/1754. Even before that, my approach was inherently fragile.

The pattern isn't specific to http. Any sync function that takes or returns a future/stream whose payload contains a resource has this problem (request/response.new, consume-body, and I believe wasi:sockets tcp-socket.listen). Streams of plain data can be passed through, but can't be observed without a task either (e.g. wasi:cli write-via-stream).

Colorless composition was an early goal that has narrowed. It leaves sync functions with no way to start work that continues after they return. Some directions I can see, none of which I'm confident in:

  1. Allow sync-typed functions to be lifted with the async ABI, keep running after task.return, and trap only if they block before returning.

  2. Let cooperative threads outlive the sync task that created them.

  3. Require any WIT function that takes or returns a future/stream to be async. This works, but it pushes the cost onto every caller.

Is there an intended pattern for this, or is the ability to virtualize any interface not a goal the current design can guarantee? I know just enough here to be dangerous, and deeply appreciate the effort put into the design to date.

view this post on Zulip Joel Dice (Oct 20 2026 at 18:57):

I believe a variation of this question came up in a different thread; I'll see if I can find it.

Meanwhile, have you considered spawning a thread to read from or write to a future or stream? (EDIT: yes, I see you did; sorry for skimming too fast the first time!) At the moment, that requires some obscure incantations for most guest toolchains, but it's technically possible now for C/C++, Rust, and Python now and will "just work" as support is stabilized over the next month or two. If the guest language is JS, I'm not sure what the options are or will be aside from composing with a language that supports multithreading.

view this post on Zulip Joel Dice (Oct 20 2026 at 18:59):

BTW, I believe that lifting sync-typed functions with the async ABI is currently prohibited by the Component Model.

view this post on Zulip Joel Dice (Oct 20 2026 at 19:03):

Here's the thread (no pun intended) I was thinking of: #wasi > Creating a `stream` in a sync context

view this post on Zulip Scott Andrews (Oct 20 2026 at 19:11):

My current hacky support for calling async components from a sync context. It doesn’t work for the http request.new case. https://github.com/componentized/filesystem/blob/main/crates/componentized-rt/src/lib.rs

view this post on Zulip Joel Dice (Oct 20 2026 at 19:43):

Once Rust 1.100 ships in a few weeks, you'll be able to use std::thread::spawn (and it should work on nightly now).

view this post on Zulip Alex Crichton (Oct 20 2026 at 20:05):

This does sound like a poster-child style use case for threads if that can work, and there's no restriction that threads must live within the context of their task -- I think it'd work for this use case. As a caveat though std::thread::spawn won't work in Rust 1.100 out-the-box, you'll need the mysterious incantations

view this post on Zulip Scott Andrews (Oct 20 2026 at 20:24):

Is wasm32-wasip3 required? I really want to avoid the import barf that entails.

view this post on Zulip Alex Crichton (Oct 20 2026 at 20:25):

std::thread::spawn will only be supported on wasip3 yeah, no other target will support threading in rust

view this post on Zulip Scott Andrews (Oct 20 2026 at 20:35):

an aside: I find it odd that a platform with a zero capability by default model would default to enabling every capability on the host for the premier build target.

view this post on Zulip Scott Andrews (Oct 20 2026 at 20:37):

All my components are currently built with wasm32-unknown-unknown and wasm-tools component new in order to get only the imports the component actually needs.

I saw wasm32-none-component landed yesterday in tier 3, but that's a bit bare bones.

view this post on Zulip Scott Andrews (Oct 20 2026 at 23:06):

I tried wasm32-wasip3 with rust 1.100 (beta), wasi-sdk 34 and wasmtime main@28e3807. The wasi:http request.new is able to spawn a new thread and returns, however, the next call into the component triggers a memory fault:

Error: error while executing at wasm backtrace:
    0:  0x27a9f - trace.wasm!dlfree
    1:  0x27795 - trace.wasm!free
    2:   0x6c19 - trace.wasm!<wit_bindgen[1dc4b884a9bc26]::rt::Cleanup as core[8a7f440ef15db33b]::ops::drop::Drop>::drop
    3:   0x6bf5 - trace.wasm!core[8a7f440ef15db33b]::ptr::drop_glue::<wit_bindgen[1dc4b884a9bc26]::rt::async_support::subtask::InProgress<trace[537ff8db433888f6]::wasi::http::client::send::{closure#0}::_MySubtask>>
    4:   0x7706 - trace.wasm!<wit_bindgen[1dc4b884a9bc26]::rt::async_support::waitable::WaitableOperation<wit_bindgen[1dc4b884a9bc26]::rt::async_support::subtask::SubtaskOps<trace[537ff8db433888f6]::wasi::http::client::send::{closure#0}::_MySubtask>>>::poll_complete_with_code
    5:   0xa8d6 - trace.wasm!trace[537ff8db433888f6]::exports::wasi::http::client::_export_send_cabi::<trace[537ff8db433888f6]::Trace>::{closure#0}
    6:   0x8493 - trace.wasm!<wit_bindgen[1dc4b884a9bc26]::rt::async_support::TaskState>::callback
    7:   0x419a - trace.wasm!wit_bindgen[1dc4b884a9bc26]::rt::async_support::callback
    8:   0x4429 - trace.wasm![callback][async-lift]wasi:http/client@0.3.0#send
    9:  0x3ee8a - <unknown>!<wasm function 112>

Caused by:
    0: memory fault at wasm address 0x80105ca8 in linear memory of size 0x410000
    1: wasm trap: out of bounds memory access

full backtrace at https://pastebin.com/vqbi2tw5

view this post on Zulip Alex Crichton (Oct 21 2026 at 14:54):

That looks like memory corruption in the module itself, and for debugging that I'd recommend using the dlmalloc crate with the debug feature turne on. That may help narrow it down to the point of corruption. One common cause with threads is stack overflow in the case that there's a big stack buffer or perhaps the stack is just too small

view this post on Zulip Scott Andrews (Oct 21 2026 at 19:21):

It looks like the root cause of the memory corruption was resolved by https://github.com/WebAssembly/wasi-libc/pull/884. With that fix applied to wasi-sdk and wasm32-wasip3 std::thread::spawn seems to work.

view this post on Zulip Scott Andrews (Oct 21 2026 at 19:28):

Was also able to hack around the additional wasi imports brought in by wasm32-wasip3 through a combination of dead code pruning and a no-op stub component. It's unfortunate that it's necessary to add bloat to a component to remove capabilities that aren't needed or desired.

view this post on Zulip Till Schneidereit (Oct 23 2026 at 10:24):

an aside: I find it odd that a platform with a zero capability by default model would default to enabling every capability on the host for the premier build target.

You're not wrong that it'd be a lot nicer conceptually if we could avoid this. The reality is, though, that we need to meet developers where they are for any of this to be useful. And developers exist in established ecosystems, backed by language standard libraries and package repositories that fundamentally expect these interfaces to exist.

(An aside to my answer to your aside: making interfaces available isn't the same as making capabilities available. E.g., wasi:filesystem being available doesn't mean you have access to any particular file handle, or the ability to gain that access. The actual capabilities system isn't ideal, but also far stronger than "default[ing] to enabling every capability on the host".)

view this post on Zulip Scott Andrews (Oct 23 2026 at 11:27):

My concern is less that their is a way to turn these imports on, and more that their isn’t an obvious way to turn the off. I agree that existing language ecosystems expect these capabilities, because that’s how it’s always been. Components disrupt that status quo.

Of course the host, or another component can limit the capabilities. I’m working on exactly that. It’s a powerful ability with components that two different branches of a composition can have different degrees of access to a capability like wasi:sockets.

That doesn’t preclude my stance that a component which doesn’t need the network, should never have access to the network. Latent capabilities that aren’t used today, have a history of becoming tomorrow’s security vulnerabilities.

view this post on Zulip Till Schneidereit (Oct 23 2026 at 13:51):

a component which doesn’t need the network, should never have access to the network.

Strongly agreed! The particular embedding of Wasmtime I work on (Spin) does exactly that, with deny-all defaults for both fs and network (via both sockets and http). But again, that's not the same as not providing the APIs. We tried that, hard, and found that it just doesn't work in the real world.

As for the obvious way to turn off these capabilities: targeting wasm32-unknown-unknown with your own set of imports using wit-bindgen is the best we can do. I'm also unsure about what else we could do, as long as language standard libraries are relatively monolithic—even lean ones like Rust's.

view this post on Zulip Scott Andrews (Oct 23 2026 at 14:05):

You’re hitting on the crux of this thread. wasm32-unknown-unknown is my current, and preferred, target. The problem is that the cooperative threading support is gated behind wasm32-wasip3, which brings the additional imports. I need the threading support in order to handle translating a resource wrapped in a future in a sync method.

view this post on Zulip Scott Andrews (Oct 23 2026 at 14:07):

It’s possible to obscure these imports, but it requires extra build hoops and makes the component even larger.

view this post on Zulip Scott Andrews (Oct 23 2026 at 14:09):

A phrase I heard recently and like is “absent by default”. It’s stronger than “deny by default” as the capability doesn’t exist, so there no way to bypass the deny policy.

view this post on Zulip Joel Dice (Oct 23 2026 at 16:18):

Seems like #![no_std] is a better fit for "absent by default" than wasm32-unknown-unknown. The former will give you a compile error if you try to use e.g. std::fs::File::open, whereas the latter will happily let that compile and then fail at runtime.

view this post on Zulip Joel Dice (Oct 23 2026 at 16:25):

I think the upcoming "optional imports" feature will be the sweet spot for this kind of thing and make wasm32-unknown-unknown obsolete. That will allow the wasm32-wasi* targets to use e.g. wasi:filesystem if it's present and otherwise gracefully return an error to the caller.

view this post on Zulip Scott Andrews (Oct 23 2026 at 16:35):

I don’t think you can use std::thread::spawn with #![no_std].

view this post on Zulip Joel Dice (Oct 23 2026 at 16:53):

Right, but you can't do it with wasm32-unknown-unknown, either. You can use the component model intrinsics for spawning threads with #![no_std] and --target=wasm32-wasip3, unlike with wasm32-unknown-unknown. For ideal ergonomics, we'll need to wait for optional imports.

view this post on Zulip Scott Andrews (Oct 23 2026 at 16:56):

Do you have an example or doc for how to do this? I’m traveling right now and won’t be able to try it until mid next week.

view this post on Zulip Joel Dice (Oct 23 2026 at 16:58):

No, sorry; just thought I'd chime in with what I know about the relevant current and future options.

view this post on Zulip Joel Dice (Oct 23 2026 at 17:05):

I imagine using #![no_std] and libc::pthread_create and friends would also work for wasm32-wasip3, although I haven't tried that myself.

Another approach would be to not use #![no_std] but override the default panic handler like here, which should help avoid pulling in stdio-related stuff and allow it to be dead-code-eliminated. Again, I haven't tried that myself, but seems promising.

view this post on Zulip Scott Andrews (Oct 23 2026 at 22:26):

I’ll give those a try next week, thanks

view this post on Zulip Scott Andrews (Oct 27 2026 at 16:16):

no_std was able to reduce the imports down to only wasi:clocks/types (for duration) and wasi:cli/exit (used by __wasi_coop_thread_start).

view this post on Zulip Scott Andrews (Oct 27 2026 at 16:29):

defining a custom exit func removes that import.

view this post on Zulip Scott Andrews (Oct 27 2026 at 16:55):

wasi:clocks is a dependency of http, which my commonest needs. The version bumped which made me think it was a new dep. With no_std and overriding the exit method I'm back down to the same imports/exports as wasm32-unknown-unknown. The downside is that I can no longer use the parts of std that would never have triggered an import.

view this post on Zulip Joel Dice (Oct 27 2026 at 16:59):

Are the parts of std that you need available in alloc (e.g. Vec, etc.)?

view this post on Zulip Scott Andrews (Oct 27 2026 at 17:10):

yea, everything I need is available in alloc or core

view this post on Zulip Scott Andrews (Oct 27 2026 at 18:39):

Also found that system-clock and monotonic-clock are pulled in by the threads support via __tl_lock and __tl_unlock, even with no_std. wasm-opt was removing the imports before I noticed.


Last updated: Oct 31 2026 at 23:03 UTC