Hacktoberfest 2026:维护者为十月标记出来的 issue,仍然开放、适合新手。 浏览 Hacktoberfest issue

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

已关闭
#15,020 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

维护者通常 1 天内回复

已经有一个关联 PR 被合并了。

  • #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
平均合并
23 小时 6 分钟
30 天内合并 PR
410

环境准备

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

bytecodealliance/wasmtime 的其他 Issue

查看 bytecodealliance/wasmtime 的全部 Issue

相似的 Issue

更多 Rust Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。