Hacktoberfest 2026:メンテナが10月に向けて印を付けた、オープンで初心者向けの issue。 Hacktoberfest の issue を見る

wasi-http p3: forwarding a partly read host body silently drops buffered bytes

オープン
#15,020 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

@adamrk がすでに取り組んでいます。

2026年10月27日 から。

  • #15027 @adamrk による — オープン

評価

難易度
4/5
見積もり時間
3〜5日
初心者へのやさしさ
35/100
issue の種類
バグ
明瞭さ
明確に書かれている
活発さ
活発
技術スタック
rust
領域
api, backend

調査の方向性

Read crates/wasmtime/src/runtime/component/concurrent/futures_and_streams.rs around the try_into closure, then inspect crates/wasi-http/src/p3/body.rs and crates/wasi-http/src/p3/response.rs for the body conversion and trailers handling. The report includes a reproducer at untriaged/0146-wasi-http-p3-forwarded-body-drops-buffered-bytes/run.sh; run it to observe the dropped bytes and ignored trailers error. Done means forwarded body bytes and trailers errors are preserved.

索引モデルが issue の本文から書いたものです。

説明

bug wasi-http

wasmtime-wasi-http p3 can silently drop the buffered portion of the body if it reads part of the body as a stream, then forwards the body.

AI Report

p3: forwarding a partly read host body silently drops buffered bytes

  • System: Linux 6.8.0-137-generic x86_64 (Ubuntu)
  • Wasmtime commit: b229cb936b35039bda5a04c9eb8e65d337dfade7
  • Date: 2026-10-12
  • Model: Claude Opus 5.5
  • Lane: C (p3 streams). The coordinator re-ran the reproducer and wrote this
    report.

Summary

A WASIp3 guest gets a body stream backed by the host
(HostBodyStreamProducer) from request.consume-body or
response.consume-body. If it reads part of the stream (or does a
zero-length read, the documented readiness probe) and then passes the same
stream to response.new or request.new, the host converts the stream
back into the underlying host body with StreamReader::try_into
(p3/body.rs:72-91, p3/response.rs:441-460). The try_into closure in
crates/wasmtime/src/runtime/component/concurrent/futures_and_streams.rs: 2790-2799 takes (producer, buffer) and drops buffer. The
HostBodyStreamProducer keeps leftover frame data in that buffer after a
short read (dst.set_buffer(...), p3/body.rs:534-545), and a whole frame
after a zero-length read (:567-580). Those bytes disappear. The response
is sent with the remaining bytes, a self-consistent framing, status 200, and
the guest's transmit future resolves Ok(()).

In the same short-cut, the guest's own trailers future is closed unread
(trailers.close(...), response.rs:445). A guest that forwards a body but
reports an error through its trailers future (which per WIT must abort the
response) instead produces a clean, complete 200.

Affected surfaces

