Bug
TcpStream::read_bytes and TcpStream::peek collapse cancellation into an empty Vec<u8>:
// crates/astrid-capsule/src/engine/wasm/host/net/tcp_stream.rs
fn read_bytes(...) -> Result<Vec<u8>, ErrorCode> {
let result = util::bounded_block_on_cancellable(&rt, &sem, &tok, async {...});
match result {
...
None => Ok(Vec::new()), // ← cancellation → empty vec
}
}
fn peek(...) -> Result<Vec<u8>, ErrorCode> {
let result = util::bounded_block_on_cancellable(...);
result.unwrap_or(Ok(Vec::new())) // ← cancellation → empty vec
}
An empty Vec<u8> is the conventional "clean EOF" signal in byte-stream reads (matches std::io::Read::read / tokio::io::AsyncReadExt::read). The current behaviour collapses two distinct events into the same signal:
| Event |
Should mean |
Current return |
| Peer closed cleanly, no more bytes |
EOF |
Ok(Vec::new()) ✓ |
| Capsule being torn down (cancel) |
"this read didn't complete" |
Ok(Vec::new()) ✗ (looks like EOF) |
The framed-read path (read_frame) and the write path (write_bytes, shutdown) already return Err(ErrorCode::Closed) on cancellation. read_bytes / peek are the outliers.
Impact
Capsules with EOF-triggered finalizers — write trailers, send last-message IPC, flush log, transition to "stream complete" state — will execute those finalizers under a forced unload when the cancel fires mid-read. The bug surface is narrow (the capsule has milliseconds left to live) but it's a real correctness hole.
Reported by Gemini on PR #758 (CHANGELOG.md:48 thread).
Fix
Map None → Err(ErrorCode::Closed) in both read_bytes and peek. Empty Vec<u8> keeps its "clean EOF" meaning (peer closed gracefully, no bytes remaining); cancellation now surfaces as a distinct error.
Out of scope
Bug
TcpStream::read_bytesandTcpStream::peekcollapse cancellation into an emptyVec<u8>:An empty
Vec<u8>is the conventional "clean EOF" signal in byte-stream reads (matchesstd::io::Read::read/tokio::io::AsyncReadExt::read). The current behaviour collapses two distinct events into the same signal:Ok(Vec::new())✓Ok(Vec::new())✗ (looks like EOF)The framed-read path (
read_frame) and the write path (write_bytes,shutdown) already returnErr(ErrorCode::Closed)on cancellation.read_bytes/peekare the outliers.Impact
Capsules with EOF-triggered finalizers — write trailers, send last-message IPC, flush log, transition to "stream complete" state — will execute those finalizers under a forced unload when the cancel fires mid-read. The bug surface is narrow (the capsule has milliseconds left to live) but it's a real correctness hole.
Reported by Gemini on PR #758 (
CHANGELOG.md:48thread).Fix
Map
None→Err(ErrorCode::Closed)in bothread_bytesandpeek. EmptyVec<u8>keeps its "clean EOF" meaning (peer closed gracefully, no bytes remaining); cancellation now surfaces as a distinct error.Out of scope
[Unreleased]from feat(kernel)!: migrate to per-domain WIT host ABI #752 + feat(net): outbound TCP host fns (full std::net::TcpStream parity) #746 accumulation) is bundled with this fix as a tidy-up. The release PR (chore: release 0.7.0 #758) will roll the cleaner structure into[0.7.0]after rebase.