virtio-devices: vsock: support half-closed host unix sockets
When the host-side unix socket peer does shutdown(SHUT_WR), read() returns 0 on the stream. Previously this was treated as a full connection closure, sending VSOCK_OP_SHUTDOWN with both SHUTDOWN_RCV and SHUTDOWN_SEND flags and arming the kill timer. This prevented the guest from sending any further data back through the still-writable socket. Change LocalClosed to carry a bool indicating whether receiving from the guest is also shut down. On stream EOF, only set SHUTDOWN_SEND (host won't send anymore) and transition to LocalClosed(false), keeping the connection alive for guest-to-host data flow. The connection fully closes when the guest also sends SHUTDOWN with SHUTDOWN_SEND, or sends RST. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2f77d61b52
commit
c5682801d6
2 changed files with 66 additions and 25 deletions
|
|
@ -215,15 +215,12 @@ where
|
|||
match self.stream.read(&mut buf[..max_len]) {
|
||||
Ok(read_cnt) => {
|
||||
if read_cnt == 0 {
|
||||
// A 0-length read means the host stream was closed down. In that case,
|
||||
// we'll ask our peer to shut down the connection. We can neither send nor
|
||||
// receive any more data.
|
||||
self.state = ConnState::LocalClosed;
|
||||
self.expiry = Some(
|
||||
Instant::now() + Duration::from_millis(defs::CONN_SHUTDOWN_TIMEOUT_MS),
|
||||
);
|
||||
// A 0-length read means the host stream's read side was closed (e.g.
|
||||
// the host peer did shutdown(SHUT_WR)). We can no longer send data to
|
||||
// the guest, but the guest may still send data to us (to be written
|
||||
// to the host stream).
|
||||
self.state = ConnState::LocalClosed(false);
|
||||
pkt.set_op(uapi::VSOCK_OP_SHUTDOWN)
|
||||
.set_flag(uapi::VSOCK_FLAGS_SHUTDOWN_RCV)
|
||||
.set_flag(uapi::VSOCK_FLAGS_SHUTDOWN_SEND);
|
||||
} else {
|
||||
// On a successful data read, we fill in the packet with the RW op, and
|
||||
|
|
@ -288,7 +285,9 @@ where
|
|||
// Most frequent case: this is an established connection that needs to forward some
|
||||
// data to the host stream. Also works for a connection that has begun shutting
|
||||
// down, but the peer still has some data to send.
|
||||
ConnState::Established | ConnState::PeerClosed(_, false)
|
||||
ConnState::Established
|
||||
| ConnState::PeerClosed(_, false)
|
||||
| ConnState::LocalClosed(false)
|
||||
if pkt.op() == uapi::VSOCK_OP_RW =>
|
||||
{
|
||||
if pkt.buf().is_none() {
|
||||
|
|
@ -359,6 +358,26 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
// The peer wants to shut down while the local stream's read side is already closed.
|
||||
// We only care about the send indication: if the peer will no longer send data, both
|
||||
// directions are now closed and we can schedule termination.
|
||||
ConnState::LocalClosed(ref mut recv_off)
|
||||
if pkt.op() == uapi::VSOCK_OP_SHUTDOWN =>
|
||||
{
|
||||
let send_off = pkt.flags() & uapi::VSOCK_FLAGS_SHUTDOWN_SEND != 0;
|
||||
if send_off && !*recv_off {
|
||||
*recv_off = true;
|
||||
if self.tx_buf.is_empty() {
|
||||
self.pending_rx.insert(PendingRx::Rst);
|
||||
} else {
|
||||
self.expiry = Some(
|
||||
Instant::now()
|
||||
+ Duration::from_millis(defs::CONN_SHUTDOWN_TIMEOUT_MS),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A credit update from our peer is valid only in a state which allows data
|
||||
// transfer towards the peer.
|
||||
ConnState::Established | ConnState::PeerInit | ConnState::PeerClosed(false, _)
|
||||
|
|
@ -369,7 +388,10 @@ where
|
|||
|
||||
// A credit request from our peer is valid only in a state which allows data
|
||||
// transfer from the peer. We'll respond with a credit update packet.
|
||||
ConnState::Established | ConnState::PeerInit | ConnState::PeerClosed(_, false)
|
||||
ConnState::Established
|
||||
| ConnState::PeerInit
|
||||
| ConnState::PeerClosed(_, false)
|
||||
| ConnState::LocalClosed(false)
|
||||
if pkt.op() == uapi::VSOCK_OP_CREDIT_REQUEST =>
|
||||
{
|
||||
self.pending_rx.insert(PendingRx::CreditUpdate);
|
||||
|
|
@ -424,7 +446,7 @@ where
|
|||
// We're generally interested in being notified when data can be read from the host
|
||||
// stream, unless we're in a state which doesn't allow moving data from host to guest.
|
||||
match self.state {
|
||||
ConnState::Killed | ConnState::LocalClosed | ConnState::PeerClosed(true, _) => (),
|
||||
ConnState::Killed | ConnState::LocalClosed(_) | ConnState::PeerClosed(true, _) => (),
|
||||
_ if self.need_credit_update_from_peer() => (),
|
||||
_ => evset.insert(epoll::Events::EPOLLIN),
|
||||
}
|
||||
|
|
@ -468,7 +490,10 @@ where
|
|||
|
||||
// If this connection was shutting down, but is waiting to drain the TX buffer
|
||||
// before forceful termination, the wait might be over.
|
||||
if self.state == ConnState::PeerClosed(true, true) && self.tx_buf.is_empty() {
|
||||
if (self.state == ConnState::PeerClosed(true, true)
|
||||
|| self.state == ConnState::LocalClosed(true))
|
||||
&& self.tx_buf.is_empty()
|
||||
{
|
||||
self.pending_rx.insert(PendingRx::Rst);
|
||||
} else if self.peer_needs_credit_update() {
|
||||
// If we've freed up some more buffer space, we may need to let the peer know it
|
||||
|
|
@ -973,7 +998,7 @@ mod unit_tests {
|
|||
}
|
||||
|
||||
// A recv attempt in an invalid state should yield an instant reset packet.
|
||||
ctx.conn.state = ConnState::LocalClosed;
|
||||
ctx.conn.state = ConnState::LocalClosed(true);
|
||||
ctx.notify_epollin();
|
||||
ctx.recv();
|
||||
assert_eq!(ctx.pkt.op(), uapi::VSOCK_OP_RST);
|
||||
|
|
@ -981,25 +1006,39 @@ mod unit_tests {
|
|||
|
||||
#[test]
|
||||
fn test_local_close() {
|
||||
// When the host stream's read side is closed (e.g. peer did shutdown(SHUT_WR)),
|
||||
// we should only indicate SHUTDOWN_SEND (we won't send anymore), not SHUTDOWN_RCV,
|
||||
// since the guest can still send data to us.
|
||||
let mut ctx = CsmTestContext::new_established();
|
||||
let mut stream = TestStream::new();
|
||||
stream.read_state = StreamState::Closed;
|
||||
ctx.set_stream(stream);
|
||||
ctx.notify_epollin();
|
||||
ctx.recv();
|
||||
// When the host-side stream is closed, we can neither send not receive any more data.
|
||||
// Therefore, the vsock shutdown packet that we'll deliver to the guest must contain both
|
||||
// the no-more-send and the no-more-recv indications.
|
||||
assert_eq!(ctx.pkt.op(), uapi::VSOCK_OP_SHUTDOWN);
|
||||
assert_ne!(ctx.pkt.flags() & uapi::VSOCK_FLAGS_SHUTDOWN_SEND, 0);
|
||||
assert_ne!(ctx.pkt.flags() & uapi::VSOCK_FLAGS_SHUTDOWN_RCV, 0);
|
||||
assert_eq!(ctx.pkt.flags() & uapi::VSOCK_FLAGS_SHUTDOWN_RCV, 0);
|
||||
|
||||
// The kill timer should now be armed.
|
||||
assert!(ctx.conn.will_expire());
|
||||
assert!(
|
||||
ctx.conn.expiry().unwrap()
|
||||
< Instant::now() + Duration::from_millis(defs::CONN_SHUTDOWN_TIMEOUT_MS)
|
||||
);
|
||||
// The connection should be in half-closed state, not fully closed.
|
||||
assert_eq!(ctx.conn.state, ConnState::LocalClosed(false));
|
||||
|
||||
// No kill timer should be set for half-close.
|
||||
assert!(!ctx.conn.will_expire());
|
||||
|
||||
// Guest should still be able to send data to us.
|
||||
let data = &[1, 2, 3, 4];
|
||||
ctx.init_data_pkt(data);
|
||||
ctx.send();
|
||||
assert_eq!(ctx.conn.stream.write_buf, data.to_vec());
|
||||
|
||||
// When the guest also shuts down sending, the connection should be fully closed.
|
||||
ctx.init_pkt(uapi::VSOCK_OP_SHUTDOWN, 0)
|
||||
.set_flags(uapi::VSOCK_FLAGS_SHUTDOWN_SEND);
|
||||
ctx.send();
|
||||
assert_eq!(ctx.conn.state, ConnState::LocalClosed(true));
|
||||
assert!(ctx.conn.has_pending_rx());
|
||||
ctx.recv();
|
||||
assert_eq!(ctx.pkt.op(), uapi::VSOCK_OP_RST);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -52,8 +52,10 @@ pub enum ConnState {
|
|||
PeerInit,
|
||||
/// The connection handshake has been performed successfully, and data can now be exchanged.
|
||||
Established,
|
||||
/// The host (AF_UNIX) socket was closed.
|
||||
LocalClosed,
|
||||
/// The host (AF_UNIX) socket's read side was closed (e.g. the host peer did
|
||||
/// shutdown(SHUT_WR)). The bool indicates whether receiving from the guest is also shut down
|
||||
/// (i.e. the connection is fully closed from the local perspective).
|
||||
LocalClosed(bool),
|
||||
/// A VSOCK_OP_SHUTDOWN packet was received from the guest. The tuple represents the guest R/W
|
||||
/// indication: (will_not_recv_anymore_data, will_not_send_anymore_data).
|
||||
PeerClosed(bool, bool),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue