Skip to content

Block on pending writes in close - #845

Merged
alexcrichton merged 2 commits into
WebAssembly:mainfrom
alexcrichton:close-blocks-on-writes
Jul 21, 2026
Merged

alexcrichton merged 2 commits into
WebAssembly:mainfrom
alexcrichton:close-blocks-on-writes

Conversation

@alexcrichton

Copy link
Copy Markdown
Collaborator

This commit updates the close function to block on pending socket writes if they are present. This was already done for UDP sends but I forgot to do this for TCP/files/etc. This is effectively the decision reached in WebAssembly/component-model#617.

This is a bit of a curious case as well because the blocking behavior in close is only needed when writes are buffered by wasi-libc, and writes are only buffered when a stream is deduced as not respecting 0-length operations for readiness. The test case here is written with TCP, and TCP is the poster-child for "should respect 0-length operation", so the entire premise of this commit may be a bit suspect.

The underlying cause, however, is how Tokio and certain interleavings of poll work together. Tokio, Wasmtime's runtime for TCP, will report a TCP stream are ready for writes with no syscalls performed until some operation actually hits EWOULDBLOCK. This means that performing a zero-length operation is a noop until something else actually blocks. Normally this is fine, but it means that wasi-libc can deduce that Wasmtime's TCP streams are misbehaving via:

  • A socket is filled such that one more call to write will block, but otherwise hasn't blocked yet.
  • A call to poll is made which does a zero-length write. Tokio's never seen EWOULDBLOCK so it says that the stream is immediately ready.
  • The next call to write thinks that the previous zero-length write should force readiness, but now Tokio discoveres for the first time that the socket isn't ready, blocking the write. This confuses wasi-libc into thinking that writes must be buffered now.

This can probably be fixed with enhanced logic in wasi-libc, but it's also orthogonal to the actual fix here. The actual fix is needed no matter what, blocking on reported-successful writes. A future optimization in wasi-libc might mean that it doesn't so aggressively start buffering writes, but that's left for later. For now the test included here exercises this path, and while it might stop in the future that's hopefully not a huge issue.

This commit updates the `close` function to block on pending socket
writes if they are present. This was already done for UDP `send`s but I
forgot to do this for TCP/files/etc. This is effectively the decision
reached in WebAssembly/component-model#617.

This is a bit of a curious case as well because the blocking behavior in
`close` is only needed when writes are buffered by wasi-libc, and writes
are only buffered when a stream is deduced as not respecting 0-length
operations for readiness. The test case here is written with TCP, and
TCP is the poster-child for "should respect 0-length operation", so the
entire premise of this commit may be a bit suspect.

The underlying cause, however, is how Tokio and certain interleavings of
`poll` work together. Tokio, Wasmtime's runtime for TCP, will report a
TCP stream are ready for writes with no syscalls performed until some
operation actually hits `EWOULDBLOCK`. This means that performing a
zero-length operation is a noop until something else actually blocks.
Normally this is fine, but it means that wasi-libc can deduce that
Wasmtime's TCP streams are misbehaving via:

* A socket is filled such that one more call to `write` will block, but
  otherwise hasn't blocked yet.
* A call to `poll` is made which does a zero-length write. Tokio's never
  seen `EWOULDBLOCK` so it says that the stream is immediately ready.
* The next call to `write` thinks that the previous zero-length write
  should force readiness, but now Tokio discoveres for the first time
  that the socket isn't ready, blocking the write. This confuses
  wasi-libc into thinking that writes must be buffered now.

This can probably be fixed with enhanced logic in wasi-libc, but it's
also orthogonal to the actual fix here. The actual fix is needed no
matter what, blocking on reported-successful writes. A future
optimization in wasi-libc might mean that it doesn't so aggressively
start buffering writes, but that's left for later. For now the test
included here exercises this path, and while it might stop in the future
that's hopefully not a huge issue.
@alexcrichton
alexcrichton requested a review from dicej July 21, 2026 20:10
@alexcrichton
alexcrichton merged commit 7862a87 into WebAssembly:main Jul 21, 2026
35 checks passed
@alexcrichton
alexcrichton deleted the close-blocks-on-writes branch July 21, 2026 21:38
@dicej

dicej commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

I wonder (as someone who is continuously confronted with my own ignorance of POSIX despite using it for decades) how this overlaps with SO_LINGER.

@alexcrichton

Copy link
Copy Markdown
Collaborator Author

Unfortunately I don't think that has much intersection with this, this is a libc-buffered write which was reported successful followed by a close, and if we were to drop the data on the floor that already breaks tests in Python and would probably break real-world programs as well

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants