wasi-http p3: forwarding a partly read host body silently drops buffered bytes
メンテナーはふだん 1 日以内に返信
評価
調査の方向性
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 の本文から書いたものです。
説明
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_intodiscards 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(andbody.rs:72-91): the
short-cut is used whenevertry_intosucceeds, and it also discards the
guest's trailers future.
Reproduction
Running the reproducer. Pass a
wasmtimeCLI built from the commit
under test (found on b229cb936b) as the script asks (WASMTIME, or an
argument). SetCARGO_TARGET_DIRto share builds. Guests need stable
wasm32-wasip2and/or nightlywasm32-wasip3.
This finding's guests use the exact WIT of the Wasmtime under test: also
setWASMTIME_SRCto that Wasmtime checkout (link-wit.shlinks 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 corewasmtime'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.
-
The core
try_intoclosure still dropsbufferon 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" -
The producer still leaves data in that buffer, and the short-cut still
closes the guest's trailers future unread. Onmainthe short-cut is in
Body::new_guest(p3/body.rs), whichresponse.newandrequest.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 }; -
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()))) -
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) -
Reproducer:
run.sh,guest/andlink-wit.shin this directory,
unchanged. The Observed output above is the audit's (coordinator re-run
on a debug CLI built fromb229cb936b; the lane saw the same on release).
Verified vs. believed
- Verified in this triage (code reading at the commits above): the
buffer-droppingtry_intoclosure and the trailers-closing short-cut are
onmain, 48, 49 and 50. The closure is byte-identical between the
audit's commit andmain. - 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-lengthon
Body::Hostresponses), 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 corewasmtime(try_intorefusing whenbufferis
non-empty, or handing the buffer over) or only inwasi-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
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートあり
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
bytecodealliance/wasmtime のほかの issue
-
bug wasm-proposal:exceptions wasmtime:debugging
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
bytecodealliance/wasmtime#14102 ·
メンテナーはふだん 1 日以内に返信
-
wasm-proposal:gc
難易度 2/5 1〜3時間 初心者へのやさしさ 62/100
bytecodealliance/wasmtime#13808 · コメント 2 件 ·
メンテナーはふだん 1 日以内に返信
-
bug
難易度 4/5 3〜5日 初心者へのやさしさ 24/100
bytecodealliance/wasmtime#15036 ·
メンテナーはふだん 1 日以内に返信
-
bug wasi-http
難易度 3/5 1〜2日 初心者へのやさしさ 32/100
bytecodealliance/wasmtime#15026 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
Compile-time builtin composition resolves guest imports from host working directory & has path failures対応中かも @dyslexicsteak が 1 日前に担当しました。 オープンbug
難易度 5/5 1週間以上 初心者へのやさしさ 15/100
bytecodealliance/wasmtime#15024 ·
メンテナーはふだん 1 日以内に返信
bytecodealliance/wasmtime の issue をすべて見る
似ている issue
-
desktop documentation
難易度 1/5 1時間未満 初心者へのやさしさ 90/100
vfarcic/dot-agent-deck#2008 ·
メンテナーはふだん 1 日以内に返信
-
needs triage
難易度 1/5 1時間未満 初心者へのやさしさ 85/100
pbakaus/impeccable#1210 ·
メンテナーはふだん 1 日以内に返信
-
area: ci priority: P4 ready-for-agent
難易度 2/5 1〜3時間 初心者へのやさしさ 74/100
shep-pm/shep#1249 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
L: javascript
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
dependabot/dependabot-core#16744 ·
メンテナーはふだん 1 日以内に返信
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
nautechsystems/nautilus_trader#5486 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信