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]) {
|
match self.stream.read(&mut buf[..max_len]) {
|
||||||
Ok(read_cnt) => {
|
Ok(read_cnt) => {
|
||||||
if read_cnt == 0 {
|
if read_cnt == 0 {
|
||||||
// A 0-length read means the host stream was closed down. In that case,
|
// A 0-length read means the host stream's read side was closed (e.g.
|
||||||
// we'll ask our peer to shut down the connection. We can neither send nor
|
// the host peer did shutdown(SHUT_WR)). We can no longer send data to
|
||||||
// receive any more data.
|
// the guest, but the guest may still send data to us (to be written
|
||||||
self.state = ConnState::LocalClosed;
|
// to the host stream).
|
||||||
self.expiry = Some(
|
self.state = ConnState::LocalClosed(false);
|
||||||
Instant::now() + Duration::from_millis(defs::CONN_SHUTDOWN_TIMEOUT_MS),
|
|
||||||
);
|
|
||||||
pkt.set_op(uapi::VSOCK_OP_SHUTDOWN)
|
pkt.set_op(uapi::VSOCK_OP_SHUTDOWN)
|
||||||
.set_flag(uapi::VSOCK_FLAGS_SHUTDOWN_RCV)
|
|
||||||
.set_flag(uapi::VSOCK_FLAGS_SHUTDOWN_SEND);
|
.set_flag(uapi::VSOCK_FLAGS_SHUTDOWN_SEND);
|
||||||
} else {
|
} else {
|
||||||
// On a successful data read, we fill in the packet with the RW op, and
|
// 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
|
// 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
|
// data to the host stream. Also works for a connection that has begun shutting
|
||||||
// down, but the peer still has some data to send.
|
// 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.op() == uapi::VSOCK_OP_RW =>
|
||||||
{
|
{
|
||||||
if pkt.buf().is_none() {
|
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
|
// A credit update from our peer is valid only in a state which allows data
|
||||||
// transfer towards the peer.
|
// transfer towards the peer.
|
||||||
ConnState::Established | ConnState::PeerInit | ConnState::PeerClosed(false, _)
|
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
|
// 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.
|
// 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 =>
|
if pkt.op() == uapi::VSOCK_OP_CREDIT_REQUEST =>
|
||||||
{
|
{
|
||||||
self.pending_rx.insert(PendingRx::CreditUpdate);
|
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
|
// 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.
|
// stream, unless we're in a state which doesn't allow moving data from host to guest.
|
||||||
match self.state {
|
match self.state {
|
||||||
ConnState::Killed | ConnState::LocalClosed | ConnState::PeerClosed(true, _) => (),
|
ConnState::Killed | ConnState::LocalClosed(_) | ConnState::PeerClosed(true, _) => (),
|
||||||
_ if self.need_credit_update_from_peer() => (),
|
_ if self.need_credit_update_from_peer() => (),
|
||||||
_ => evset.insert(epoll::Events::EPOLLIN),
|
_ => evset.insert(epoll::Events::EPOLLIN),
|
||||||
}
|
}
|
||||||
|
|
@ -468,7 +490,10 @@ where
|
||||||
|
|
||||||
// If this connection was shutting down, but is waiting to drain the TX buffer
|
// If this connection was shutting down, but is waiting to drain the TX buffer
|
||||||
// before forceful termination, the wait might be over.
|
// 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);
|
self.pending_rx.insert(PendingRx::Rst);
|
||||||
} else if self.peer_needs_credit_update() {
|
} 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
|
// 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.
|
// 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.notify_epollin();
|
||||||
ctx.recv();
|
ctx.recv();
|
||||||
assert_eq!(ctx.pkt.op(), uapi::VSOCK_OP_RST);
|
assert_eq!(ctx.pkt.op(), uapi::VSOCK_OP_RST);
|
||||||
|
|
@ -981,25 +1006,39 @@ mod unit_tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_local_close() {
|
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 ctx = CsmTestContext::new_established();
|
||||||
let mut stream = TestStream::new();
|
let mut stream = TestStream::new();
|
||||||
stream.read_state = StreamState::Closed;
|
stream.read_state = StreamState::Closed;
|
||||||
ctx.set_stream(stream);
|
ctx.set_stream(stream);
|
||||||
ctx.notify_epollin();
|
ctx.notify_epollin();
|
||||||
ctx.recv();
|
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_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_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.
|
// The connection should be in half-closed state, not fully closed.
|
||||||
assert!(ctx.conn.will_expire());
|
assert_eq!(ctx.conn.state, ConnState::LocalClosed(false));
|
||||||
assert!(
|
|
||||||
ctx.conn.expiry().unwrap()
|
// No kill timer should be set for half-close.
|
||||||
< Instant::now() + Duration::from_millis(defs::CONN_SHUTDOWN_TIMEOUT_MS)
|
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]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -52,8 +52,10 @@ pub enum ConnState {
|
||||||
PeerInit,
|
PeerInit,
|
||||||
/// The connection handshake has been performed successfully, and data can now be exchanged.
|
/// The connection handshake has been performed successfully, and data can now be exchanged.
|
||||||
Established,
|
Established,
|
||||||
/// The host (AF_UNIX) socket was closed.
|
/// The host (AF_UNIX) socket's read side was closed (e.g. the host peer did
|
||||||
LocalClosed,
|
/// 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
|
/// 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).
|
/// indication: (will_not_recv_anymore_data, will_not_send_anymore_data).
|
||||||
PeerClosed(bool, bool),
|
PeerClosed(bool, bool),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue