Stream: git-wasmtime

Topic: wasmtime / PR #14743 wasi-http(p3): tie outbound I/O task...


view this post on Zulip Wasmtime GitHub notifications bot (Oct 14 2026 at 16:46):

rvolosatovs opened PR #14743 from rvolosatovs:wasi-http-io-task-ownership to bytecodealliance:main:

The task driving an outbound request's connection was kept alive by three references: the transmission future, the response body and the request body. The request body reference is circular in the default send_request implementation, since the request body is owned by the hyper connection that the task itself drives. A guest could therefore leave connections running after the store was dropped, with no way for the embedder to reclaim them, e.g. by feeding an endless upstream response body into another outbound request.

Drop the request and transmission future references and instead have the store hold a reference to the task until it finishes. Dropping the store now cancels every outbound connection not referenced by something the embedder holds, while a response body handed to the host unchanged keeps its connection alive for as long as the host reads it.

As a consequence, the guest dropping all of its handles no longer cancels the task; it runs until it finishes or the store is dropped.

Refs https://github.com/bytecodealliance/wasmtime/pull/14633

view this post on Zulip Wasmtime GitHub notifications bot (Oct 14 2026 at 16:46):

rvolosatovs requested alexcrichton for a review on PR #14743.

view this post on Zulip Wasmtime GitHub notifications bot (Oct 14 2026 at 16:46):

rvolosatovs requested wasmtime-wasi-reviewers for a review on PR #14743.

view this post on Zulip Wasmtime GitHub notifications bot (Oct 14 2026 at 16:46):

rvolosatovs requested adamrk for a review on PR #14743.

view this post on Zulip Wasmtime GitHub notifications bot (Oct 14 2026 at 16:46):

rvolosatovs requested wasmtime-wasi-reviewers for a review on PR #14743.

view this post on Zulip Wasmtime GitHub notifications bot (Oct 14 2026 at 18:04):

:memo: adamrk submitted PR review:

Much nicer than what I had before!

view this post on Zulip Wasmtime GitHub notifications bot (Oct 14 2026 at 18:04):

:speech_balloon: adamrk created PR review comment:

So I guess one difference here is that the guest can't cancel the io by dropping the response body anymore, right? (The drop just decrements the Arc ref count). Would it make sense to wrap this io in a type that cancels on drop?

view this post on Zulip Wasmtime GitHub notifications bot (Oct 14 2026 at 18:52):

alexcrichton unassigned alexcrichton from PR #14743 wasi-http(p3): tie outbound I/O task lifetime to the store.

view this post on Zulip Wasmtime GitHub notifications bot (Oct 14 2026 at 18:52):

alexcrichton requested adamrk for a review on PR #14743.

view this post on Zulip Wasmtime GitHub notifications bot (Oct 15 2026 at 12:07):

:memo: rvolosatovs submitted PR review.

view this post on Zulip Wasmtime GitHub notifications bot (Oct 15 2026 at 12:07):

:speech_balloon: rvolosatovs created PR review comment:

So I guess one difference here is that the guest can't cancel the io by dropping the response body anymore, right?

Sort of, and I think that's the expected behavior: if a guest calls send/handle and drops the response, IMO it'd still expect the request body to be transmitted, not aborted. The default implementation uses HTTP/1 with hyper and the hyper future will not resolve until all buffered bytes have been flushed. Ultimately, dropping the response body would, in fact, cancel I/O once the flush completes. A server, which does not read request body would keep the I/O pending. I tested with this example:

use http_body_util::channel::Channel;
use hyper::body::Bytes;
use hyper::client::conn::http1;
use std::time::Duration;
use tokio::io::{AsyncWriteExt, copy, sink};
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::oneshot;

#[tokio::main]
async fn main() {
    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
    let addr = listener.local_addr().unwrap();
    let (dropped_tx, dropped_rx) = oneshot::channel();
    let server = tokio::spawn(async move {
        let (mut conn, _) = listener.accept().await.unwrap();
        conn.readable().await.unwrap();
        conn.write_all(b"HTTP/1.1 200 OK\r\n").await.unwrap();
        conn.write_all(b"transfer-encoding: chunked\r\n\r\n")
            .await
            .unwrap();
        dropped_rx.await.unwrap();
        println!("server: response dropped, reading request");
        let n = copy(&mut conn, &mut sink()).await;
        println!("server: connection closed after receiving {n:?} bytes");
    });

    let stream = TcpStream::connect(addr).await.unwrap();
    let io = hyper_util::rt::TokioIo::new(stream);
    let (mut sender, conn) = http1::handshake(io).await.unwrap();
    let conn = tokio::spawn(conn);
    let (mut body_tx, body) = Channel::<Bytes>::new(1);
    let req = hyper::Request::post("/").body(body).unwrap();
    let res = sender.send_request(req).await.unwrap();
    let upload = tokio::spawn(async move {
        for i in 0.. {
            if body_tx.send_data(vec![0xff; 0xffff].into()).await.is_err() {
                println!("client: upload aborted after {i} buffers");
                return;
            }
        }
    });
    tokio::time::sleep(Duration::from_secs(1)).await;
    println!("client: upload stalled: {}", !upload.is_finished());
    drop(res);
    dropped_tx.send(()).unwrap();
    let res = tokio::time::timeout(Duration::from_secs(5), conn).await;
    println!("client: connection resolved: {res:?}");
    upload.await.unwrap();
    server.await.unwrap();
}

Dropping the store would abort the transmit immediately.

IMO embedders that want to tighten this, e.g. ensure that the I/O is done within a timeout would implement a custom send_request, e.g. by potentially wrapping the default_send_request and wrapping the returned future (https://github.com/bytecodealliance/wasmtime/blob/b140c545f5e5523fed8ee42a6166b1fa04d39b31/crates/wasi-http/src/ctx.rs#L259) with a timeout.
In fact, how about we provide such wrapper for send_request? (like #14633) Perhaps we could even provide a trait and with_flush_timeout method, such that embedders could do something like:

fn send_request(req) ... {
   default_send_request(req).with_flush_timeout(Duration::from_secs(10))
}

view this post on Zulip Wasmtime GitHub notifications bot (Oct 15 2026 at 14:48):

:memo: adamrk submitted PR review.

view this post on Zulip Wasmtime GitHub notifications bot (Oct 15 2026 at 14:48):

:speech_balloon: adamrk created PR review comment:

Regarding cancellation by dropping the body: I guess the WASI spec doesn't say anything about dropping cancelling the operation, so we should probably keep it as you have here.

Regarding the wrapper: I think it could make sense, but not sure anyone really needs it for now. The main concern was the DOS potential so I'd say we put this in as-is and then do something like the wrapper if it comes up again.

view this post on Zulip Wasmtime GitHub notifications bot (Oct 15 2026 at 14:48):

:thumbs_up: adamrk submitted PR review.

view this post on Zulip Wasmtime GitHub notifications bot (Oct 15 2026 at 17:31):

rvolosatovs added PR #14743 wasi-http(p3): tie outbound I/O task lifetime to the store to the merge queue.

view this post on Zulip Wasmtime GitHub notifications bot (Oct 15 2026 at 17:57):

:check: rvolosatovs merged PR #14743.

view this post on Zulip Wasmtime GitHub notifications bot (Oct 15 2026 at 17:57):

rvolosatovs removed PR #14743 wasi-http(p3): tie outbound I/O task lifetime to the store from the merge queue.


Last updated: Oct 31 2026 at 23:03 UTC