Skip to main content

arcbox_virtio_vsock/
manager.rs

1//! Host-side vsock connection manager for the HV (Hypervisor.framework) backend.
2//!
3//! Implements a connection state machine inspired by vhost-device-vsock's
4//! `VsockConnection`. Each connection tracks:
5//! - A bitmask-based RX priority queue (`RxOps`) for pending host→guest ops
6//! - Credit flow control (`fwd_cnt`, `peer_buf_alloc`, `peer_fwd_cnt`, `rx_cnt`)
7//! - Connection lifecycle (`connect` flag)
8//!
9//! The manager maintains a `backend_rxq` — a FIFO of connections with pending
10//! RX operations. The VMM's `poll_vsock_rx` drains this queue, filling guest
11//! RX descriptors from the highest-priority pending operation per connection.
12
13use std::collections::{HashMap, VecDeque};
14use std::num::Wrapping;
15#[cfg(test)]
16use std::os::unix::io::FromRawFd;
17use std::os::unix::io::{AsRawFd, OwnedFd, RawFd};
18use std::sync::Arc;
19use std::sync::atomic::{AtomicU32, Ordering};
20
21use crate::VsockHostConnections;
22
23/// Wakeup hook invoked when host→guest RX work appears from outside the
24/// injection driver (new connection, handshake completion, credit grants).
25///
26/// The VMM installs a callback that wakes its vsock-io worker so injection
27/// runs immediately instead of waiting for the next natural vCPU exit —
28/// without it, an idle guest adds ~100 ms per host→guest leg.
29pub type VsockDoorbell = Arc<dyn Fn() + Send + Sync>;
30
31// ============================================================================
32// RxOps: Per-connection pending RX operation bitmask
33// ============================================================================
34
35/// Pending RX operations for a single connection, stored as a u8 bitmask.
36///
37/// Dequeued in fixed priority order (lowest bit = highest priority).
38/// Each operation type can only be pending once at a time.
39#[derive(Debug, Clone, Copy, Default)]
40pub struct RxOps(u8);
41
42impl RxOps {
43    // Priority order (lowest bit wins): Request > Rw > Response > CreditUpdate > Reset > CreditRequest
44    pub const REQUEST: u8 = 0x01;
45    pub const RW: u8 = 0x02;
46    pub const RESPONSE: u8 = 0x04;
47    pub const CREDIT_UPDATE: u8 = 0x08;
48    pub const RESET: u8 = 0x10;
49    pub const CREDIT_REQUEST: u8 = 0x20;
50
51    /// Returns true if any operation is pending.
52    pub fn pending(&self) -> bool {
53        self.0 != 0
54    }
55
56    /// Enqueues an operation (sets bit).
57    pub fn enqueue(&mut self, op: u8) {
58        self.0 |= op;
59    }
60
61    /// Dequeues the highest-priority pending operation (clears bit).
62    /// Returns the operation bitmask, or 0 if nothing pending.
63    pub fn dequeue(&mut self) -> u8 {
64        if self.0 == 0 {
65            return 0;
66        }
67        // Lowest set bit = highest priority.
68        let op = self.0 & self.0.wrapping_neg();
69        self.0 &= !op;
70        op
71    }
72
73    /// Peeks at the highest-priority pending operation without removing it.
74    pub fn peek(&self) -> u8 {
75        if self.0 == 0 {
76            return 0;
77        }
78        self.0 & self.0.wrapping_neg()
79    }
80}
81
82// ============================================================================
83// VsockConnectionId
84// ============================================================================
85
86/// Unique identifier for a host↔guest vsock connection.
87///
88/// The vsock protocol identifies connections by the 4-tuple
89/// `(src_cid, src_port, dst_cid, dst_port)`. Since host CID is always 2
90/// and guest CID is always 3, the pair `(host_port, guest_port)` suffices.
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
92pub struct VsockConnectionId {
93    pub host_port: u32,
94    pub guest_port: u32,
95}
96
97// ============================================================================
98// VsockConnection: Per-connection state machine
99// ============================================================================
100
101/// Default host-side TX buffer size (also advertised as `buf_alloc` to guest).
102pub const TX_BUFFER_SIZE: u32 = 64 * 1024;
103
104/// Bytes consumed since the last credit packet before we send a proactive
105/// `CREDIT_UPDATE`.
106///
107/// Chosen as 4 KB (one page) so the guest sees a refreshed fwd_cnt roughly
108/// every page of drained traffic. A coarser threshold (e.g. 3/4 of
109/// `TX_BUFFER_SIZE` = 48 KB) stalls the guest TX path on bursts between
110/// the window size and the threshold: the guest exhausts its view of our
111/// free buffer and blocks waiting for a credit packet we haven't sent yet.
112/// A finer threshold would just spam credit-only packets without reducing
113/// stall probability meaningfully.
114pub const CREDIT_UPDATE_THRESHOLD: u32 = 4096;
115
116/// `OP_SHUTDOWN` flag: peer will not receive any more data.
117pub const VSOCK_SHUTDOWN_F_RECEIVE: u32 = 1 << 0;
118
119/// `OP_SHUTDOWN` flag: peer will not send any more data.
120pub const VSOCK_SHUTDOWN_F_SEND: u32 = 1 << 1;
121
122/// Mask of both shutdown flags — equivalent to `RST` when set.
123pub const VSOCK_SHUTDOWN_F_BOTH: u32 = VSOCK_SHUTDOWN_F_RECEIVE | VSOCK_SHUTDOWN_F_SEND;
124
125/// A single host↔guest vsock connection.
126///
127/// Owns the internal end of the socketpair. When this entry is removed from
128/// the manager (or the manager is dropped), `OwnedFd::drop` closes the fd.
129///
130/// The state machine is implicit:
131/// - `connect == false`: handshake in progress
132/// - `connect == true`: data transfer enabled
133/// - `rx_queue` contains `RxOps::RESET`: connection is being torn down
134pub struct VsockConnection {
135    pub id: VsockConnectionId,
136    pub internal_fd: OwnedFd,
137    /// Fired by vCPU thread's poll_vsock_rx after OP_REQUEST is written to
138    /// guest memory. The daemon blocks on this before returning the fd —
139    /// guarantees the guest will see the OP_REQUEST and respond (RST or
140    /// RESPONSE) so the daemon's read won't hang indefinitely.
141    pub injected_notify: Option<std::sync::mpsc::Sender<()>>,
142    pub guest_cid: u64,
143
144    /// Whether the connection handshake is complete.
145    pub connect: bool,
146
147    /// Per-connection pending RX operations (bitmask priority queue).
148    pub rx_queue: RxOps,
149
150    // -- Credit flow control --
151    /// Total bytes forwarded from host tx_buf to the actual host stream.
152    /// Sent to guest in every packet so it knows how much host buffer is free.
153    pub fwd_cnt: Wrapping<u32>,
154
155    /// `fwd_cnt` value at the time of the last credit update sent to guest.
156    /// Used to decide when a proactive CreditUpdate is warranted.
157    last_fwd_cnt: Wrapping<u32>,
158
159    /// Guest's advertised buffer allocation (extracted from every incoming pkt).
160    pub peer_buf_alloc: u32,
161
162    /// Guest's forwarded count (extracted from every incoming packet).
163    pub peer_fwd_cnt: Wrapping<u32>,
164
165    /// Total bytes sent TO the guest via RX virtqueue.
166    pub rx_cnt: Wrapping<u32>,
167
168    /// Set when a `CREDIT_REQUEST` packet has been enqueued for the peer and
169    /// the peer has not yet answered with a `CREDIT_UPDATE`. Keeps us from
170    /// spamming repeated credit requests each time we see a low-credit RW —
171    /// one in-flight at a time is enough to refresh our view.
172    credit_request_pending: bool,
173
174    /// Set when the peer sent `OP_SHUTDOWN` with `F_RECEIVE` — it won't
175    /// accept any more data. We must stop emitting `RW` for this connection
176    /// but keep the fd open so pending peer→host data can still drain.
177    peer_no_recv: bool,
178}
179
180impl VsockConnection {
181    /// Creates a new connection for a host-initiated connect (OP_REQUEST).
182    pub fn new_local_init(
183        id: VsockConnectionId,
184        guest_cid: u64,
185        fd: OwnedFd,
186        injected_notify: std::sync::mpsc::Sender<()>,
187    ) -> Self {
188        let mut conn = Self {
189            id,
190            internal_fd: fd,
191            guest_cid,
192            connect: false,
193            injected_notify: Some(injected_notify),
194            rx_queue: RxOps::default(),
195            fwd_cnt: Wrapping(0),
196            last_fwd_cnt: Wrapping(0),
197            peer_buf_alloc: 0,
198            peer_fwd_cnt: Wrapping(0),
199            rx_cnt: Wrapping(0),
200            credit_request_pending: false,
201            peer_no_recv: false,
202        };
203        // Enqueue OP_REQUEST to be sent to guest on the next RX fill.
204        conn.rx_queue.enqueue(RxOps::REQUEST);
205        conn
206    }
207
208    /// Returns the number of bytes the guest can still receive.
209    ///
210    /// `peer_buf_alloc - (rx_cnt - peer_fwd_cnt)` = total guest buffer minus
211    /// bytes currently in-flight (sent but not yet consumed by the guest).
212    pub fn peer_avail_credit(&self) -> usize {
213        (Wrapping(self.peer_buf_alloc) - (self.rx_cnt - self.peer_fwd_cnt)).0 as usize
214    }
215
216    /// Updates peer credit state from an incoming guest packet. Also clears
217    /// any in-flight `CREDIT_REQUEST` marker: the peer has just told us the
218    /// fresh state, so whatever we asked about is answered.
219    pub fn update_peer_credit(&mut self, buf_alloc: u32, fwd_cnt: u32) {
220        self.peer_buf_alloc = buf_alloc;
221        self.peer_fwd_cnt = Wrapping(fwd_cnt);
222        self.credit_request_pending = false;
223    }
224
225    /// Enqueues a `CREDIT_REQUEST` op if peer credit has fallen below half
226    /// the peer's advertised buffer and no request is already in flight.
227    ///
228    /// Call from the RX path after sending data the peer now has to process.
229    /// Sending the request proactively — rather than only when credit hits
230    /// zero — means we refresh our (possibly stale) view of `peer_fwd_cnt`
231    /// before we actually deplete our window, avoiding a full TX stall.
232    pub fn maybe_request_credit(&mut self) {
233        if self.credit_request_pending || self.peer_buf_alloc == 0 {
234            return;
235        }
236        let half = (self.peer_buf_alloc / 2) as usize;
237        if self.peer_avail_credit() < half {
238            self.rx_queue.enqueue(RxOps::CREDIT_REQUEST);
239            self.credit_request_pending = true;
240        }
241    }
242
243    /// Marks a `CREDIT_REQUEST` as in-flight without going through the
244    /// `RxOps` queue. Used when the caller emits the request packet directly
245    /// in the RW-with-zero-credit fallback path — we still want the pending
246    /// flag set so `maybe_request_credit` doesn't duplicate us.
247    pub fn note_credit_request_sent(&mut self) {
248        self.credit_request_pending = true;
249    }
250
251    /// True iff we're waiting on the peer for a credit update.
252    #[must_use]
253    pub fn credit_request_pending(&self) -> bool {
254        self.credit_request_pending
255    }
256
257    /// Peer sent `OP_SHUTDOWN` with `F_RECEIVE`. Record the half-close so the
258    /// RX injection path stops trying to deliver more `RW` packets.
259    pub fn mark_peer_no_recv(&mut self) {
260        self.peer_no_recv = true;
261    }
262
263    /// True iff the peer has half-closed its receive side.
264    #[must_use]
265    pub const fn peer_no_recv(&self) -> bool {
266        self.peer_no_recv
267    }
268
269    /// Whether we may send more data to the peer. False once the handshake
270    /// hasn't completed or the peer has told us it won't accept more.
271    #[must_use]
272    pub const fn accepts_data(&self) -> bool {
273        self.connect && !self.peer_no_recv
274    }
275
276    /// Called after data is written to the host stream (from guest OP_RW).
277    /// Advances `fwd_cnt` and enqueues a CreditUpdate if buffer is getting low.
278    pub fn advance_fwd_cnt(&mut self, bytes: u32) {
279        self.fwd_cnt += Wrapping(bytes);
280
281        // Proactive credit update once enough has been drained that the peer's
282        // in-flight window is meaningfully stale.
283        let consumed = (self.fwd_cnt - self.last_fwd_cnt).0;
284        if consumed >= CREDIT_UPDATE_THRESHOLD {
285            self.rx_queue.enqueue(RxOps::CREDIT_UPDATE);
286        }
287    }
288
289    /// Records bytes sent to the guest and returns the new rx_cnt.
290    pub fn record_rx(&mut self, bytes: u32) {
291        self.rx_cnt += Wrapping(bytes);
292    }
293
294    /// Marks that a CreditUpdate was sent to the guest (syncs last_fwd_cnt).
295    pub fn mark_credit_sent(&mut self) {
296        self.last_fwd_cnt = self.fwd_cnt;
297    }
298}
299
300// ============================================================================
301// VsockConnectionManager
302// ============================================================================
303
304/// Manages all active host-initiated vsock connections for the HV backend.
305///
306/// Thread-safe: wrapped in `Arc<Mutex<>>` and shared between the daemon
307/// threads (which call `allocate`) and vCPU threads (which call `poll`
308/// methods via `VsockHostConnections` trait).
309pub struct VsockConnectionManager {
310    connections: HashMap<VsockConnectionId, VsockConnection>,
311    /// FIFO of connection IDs with pending RX operations.
312    /// Consumed by `poll_vsock_rx` → `recv_pkt`.
313    pub backend_rxq: VecDeque<VsockConnectionId>,
314    /// Monotonically increasing counter for ephemeral host port allocation.
315    next_host_port: AtomicU32,
316    /// Rung when RX work appears from producer paths (allocate, handshake
317    /// completion, credit grants). NOT rung from the injection driver's own
318    /// enqueues (`enqueue_rw`/`enqueue_reset` and in-loop re-pushes) — the
319    /// driver is already awake there, and ringing on its re-pushes would
320    /// spin it while the guest catches up.
321    doorbell: Option<VsockDoorbell>,
322}
323
324impl VsockConnectionManager {
325    /// Starting ephemeral port. Each connection gets the next value.
326    const EPHEMERAL_PORT_BASE: u32 = 50_000;
327
328    /// Creates a new empty connection manager.
329    pub fn new() -> Self {
330        Self {
331            connections: HashMap::new(),
332            backend_rxq: VecDeque::new(),
333            next_host_port: AtomicU32::new(Self::EPHEMERAL_PORT_BASE),
334            doorbell: None,
335        }
336    }
337
338    /// Installs the doorbell rung when new host→guest RX work appears.
339    pub fn set_doorbell(&mut self, doorbell: VsockDoorbell) {
340        self.doorbell = Some(doorbell);
341    }
342
343    fn ring_doorbell(&self) {
344        if let Some(doorbell) = &self.doorbell {
345            doorbell();
346        }
347    }
348
349    /// Allocates a new connection to `guest_port`, returning a unique ID.
350    ///
351    /// The `internal_fd` is the internal end of a socketpair; the external
352    /// end was returned to the daemon caller. Ownership of `internal_fd`
353    /// transfers to the manager — it will be closed automatically when the
354    /// connection is removed.
355    ///
356    /// Enqueues `RxOps::REQUEST` and pushes to `backend_rxq` so the next
357    /// `poll_vsock_rx` sends OP_REQUEST to the guest.
358    /// Allocates a new connection to `guest_port`, returning the ID and a
359    /// receiver that signals when the connection is established (OP_RESPONSE)
360    /// or rejected (OP_RST). The daemon should wait on this receiver before
361    /// using the socketpair for data transfer.
362    /// Allocates a new connection. Returns the ID and a receiver that fires
363    /// when the vCPU thread has injected the OP_REQUEST into guest memory.
364    /// The daemon MUST wait on this receiver before using the fd.
365    pub fn allocate(
366        &mut self,
367        guest_port: u32,
368        guest_cid: u64,
369        internal_fd: OwnedFd,
370    ) -> (VsockConnectionId, std::sync::mpsc::Receiver<()>) {
371        let host_port = self.next_host_port.fetch_add(1, Ordering::Relaxed);
372        let id = VsockConnectionId {
373            host_port,
374            guest_port,
375        };
376        let (tx, rx) = std::sync::mpsc::channel();
377        let conn = VsockConnection::new_local_init(id, guest_cid, internal_fd, tx);
378        self.connections.insert(id, conn);
379        // Signal that this connection has a pending RX op (OP_REQUEST).
380        self.backend_rxq.push_back(id);
381        self.ring_doorbell();
382        tracing::info!(
383            "VsockConnectionManager: allocated connection guest_port={} host_port={} — \
384             OP_REQUEST enqueued",
385            guest_port,
386            host_port,
387        );
388        (id, rx)
389    }
390
391    /// Returns a snapshot of all connected (id, raw_fd) pairs for polling.
392    ///
393    /// The caller uses these to `libc::read` from each fd and, if data is
394    /// available, enqueue `RxOps::RW` and push to `backend_rxq`.
395    pub fn connected_fds(&self) -> Vec<(VsockConnectionId, RawFd)> {
396        self.connections
397            .values()
398            .filter(|c| c.connect)
399            .map(|c| (c.id, c.internal_fd.as_raw_fd()))
400            .collect()
401    }
402
403    /// Returns a mutable reference to a connection.
404    pub fn get_mut(&mut self, id: &VsockConnectionId) -> Option<&mut VsockConnection> {
405        self.connections.get_mut(id)
406    }
407
408    /// Returns a reference to a connection.
409    pub fn get(&self, id: &VsockConnectionId) -> Option<&VsockConnection> {
410        self.connections.get(id)
411    }
412
413    /// Enqueues a data-available RX op for a connected stream.
414    pub fn enqueue_rw(&mut self, id: VsockConnectionId) {
415        if let Some(conn) = self.connections.get_mut(&id) {
416            conn.rx_queue.enqueue(RxOps::RW);
417            self.backend_rxq.push_back(id);
418        }
419    }
420
421    /// Enqueues a reset for a connection (e.g., when host stream closes).
422    pub fn enqueue_reset(&mut self, id: VsockConnectionId) {
423        if let Some(conn) = self.connections.get_mut(&id) {
424            conn.rx_queue.enqueue(RxOps::RESET);
425            self.backend_rxq.push_back(id);
426        }
427    }
428
429    /// Removes a connection and closes its fd.
430    pub fn remove(&mut self, id: &VsockConnectionId) {
431        if let Some(mut conn) = self.connections.remove(id) {
432            // Best-effort: notify the receiver (if still alive) that this
433            // connection is being torn down.
434            if let Some(tx) = conn.injected_notify.take() {
435                let _ = tx.send(());
436            }
437            // OwnedFd dropped here, closing the socketpair.
438            // Remove from backend_rxq too.
439            self.backend_rxq.retain(|qid| qid != id);
440            tracing::info!(
441                "VsockConnectionManager: removed connection guest_port={} host_port={} — fd closed",
442                id.guest_port,
443                id.host_port,
444            );
445        }
446    }
447
448    /// Returns IDs of connections that have pending RX ops but are NOT
449    /// already in the `backend_rxq`. Used after TX processing to pick up
450    /// newly-enqueued ops (e.g., CreditUpdate after guest OP_CREDIT_REQUEST).
451    pub fn connections_with_pending_rx(&self) -> Vec<VsockConnectionId> {
452        let in_queue: std::collections::HashSet<_> = self.backend_rxq.iter().copied().collect();
453        self.connections
454            .values()
455            .filter(|c| c.rx_queue.pending() && !in_queue.contains(&c.id))
456            .map(|c| c.id)
457            .collect()
458    }
459
460    /// Returns the number of active connections.
461    #[cfg(test)]
462    pub fn len(&self) -> usize {
463        self.connections.len()
464    }
465
466    /// Returns `true` if there are no active connections.
467    #[cfg(test)]
468    pub fn is_empty(&self) -> bool {
469        self.connections.is_empty()
470    }
471}
472
473impl VsockHostConnections for VsockConnectionManager {
474    fn fd_for(&self, guest_port: u32, host_port: u32) -> Option<RawFd> {
475        let id = VsockConnectionId {
476            host_port,
477            guest_port,
478        };
479        self.connections
480            .get(&id)
481            .filter(|c| c.connect)
482            .map(|c| c.internal_fd.as_raw_fd())
483    }
484
485    fn mark_connected(&mut self, guest_port: u32, host_port: u32) {
486        let id = VsockConnectionId {
487            host_port,
488            guest_port,
489        };
490        if let Some(conn) = self.connections.get_mut(&id) {
491            conn.connect = true;
492            // The daemon may already have written request data into the
493            // socketpair while the handshake was in flight; wake the
494            // injection driver so it starts watching this fd now.
495            self.ring_doorbell();
496            tracing::info!("VsockConnectionManager: connection {:?} now Connected", id,);
497        } else {
498            tracing::warn!(
499                "VsockConnectionManager: mark_connected for unknown connection \
500                 guest_port={} host_port={}",
501                guest_port,
502                host_port,
503            );
504        }
505    }
506
507    fn remove_connection(&mut self, guest_port: u32, host_port: u32) {
508        let id = VsockConnectionId {
509            host_port,
510            guest_port,
511        };
512        self.remove(&id);
513    }
514
515    fn update_peer_credit(
516        &mut self,
517        guest_port: u32,
518        host_port: u32,
519        buf_alloc: u32,
520        fwd_cnt: u32,
521    ) {
522        let id = VsockConnectionId {
523            host_port,
524            guest_port,
525        };
526        if let Some(conn) = self.connections.get_mut(&id) {
527            conn.update_peer_credit(buf_alloc, fwd_cnt);
528        }
529    }
530
531    fn advance_fwd_cnt(&mut self, guest_port: u32, host_port: u32, bytes: u32) -> bool {
532        let id = VsockConnectionId {
533            host_port,
534            guest_port,
535        };
536        if let Some(conn) = self.connections.get_mut(&id) {
537            conn.advance_fwd_cnt(bytes);
538            if conn.rx_queue.pending() {
539                self.backend_rxq.push_back(id);
540                self.ring_doorbell();
541                return true;
542            }
543        }
544        false
545    }
546
547    fn enqueue_credit_update(&mut self, guest_port: u32, host_port: u32) {
548        let id = VsockConnectionId {
549            host_port,
550            guest_port,
551        };
552        if let Some(conn) = self.connections.get_mut(&id) {
553            conn.rx_queue.enqueue(RxOps::CREDIT_UPDATE);
554            self.backend_rxq.push_back(id);
555            self.ring_doorbell();
556        }
557    }
558
559    fn handle_shutdown(&mut self, guest_port: u32, host_port: u32, flags: u32) {
560        // Both bits set (or flags==0, which is spec-invalid but treated as
561        // worst case) → full teardown, matching the default trait impl.
562        if flags == 0 || flags & VSOCK_SHUTDOWN_F_BOTH == VSOCK_SHUTDOWN_F_BOTH {
563            self.remove_connection(guest_port, host_port);
564            return;
565        }
566
567        let id = VsockConnectionId {
568            host_port,
569            guest_port,
570        };
571        if flags & VSOCK_SHUTDOWN_F_RECEIVE != 0 {
572            if let Some(conn) = self.connections.get_mut(&id) {
573                conn.mark_peer_no_recv();
574            }
575        }
576        // `VSOCK_SHUTDOWN_F_SEND`: guest will not send any more data. Propagate
577        // the half-close to the daemon-side fd by shutting down the write side
578        // of the internal socketpair end — the daemon's `read(fds[0])` then
579        // returns EOF. The reverse direction (daemon→guest writes) stays open
580        // so the host can drain any in-flight bytes and finish the session.
581        //
582        // Without this, the daemon's RawFdStream poll_read never observes the
583        // guest's half-close and `copy_bidirectional` stalls forever. This
584        // manifests as `docker run <image>` (foreground attach) hanging after
585        // the container exits: dockerd closes its end of attach, the guest
586        // agent sends OP_SHUTDOWN F_SEND, but the daemon-side bridge never
587        // learns about it and the Docker CLI waits indefinitely for EOF.
588        if flags & VSOCK_SHUTDOWN_F_SEND != 0 {
589            if let Some(conn) = self.connections.get(&id) {
590                let fd = conn.internal_fd.as_raw_fd();
591                // SAFETY: `fd` is borrowed from an `OwnedFd` held by the
592                // connection map; it remains valid for the duration of this
593                // call.
594                let r = unsafe { libc::shutdown(fd, libc::SHUT_WR) };
595                if r != 0 {
596                    let err = std::io::Error::last_os_error();
597                    // ENOTCONN / EINVAL are benign — peer already tore down,
598                    // or the write side was already shut (repeat F_SEND).
599                    // Match the pattern used by the daemon-side shutdown in
600                    // `rpc/arcbox-transport/src/vsock/stream.rs`.
601                    if !matches!(err.raw_os_error(), Some(libc::ENOTCONN | libc::EINVAL)) {
602                        tracing::warn!(
603                            guest_port,
604                            host_port,
605                            "shutdown(internal_fd, SHUT_WR) for F_SEND failed: {}",
606                            err,
607                        );
608                    }
609                }
610            }
611        }
612    }
613}
614
615impl Default for VsockConnectionManager {
616    fn default() -> Self {
617        Self::new()
618    }
619}
620
621#[cfg(test)]
622mod tests {
623    use super::*;
624
625    fn make_socketpair() -> (OwnedFd, OwnedFd) {
626        let mut fds: [libc::c_int; 2] = [0; 2];
627        let ret =
628            unsafe { libc::socketpair(libc::AF_UNIX, libc::SOCK_STREAM, 0, fds.as_mut_ptr()) };
629        assert_eq!(ret, 0);
630        unsafe { (OwnedFd::from_raw_fd(fds[0]), OwnedFd::from_raw_fd(fds[1])) }
631    }
632
633    #[test]
634    fn rx_ops_priority_order() {
635        let mut ops = RxOps::default();
636        ops.enqueue(RxOps::RESET);
637        ops.enqueue(RxOps::REQUEST);
638        ops.enqueue(RxOps::RW);
639        ops.enqueue(RxOps::CREDIT_UPDATE);
640
641        // Dequeue in priority order: Request → Rw → CreditUpdate → Reset
642        assert_eq!(ops.dequeue(), RxOps::REQUEST);
643        assert_eq!(ops.dequeue(), RxOps::RW);
644        assert_eq!(ops.dequeue(), RxOps::CREDIT_UPDATE);
645        assert_eq!(ops.dequeue(), RxOps::RESET);
646        assert_eq!(ops.dequeue(), 0);
647    }
648
649    #[test]
650    fn rx_ops_dedup() {
651        let mut ops = RxOps::default();
652        ops.enqueue(RxOps::RW);
653        ops.enqueue(RxOps::RW);
654        ops.enqueue(RxOps::RW);
655
656        assert_eq!(ops.dequeue(), RxOps::RW);
657        assert_eq!(ops.dequeue(), 0); // Only one dequeue despite 3 enqueues.
658    }
659
660    #[test]
661    fn allocate_unique_host_ports() {
662        let mut mgr = VsockConnectionManager::new();
663        let (_, internal1) = make_socketpair();
664        let (_, internal2) = make_socketpair();
665
666        let (id1, _rx1) = mgr.allocate(1024, 3, internal1);
667        let (id2, _rx2) = mgr.allocate(1024, 3, internal2);
668
669        assert_ne!(id1.host_port, id2.host_port);
670        assert_eq!(id1.guest_port, 1024);
671        assert_eq!(id2.guest_port, 1024);
672        assert_eq!(mgr.len(), 2);
673    }
674
675    #[test]
676    fn allocate_enqueues_request() {
677        let mut mgr = VsockConnectionManager::new();
678        let (_, internal) = make_socketpair();
679        let (id, _rx) = mgr.allocate(1024, 3, internal);
680
681        // Should be in backend_rxq.
682        assert_eq!(mgr.backend_rxq.len(), 1);
683        assert_eq!(mgr.backend_rxq[0], id);
684
685        // Connection should have Request pending.
686        let conn = mgr.get(&id).unwrap();
687        assert_eq!(conn.rx_queue.peek(), RxOps::REQUEST);
688        assert!(!conn.connect);
689    }
690
691    #[test]
692    fn connected_fds_only_returns_connected() {
693        let mut mgr = VsockConnectionManager::new();
694        let (_, internal1) = make_socketpair();
695        let (_, internal2) = make_socketpair();
696
697        let (id1, _rx1) = mgr.allocate(1024, 3, internal1);
698        let (_id2, _rx2) = mgr.allocate(1024, 3, internal2);
699
700        assert!(mgr.connected_fds().is_empty());
701
702        mgr.mark_connected(id1.guest_port, id1.host_port);
703        let fds = mgr.connected_fds();
704        assert_eq!(fds.len(), 1);
705        assert_eq!(fds[0].0, id1);
706    }
707
708    #[test]
709    fn remove_closes_fd() {
710        let mut mgr = VsockConnectionManager::new();
711        let (_, internal) = make_socketpair();
712        let fd_raw = internal.as_raw_fd();
713        let (id, _rx) = mgr.allocate(1024, 3, internal);
714
715        mgr.mark_connected(id.guest_port, id.host_port);
716        assert!(mgr.fd_for(1024, id.host_port).is_some());
717
718        mgr.remove_connection(id.guest_port, id.host_port);
719        assert!(mgr.fd_for(1024, id.host_port).is_none());
720        assert_eq!(mgr.len(), 0);
721
722        // Verify fd is actually closed (write should fail with EBADF).
723        let ret = unsafe { libc::fcntl(fd_raw, libc::F_GETFD) };
724        assert_eq!(ret, -1);
725    }
726
727    #[test]
728    fn credit_flow_control() {
729        let mut mgr = VsockConnectionManager::new();
730        let (_, internal) = make_socketpair();
731        let (id, _rx) = mgr.allocate(1024, 3, internal);
732
733        // Simulate guest advertising 128KB buffer.
734        let conn = mgr.get_mut(&id).unwrap();
735        conn.update_peer_credit(128 * 1024, 0);
736        assert_eq!(conn.peer_avail_credit(), 128 * 1024);
737
738        // After sending 64KB to guest, available credit drops.
739        conn.record_rx(64 * 1024);
740        assert_eq!(conn.peer_avail_credit(), 64 * 1024);
741
742        // Guest forwards 32KB.
743        conn.update_peer_credit(128 * 1024, 32 * 1024);
744        assert_eq!(conn.peer_avail_credit(), 96 * 1024);
745    }
746
747    #[test]
748    fn fwd_cnt_triggers_credit_update() {
749        let mut mgr = VsockConnectionManager::new();
750        let (_, internal) = make_socketpair();
751        let (id, _rx) = mgr.allocate(1024, 3, internal);
752
753        // Drain the initial REQUEST from rx_queue.
754        let conn = mgr.get_mut(&id).unwrap();
755        conn.rx_queue.dequeue();
756
757        // Write past CREDIT_UPDATE_THRESHOLD (4 KB) → should trigger CreditUpdate.
758        conn.advance_fwd_cnt(CREDIT_UPDATE_THRESHOLD);
759        assert_eq!(conn.rx_queue.peek(), RxOps::CREDIT_UPDATE);
760    }
761
762    #[test]
763    fn fwd_cnt_below_threshold_does_not_trigger_credit_update() {
764        let mut mgr = VsockConnectionManager::new();
765        let (_, internal) = make_socketpair();
766        let (id, _rx) = mgr.allocate(1024, 3, internal);
767        let conn = mgr.get_mut(&id).unwrap();
768        conn.rx_queue.dequeue();
769
770        // One byte below threshold must NOT enqueue an update.
771        conn.advance_fwd_cnt(CREDIT_UPDATE_THRESHOLD - 1);
772        assert!(!conn.rx_queue.pending());
773    }
774
775    #[test]
776    fn maybe_request_credit_fires_below_half_window() {
777        let mut mgr = VsockConnectionManager::new();
778        let (_, internal) = make_socketpair();
779        let (id, _rx) = mgr.allocate(1024, 3, internal);
780        let conn = mgr.get_mut(&id).unwrap();
781        conn.rx_queue.dequeue(); // drain REQUEST
782
783        conn.update_peer_credit(8192, 0);
784        conn.record_rx(5000); // avail = 8192 - 5000 = 3192, below half (4096)
785        conn.maybe_request_credit();
786
787        assert_eq!(conn.rx_queue.peek(), RxOps::CREDIT_REQUEST);
788        assert!(conn.credit_request_pending());
789    }
790
791    #[test]
792    fn maybe_request_credit_noop_above_half_window() {
793        let mut mgr = VsockConnectionManager::new();
794        let (_, internal) = make_socketpair();
795        let (id, _rx) = mgr.allocate(1024, 3, internal);
796        let conn = mgr.get_mut(&id).unwrap();
797        conn.rx_queue.dequeue();
798
799        conn.update_peer_credit(8192, 0);
800        conn.record_rx(3000); // avail = 5192, above half (4096)
801        conn.maybe_request_credit();
802
803        assert!(!conn.rx_queue.pending());
804        assert!(!conn.credit_request_pending());
805    }
806
807    #[test]
808    fn maybe_request_credit_dedupes_while_pending() {
809        let mut mgr = VsockConnectionManager::new();
810        let (_, internal) = make_socketpair();
811        let (id, _rx) = mgr.allocate(1024, 3, internal);
812        let conn = mgr.get_mut(&id).unwrap();
813        conn.rx_queue.dequeue();
814
815        conn.update_peer_credit(8192, 0);
816        conn.record_rx(5000);
817        conn.maybe_request_credit();
818        // Dequeue the first request so we can see if a duplicate fires.
819        conn.rx_queue.dequeue();
820
821        conn.record_rx(100); // still below half, still pending
822        conn.maybe_request_credit();
823
824        assert!(!conn.rx_queue.pending(), "second request would be a dup");
825    }
826
827    #[test]
828    fn update_peer_credit_clears_pending_flag() {
829        let mut mgr = VsockConnectionManager::new();
830        let (_, internal) = make_socketpair();
831        let (id, _rx) = mgr.allocate(1024, 3, internal);
832        let conn = mgr.get_mut(&id).unwrap();
833        conn.rx_queue.dequeue();
834
835        conn.update_peer_credit(8192, 0);
836        conn.record_rx(5000);
837        conn.maybe_request_credit();
838        assert!(conn.credit_request_pending());
839
840        // Peer answers with a fresh fwd_cnt; pending should clear. The
841        // already-enqueued CREDIT_REQUEST op stays in rx_queue — sending it
842        // is harmless (peer just replies with another CREDIT_UPDATE) and not
843        // worth a bit-clearing helper on RxOps.
844        conn.update_peer_credit(8192, 5000);
845        assert!(!conn.credit_request_pending());
846
847        // Drain the stale CREDIT_REQUEST to simulate the next RX tick.
848        assert_eq!(conn.rx_queue.dequeue(), RxOps::CREDIT_REQUEST);
849
850        // Now that we're at full credit, maybe_request_credit stays quiet.
851        conn.maybe_request_credit();
852        assert!(!conn.rx_queue.pending());
853        assert!(!conn.credit_request_pending());
854    }
855
856    #[test]
857    fn shutdown_both_bits_removes_connection() {
858        let mut mgr = VsockConnectionManager::new();
859        let (_, internal) = make_socketpair();
860        let (id, _rx) = mgr.allocate(1024, 3, internal);
861        assert!(mgr.get(&id).is_some());
862
863        mgr.handle_shutdown(id.guest_port, id.host_port, VSOCK_SHUTDOWN_F_BOTH);
864        assert!(mgr.get(&id).is_none());
865    }
866
867    #[test]
868    fn shutdown_receive_bit_marks_half_close() {
869        let mut mgr = VsockConnectionManager::new();
870        let (_, internal) = make_socketpair();
871        let (id, _rx) = mgr.allocate(1024, 3, internal);
872
873        mgr.handle_shutdown(id.guest_port, id.host_port, VSOCK_SHUTDOWN_F_RECEIVE);
874        let conn = mgr.get(&id).expect("conn must survive half-close");
875        assert!(conn.peer_no_recv());
876        assert!(!conn.accepts_data() || !conn.connect); // connect=false initially
877    }
878
879    #[test]
880    fn shutdown_send_bit_only_is_informational() {
881        let mut mgr = VsockConnectionManager::new();
882        let (_, internal) = make_socketpair();
883        let (id, _rx) = mgr.allocate(1024, 3, internal);
884
885        mgr.handle_shutdown(id.guest_port, id.host_port, VSOCK_SHUTDOWN_F_SEND);
886        let conn = mgr.get(&id).expect("conn must survive");
887        assert!(
888            !conn.peer_no_recv(),
889            "F_SEND alone does not block host→peer RW"
890        );
891    }
892
893    #[test]
894    fn shutdown_send_bit_propagates_eof_to_daemon_fd() {
895        // Regression for ABX-372: F_SEND half-close must translate into a
896        // SHUT_WR on the internal socketpair end so the daemon-side fd reads
897        // EOF. Without this, the Docker attach bridge (`copy_bidirectional`)
898        // stalls forever after the container exits.
899        use std::io::Read;
900        use std::os::fd::IntoRawFd;
901
902        let mut mgr = VsockConnectionManager::new();
903        let (daemon_end, internal) = make_socketpair();
904        let (id, _rx) = mgr.allocate(1024, 3, internal);
905
906        // Wrap the daemon end in a blocking `UnixStream` for `read`.
907        let mut daemon_stream =
908            unsafe { std::os::unix::net::UnixStream::from_raw_fd(daemon_end.into_raw_fd()) };
909        // Bound the read so a regression doesn't hang the test runner.
910        daemon_stream
911            .set_read_timeout(Some(std::time::Duration::from_secs(2)))
912            .unwrap();
913
914        // Before the half-close, the daemon's read blocks. After F_SEND
915        // handling, it must return 0 (EOF).
916        mgr.handle_shutdown(id.guest_port, id.host_port, VSOCK_SHUTDOWN_F_SEND);
917
918        let mut buf = [0u8; 8];
919        let n = daemon_stream
920            .read(&mut buf)
921            .expect("read on daemon fd should not error");
922        assert_eq!(n, 0, "daemon fd must read EOF after F_SEND propagation");
923
924        // Reverse direction stays open: daemon can still write to the peer.
925        use std::io::Write;
926        daemon_stream
927            .write_all(b"still-alive")
928            .expect("daemon→internal write should still succeed");
929    }
930
931    #[test]
932    fn doorbell_rings_on_producer_paths() {
933        use std::sync::atomic::AtomicUsize;
934
935        let rings = Arc::new(AtomicUsize::new(0));
936        let mut mgr = VsockConnectionManager::new();
937        let rings_cb = Arc::clone(&rings);
938        mgr.set_doorbell(Arc::new(move || {
939            rings_cb.fetch_add(1, Ordering::SeqCst);
940        }));
941
942        let (_, internal) = make_socketpair();
943        let (id, _rx) = mgr.allocate(1024, 3, internal);
944        assert_eq!(rings.load(Ordering::SeqCst), 1, "allocate rings");
945
946        mgr.mark_connected(id.guest_port, id.host_port);
947        assert_eq!(rings.load(Ordering::SeqCst), 2, "mark_connected rings");
948
949        mgr.enqueue_credit_update(id.guest_port, id.host_port);
950        assert_eq!(
951            rings.load(Ordering::SeqCst),
952            3,
953            "enqueue_credit_update rings"
954        );
955
956        // advance_fwd_cnt rings only when it actually enqueues RX work.
957        assert!(mgr.advance_fwd_cnt(id.guest_port, id.host_port, CREDIT_UPDATE_THRESHOLD));
958        assert_eq!(
959            rings.load(Ordering::SeqCst),
960            4,
961            "advance_fwd_cnt rings on push"
962        );
963    }
964
965    #[test]
966    fn doorbell_silent_on_injection_driver_paths() {
967        use std::sync::atomic::AtomicUsize;
968
969        let rings = Arc::new(AtomicUsize::new(0));
970        let mut mgr = VsockConnectionManager::new();
971
972        let (_, internal) = make_socketpair();
973        let (id, _rx) = mgr.allocate(1024, 3, internal);
974        // Drain the initial REQUEST so rx_queue is empty for the checks below.
975        mgr.get_mut(&id).unwrap().rx_queue.dequeue();
976
977        // Install the doorbell after allocate so only the calls below count.
978        let rings_cb = Arc::clone(&rings);
979        mgr.set_doorbell(Arc::new(move || {
980            rings_cb.fetch_add(1, Ordering::SeqCst);
981        }));
982
983        // Sub-threshold fwd_cnt advance enqueues nothing → no ring.
984        assert!(!mgr.advance_fwd_cnt(id.guest_port, id.host_port, 1));
985        assert_eq!(rings.load(Ordering::SeqCst), 0);
986
987        // Phase-1 enqueues come from the injection driver itself — it is
988        // already awake, so these must not self-wake it.
989        mgr.enqueue_rw(id);
990        mgr.enqueue_reset(id);
991        assert_eq!(rings.load(Ordering::SeqCst), 0);
992    }
993
994    #[test]
995    fn shutdown_flags_zero_removes_connection_conservatively() {
996        // flags=0 is spec-invalid; worst-case interpretation is full close.
997        let mut mgr = VsockConnectionManager::new();
998        let (_, internal) = make_socketpair();
999        let (id, _rx) = mgr.allocate(1024, 3, internal);
1000
1001        mgr.handle_shutdown(id.guest_port, id.host_port, 0);
1002        assert!(mgr.get(&id).is_none());
1003    }
1004}