p3 (response.new via wasmtime serve, and request.new with
client.send, which has the same root cause by reading but wasn't run). p2
doesn't have the short-cut.

Attacker and embedding

No attacker is needed for the data loss. Any p3 proxy-style guest that
peeks at a body before forwarding it corrupts the data. An adversarial guest
can use the trailers path to hide errors. Default wasmtime serve.

Root cause

  • crates/wasmtime/src/runtime/component/concurrent/futures_and_streams.rs: 2790-2799: try_into discards the producer's pending buffer.
  • crates/wasi-http/src/p3/body.rs:534-545, :567-580: the producer
    legitimately leaves data in that buffer.
  • crates/wasi-http/src/p3/response.rs:441-460 (and body.rs:72-91): the
    short-cut is used whenever try_into succeeds, and it also discards the
    guest's trailers future.

Reproduction

Running the reproducer. Pass a wasmtime CLI built from the commit
under test (found on b229cb936b) as the script asks (WASMTIME, or an
argument). Set CARGO_TARGET_DIR to share builds. Guests need stable
wasm32-wasip2 and/or nightly wasm32-wasip3.
This finding's guests use the exact WIT of the Wasmtime under test: also
set WASMTIME_SRC to that Wasmtime checkout (link-wit.sh links it in).

cd untriaged/0146-wasi-http-p3-forwarded-body-drops-buffered-bytes
WASMTIME=<wasmtime> bash run.sh

It POSTs 200,000 bytes to a guest that echoes the request body as its
response body, with one of four modes: forward untouched, read 1 byte first
(and write it out separately), do a zero-length read first, or forward with a
trailers future that resolves to an error.

Observed output

Coordinator re-run on 2026-10-12, debug CLI. The lane observed the same on a
release CLI.

mode=none  expected=200000 got=200000 lost=0 OK
mode=peek1 expected=199999 got=191975 lost=8024 BUG: bytes silently dropped
mode=zero  expected=200000 got=191974 lost=8026 BUG: bytes silently dropped
mode=trailers-err status=200 got=200000 curl_exit=0
guest: trailers error delivered to host = false
guest: transmit result = Ok(())

Impact

Silent corruption of proxied HTTP bodies (truncation in the middle, with
consistent framing, so neither side notices), and the error-signalling
contract of the trailers future is bypassed. This is an integrity bug in
default, Tier 1 p3 wasi-http. No memory-safety impact. Severity: medium.

Suggested fix

In the try_into closure, only convert when the buffer is empty; otherwise
put (mine, buffer) back and return None so the generic path is used.
Or pass the buffer along so the host body can be prefixed with it. In the
wasi-http short-cut, don't drop the guest's trailers future: if the guest
supplied one, fall back to the generic path, or chain it after the host
body's trailers.

Request direction: confirmed by lane C2 (see 0154, "Other
results"). After a 1-byte peek the upstream got 191984 of 199999 bytes,
and the guest's trailers future is ignored there too.

Duplicate check

Not in the triage repository. The CM-async audit's notes examined
StreamReader::try_into only for lock contention and unwrap (round 3 and
round 4 host-api.md, streams-futures.md), not for the dropped buffer.
0002 involves the same splice path, but it's a resource leak. No upstream
fix.

Triage

Result: normal-bug. When a WASIp3 guest forwards a host body stream it
has partly read (or probed with a zero-length read), the host silently drops
the bytes buffered in the stream's producer, about 8 KiB, and ignores the
guest's trailers future. The result is corrupted proxied traffic with
consistent framing and an Ok result. That's an integrity bug for the
guest's own traffic, not a host-safety issue.

  • Triaged by Pat Hickey (agent-assisted, Claude Opus 5.5), 2026-10-13.
  • Decision: normal-bug. This was the reviewer's call: Pat Hickey chose
    the outcome, matching the pre-triage assessment's recommendation
    (background/wasi-http-audit/assessment-2026-10-13.md, Till
    Schneidereit: "normal-bug (medium functional) ... Silent corruption of
    proxied bodies. The victim is the guest's own traffic"). Pat Hickey gave
    no further reasons in this session. The reasoning below is the
    assessment's and this triage's.
Reasoning
  • Who can trigger it: the guest, by its own actions: peek-then-forward,
    or forwarding with a trailers future that reports an error. No attacker
    is needed for the data loss, and a benign proxy guest hits it. A client
    only supplies the body.
  • Release-build behaviour: release and debug alike (the lane observed a
    release CLI, the coordinator a debug CLI). Bytes are lost with no error,
    and there's no panic, no memory-safety impact, and no effect on other
    guests or the host.
  • Why not a vulnerability: the guest loses its own data, or chooses to
    hide its own error. It gains no capability or resource it didn't have,
    and nothing crosses a sandbox boundary. The trailers-error bypass lets a
    guest send a "clean" response it could have sent anyway by not reporting
    the error.
  • Feature tiers: wasi-http (Tier 1) with WASIp3
    (component-model-async/p3, on by default in the 48, 49 and 50 CLIs;
    the p3 module docs still say "experimental"). Part of the root cause is
    in core wasmtime's component-model-async streams.
  • Affected releases: 48.x, 49.x, 50.0.0-rc.1 and main (code
    reading). 36.x has no p3.
Evidence

Wasmtime checkout ~/src/wasmtime, git fetch origin on 2026-10-13,
origin/main = a59a82daf2e389e04badf436fc30d928b60241a4.

  1. The core try_into closure still drops buffer on success, and is
    unchanged since the audit:

    $ git show "a59a82daf2:crates/wasmtime/src/runtime/component/concurrent/futures_and_streams.rs" | grep -n -A9 'let try_into = Box::new'
    2796:        let try_into = Box::new(move |ty| {
    2797:            let (mine, buffer) = producer.try_lock().ok()?.take()?;
    2798:            match P::try_into(mine, ty) {
    2799:                Ok(value) => Some(value),
    2800:                Err(mine) => {
    2801:                    *producer.try_lock().ok()? = Some((mine, buffer));
    2802:                    None
    $ diff <(b229cb936b's closure) <(a59a82daf2's closure)
    (no differences) -> "try_into closure identical b229cb936b vs main"
    
  2. The producer still leaves data in that buffer, and the short-cut still
    closes the guest's trailers future unread. On main the short-cut is in
    Body::new_guest (p3/body.rs), which response.new and request.new
    both use:

    $ git show "a59a82daf2:crates/wasi-http/src/p3/body.rs" | grep -n 'set_buffer'
    569:                                        dst.set_buffer(frame.split_off(cap));
    580:                                    dst.set_buffer(frame);
    $ git show "a59a82daf2:crates/wasi-http/src/p3/body.rs" | sed -n 72,80p
            let contents = match contents
                .map(|rx| rx.try_into::<HostBodyStreamProducer>(store.as_context_mut()))
            {
                Some(Ok(mut producer)) => {
                    trailers.close(store.as_context_mut())?;
                    ...
                    let body = Body::Host { body: mem::take(&mut producer.body), result_tx };
    
  3. Release branches have the same code:

    $ for b in release-48.0.0 release-49.0.0 release-50.0.0; do git show origin/$b:.../futures_and_streams.rs | grep -n 'let (mine, buffer) = producer.try_lock().ok()?.take()?;'; git show origin/$b:crates/wasi-http/src/p3/body.rs | grep -n 'try_into::<HostBodyStreamProducer'; done
    release-48.0.0: 2767 (closure)   63 (.map(|rx| rx.try_into::<HostBodyStreamProducer<T>>(&mut *store)))
    release-49.0.0: 2767             63 (same)
    release-50.0.0: 2767             67 (.map(|rx| rx.try_into::<HostBodyStreamProducer>(store.as_context_mut())))
    
  4. Upstream commits since the audit touch the concurrent runtime, but none
    changes this closure (point 1) or the short-cut:

    $ git log --oneline b229cb936b..a59a82daf2 -- crates/wasmtime/src/runtime/component/concurrent crates/wasi-http/src/p3
    2a72ae78d7 Sprinkle some FIXMEs for memory64 support (#14685)
    ae9c64deab Fix cancelling a future/stream while cancellation is in progress (#14665)
    781d7f0207 component: remove fiber ownership unit tests (#14681)
    958bc1b673 Don't deliver spurious DROPPED events to futures (#14676)
    ee1a097390 use reference counting for guest tasks (#14548)
    030d07cb1b Document some hazards of `Val` and `{Resource,Stream,Future}Any` (#14675)
    c0ca85571c Fix a guest write to a host future being cancelled (#14672)
    3aa015666a Deliver some more spec-mandated async traps (#14668)
    
  5. Reproducer: run.sh, guest/ and link-wit.sh in this directory,
    unchanged. The Observed output above is the audit's (coordinator re-run
    on a debug CLI built from b229cb936b; the lane saw the same on release).

Verified vs. believed
  • Verified in this triage (code reading at the commits above): the
    buffer-dropping try_into closure and the trailers-closing short-cut are
    on main, 48, 49 and 50. The closure is byte-identical between the
    audit's commit and main.
  • Not re-run in this triage: run.sh. The 8,024- and 8,026-byte losses
    and the ignored trailers error are the audit's observations on
    b229cb936b. The request direction (client.send) is the audit's
    lane C2 run, reported under 0154.
  • Believed, not checked: that none of the eight upstream commits in
    point 4 changes the behaviour indirectly (e.g. by never leaving data in
    the buffer). Only the closure and the short-cut were compared.
Related and open
  • Same short-cut: 0143 (needs-cve: unchecked content-length on
    Body::Host responses), 0149 (forwarded trailers bypass
    is_forbidden_header), 0153 (transmit future reports Ok after a forwarded
    body failed). The assessment suggests one change closes 0146, 0149 and
    0153: don't take the short-cut when the producer has buffered data or the
    guest supplied a trailers future. That change touches the same lines as
    0143's fix, so whoever fixes 0143 in the private advisory fork should
    coordinate with this bug's public fix.
  • 0154: has the request-direction confirmation of this bug.
  • 0002 / GHSA-wrhh-79vh-ccpv: same splice path, a different bug
    (resource leak).
  • Open: whether the trailers-error bypass should be split out (it's an
    error-reporting contract violation, not data loss). Whether the fix
    belongs in core wasmtime (try_into refusing when buffer is
    non-empty, or handing the buffer over) or only in wasi-http.
  • No upstream issue or PR filed yet. Upstream issues are filed by humans.
Fix drafts

None. Suggested fix: see Suggested fix above.

主要言語
Rust
スター
18.8k
フォーク
1.9k
平均マージ
1日 2分
マージ済み PR(30日)
392

環境構築

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

bytecodealliance/wasmtime のほかの issue

bytecodealliance/wasmtime の issue をすべて見る

似ている issue

Rust の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。