Repository navigation
Conversation
When the guest shuts down only the sending side of a connection, it sends VSOCK_OP_SHUTDOWN with VSOCK_FLAGS_SHUTDOWN_SEND. The connection recorded it as PeerClosed(false, true) but left the host-side stream open for writing, so the host peer never read EOF. A host application that waits for the end of the guest's data before it answers, or before it closes, waited for data that would not come, until the guest closed the receiving side too. Shut down the write half of the host stream once the peer has promised to send no more data and the TX buffer has drained, both when the indication arrives and when a later EPOLLOUT flushes the buffer. The host peer then reads EOF as it would from a TCP half-close, and can keep sending: data from the host still reaches the guest in PeerClosed(false, true), as before. A failed shutdown kills the connection, as a failed write does. VsockConnectionBackend gains a shutdown_write method for this, which UnixStream implements with shutdown(Shutdown::Write). The vmm thread's seccomp filter allows shutdown with SHUT_WR only. Signed-off-by: Søren Bramer Schmidt <[email protected]>
When the host-side stream returned EOF, the connection sent the guest VSOCK_OP_SHUTDOWN with both VSOCK_FLAGS_SHUTDOWN_RCV and VSOCK_FLAGS_SHUTDOWN_SEND, and armed the kill timer. A 0-length read only means the host will send no more data, as after shutdown(SHUT_WR). The guest kernel, told that the host would receive nothing either, reset the connection, and data the guest still had to send was lost: an HTTP server behind vsock could not answer a client that half-closed after its request. Tell the two apart by epoll's hang-up. A Unix stream socket reports EPOLLHUP once both of its directions are shut, which a peer's close does and a peer's shutdown(SHUT_WR) does not. On EOF with a hang-up, or after the guest already stopped sending, nothing changes: the guest is told that neither end will send, and the kill timer is armed. On EOF without one, the guest is told only that the host will send no more data (VSOCK_FLAGS_SHUTDOWN_SEND). The connection stops polling the host stream for input, keeps forwarding the guest's data, and is terminated once the guest stops sending too. It stays registered with epoll for a hang-up meanwhile, so a host that then closes for good, or had closed while the last read was pending, terminates it at once. Signed-off-by: Søren Bramer Schmidt <[email protected]>
sorenbs
force-pushed
the
vsock-half-close
branch
from
October 28, 2026 05:30
fc44142 to
49764a7
Compare
When the guest half-closed first and the host second, the read-EOF branch replaced the guest's recorded shutdown with the host's, so the write shutdown of the host stream never ran, and the connection could be reset while the TX buffer still held data for the host. The host then read a clean EOF short of what the guest sent. Four call sites each decided when the connection was done, from booleans beside ConnState that allowed combinations that cannot occur. Replace them with one step, advance_shutdown(), called after every event that can change the shutdown state. Once the peer has promised to send no more data and the TX buffer has drained, it shuts down the write half of the host stream. Once neither end will send any more data, it schedules an RST after the TX buffer has drained, and arms the kill timer while it drains. A StreamEnd enum records how far the host stream has ended: open, hung up, or half-closed. A new unit test covers a host half-close that follows the guest's while data is still buffered for the host, and a muxer test covers both half-closes in that order over a real Unix socket. Signed-off-by: Søren Bramer Schmidt <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Changes
Three commits: one per direction, and one that ends a connection from a single step. Each builds on the one before it.
fix(vsock): pass a guest's send shutdown to the host streamVSOCK_OP_SHUTDOWNwith onlyVSOCK_FLAGS_SHUTDOWN_SEND, the connection now shuts down the write half of the host-side stream, once its TX buffer has drained. That happens both when the shutdown packet arrives and when a laterEPOLLOUTflushes the buffer. The host reads EOF, as it would from a TCP half-close, and can still send to the guest.VsockConnectionBackendgainsshutdown_write, whichUnixStreamimplements withshutdown(Shutdown::Write).vmmthread's seccomp filter allowsshutdownwithSHUT_WRonly, on x86_64 and aarch64.fix(vsock): pass a host half-close to the guest as a half-closeVSOCK_FLAGS_SHUTDOWN_SEND, instead of both flags.EPOLLHUP, or after the guest has already stopped sending, behaves as before: both flags, and the kill timer. A Unix stream socket reportsEPOLLHUPonce both of its directions are shut. A peer'sclosedoes that; a peer'sshutdown(SHUT_WR)does not.EPOLLHUP, which the kernel reports whatever the interest set. A host that closes for good after its half-close, or that closed while the last read was pending, kills the connection at once.fix(vsock): end a connection from one step once neither end sendsadvance_shutdown(), now decides after every event that can change the shutdown state. It replaces four call sites that each decided when the connection was done.StreamEndenum, with the values open, hung up, and half-closed, replaces two booleans besideConnStatethat allowed combinations that cannot occur.Reason
Today a half-close is lost in one direction and turned into a reset in the other.
PeerClosed(false, true)and goes no further. The host never reads EOF until the guest closes the receiving side too. A host application that waits for the end of the guest's request before it answers or closes, such as an HTTP/1.0 server or a proxy that forwards the half-close, waits for data that never comes.shutdown(SHUT_WR)is passed to the guest as a shutdown of both directions. The Linux guest driver then resets the connection whenever it holds no unread data. Data the guest was still about to send is lost. An HTTP server in a guest cannot answer a client that half-closed after its request, if a host proxy forwards that half-close.We found this running a TCP proxy on the host. The proxy carries each guest's outbound TCP connections over vsock, and other guests' inbound ones. Without the first fix, a guest workload's closed connection never reached the remote end, so the proxy had to guess when to end it. Without the second, forwarding a client's half-close to a guest server reset the server's connection before it answered. Measured between two microVMs on one host, with both fixes:
Our "before" build is not stock Firecracker, so its numbers do not reproduce against plain Firecracker. It worked around the first bug: the guest-side agent waited until the host had sent nothing for 5 seconds, then closed both directions. That workaround capped the closing client at the proxy's slots, 496 of 5,000 connections, and lost the answer that came 7 seconds later. With only the first fix and without the workaround, the second bug showed: the half-closing client got no answer at all, even an immediate one.
No timer on a half-closed connection. A half-closed connection stays open as long as both ends keep their sockets, as a TCP connection does after
shutdown(SHUT_WR). Each end can still end it: the guest by closing, which sends both flags, and the host by closing, which raisesEPOLLHUP. A timer would cut connections that legitimately stay half-open for a long time, such as a client that half-closes after its request while the server streams a long answer. How long to wait for the other side after a half-close is the application's policy, so this change adds none.Tests:
UnixStream: one per direction, a host close for good after a half-close, and both half-closes, guest first. They depend on the kernel reportingEPOLLHUPfor a close and not for a write shutdown.test_local_closeandtest_muxer_killq, which drop the host stream, still see both flags and the kill timer.All vsock unit tests pass with
--test-threads=1, on each commit. In parallel,test_vsock_basic_metricsandtest_muxer_killqfail onmainwithout this change too, because they assert global metric counters.Checks. I had no Docker host, so I ran the commands
devtool checkbuild --allanddevtool checkstylerun, with the pinned toolchain (Rust 1.97.0) and the pinned Python tool versions, on each commit:cargo clippy --profile ci --target x86_64-unknown-linux-gnu --target aarch64-unknown-linux-gnu --all --all-targets --all-features -- -D warningscargo fmt --all -- --checkandcargo-sort --workspace --check --groupedgitlintwithframework/gitlint_rules.py, andmdformat --checkon every Markdown fileseccompiler-binon both edited filter filesThe licence, Python, and swagger style checks cover files this change does not touch.
cargo-sortreportssrc/vmm/Cargo.tomlas unsorted onmaintoo; no commit here touches it.Known limitation, not changed here. Once both ends have closed while the device still holds data for the host, the kill timer ends the connection after
CONN_SHUTDOWN_TIMEOUT_MSand drops what the device holds, as it does onmain. A host that reads slowly can therefore miss up to the device's buffer, and its read still ends in a clean EOF. Removing the timer alone would not help: a Linux guest that closes its socket resets the connection after its own 8 s close timeout, and the device then drops the same bytes. We reproduced both on a KVM guest. A fix would acknowledge the guest's close at once and deliver the held bytes without a timer, which seems better as a separate change.License Acceptance
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. For more information on following Developer Certificate of Origin and signing off your commits, please check
CONTRIBUTING.md.PR Checklist
tools/devtool checkbuild --allto verify that the PR passes build checks on all supported architectures. (Its command, outside the container; see Checks.)tools/devtool checkstyle --no-clippyto verify that the PR passes the automated style checks. (Its commands, outside the container; see Checks.)CHANGELOG.md.TODO. (No newTODOs.)rust-vmm.🤖 Generated with Claude Code