Skip to main content

arcbox_virtio_vsock/
device.rs

1//! `VirtioVsock` device — TX/RX queue handling, custom-VMM hot path, `VirtioDevice` impl.
2
3mod rx_injection;
4#[cfg(test)]
5mod tests;
6mod virtio_device;
7
8use std::collections::HashMap;
9use std::sync::{Arc, Mutex, RwLock};
10
11use arcbox_virtio_core::error::{Result, VirtioError};
12use arcbox_virtio_core::queue::VirtQueue;
13use arcbox_virtio_core::{DeviceCtx, virtio_bindings};
14
15use crate::addr::{HOST_CID, RESERVED_CID, VsockAddr, VsockHostConnections};
16use crate::backend::VsockBackend;
17use crate::connection::{ConnectionState, VsockConnection};
18use crate::manager::VsockConnectionManager;
19use crate::protocol::{VsockHeader, VsockOp};
20
21/// Forwards `buf` to `fd` with partial-write + `EAGAIN` handling.
22///
23/// A single `libc::write` on a non-blocking socketpair can return short
24/// (SO_SNDBUF full) or `EAGAIN` (buffer completely full). The previous
25/// implementation dropped the tail in both cases, silently truncating
26/// responses larger than the socket buffer (macOS default ~8 KiB). This
27/// helper loops until all bytes are written, the peer closes the fd, or
28/// the deadline expires. Returns the total number of bytes successfully
29/// delivered.
30///
31/// Runs on the vCPU thread via the BSP's TX handler, so we cap the total
32/// poll wait at a few milliseconds per call — enough to let the client
33/// drain typical RPC responses, short enough that a slow consumer does
34/// not stall the guest indefinitely. If the cap is hit we return a short
35/// count; `advance_fwd_cnt` then reflects only what was delivered, and
36/// the guest's credit accounting backs off naturally. See ABX-365.
37fn write_all_with_backoff(fd: i32, buf: &[u8]) -> usize {
38    const MAX_POLL_RETRIES: u32 = 16;
39    const POLL_TIMEOUT_MS: libc::c_int = 2; // total worst case: 32 ms
40
41    let mut offset = 0usize;
42    let mut eagain_retries = 0u32;
43
44    while offset < buf.len() {
45        // SAFETY: fd is a valid connected socket from the manager;
46        // `buf[offset..]` is a live slice for the remaining bytes.
47        let ret = unsafe {
48            libc::write(
49                fd,
50                buf[offset..].as_ptr().cast::<libc::c_void>(),
51                buf.len() - offset,
52            )
53        };
54
55        use std::cmp::Ordering;
56        match ret.cmp(&0) {
57            Ordering::Greater => {
58                offset += ret as usize;
59                eagain_retries = 0;
60            }
61            Ordering::Equal => {
62                // Peer closed. Nothing more we can do.
63                break;
64            }
65            Ordering::Less => {
66                let err = std::io::Error::last_os_error();
67                match err.raw_os_error() {
68                    Some(e) if e == libc::EAGAIN || e == libc::EWOULDBLOCK => {
69                        if eagain_retries >= MAX_POLL_RETRIES {
70                            tracing::warn!(
71                                "Vsock: giving up after {MAX_POLL_RETRIES} EAGAIN retries at offset {offset}/{} on fd {fd}",
72                                buf.len(),
73                            );
74                            break;
75                        }
76                        eagain_retries += 1;
77                        // Wait for POLLOUT so the next write has a chance.
78                        let mut pfd = libc::pollfd {
79                            fd,
80                            events: libc::POLLOUT,
81                            revents: 0,
82                        };
83                        // SAFETY: single pollfd on the stack, count=1.
84                        let _ = unsafe { libc::poll(&mut pfd, 1, POLL_TIMEOUT_MS) };
85                    }
86                    Some(libc::EINTR) => {}
87                    _ => {
88                        tracing::warn!("Vsock: write to fd {fd} failed at offset {offset}: {err}");
89                        break;
90                    }
91                }
92            }
93        }
94    }
95
96    offset
97}
98
99/// Vsock device configuration.
100#[derive(Debug, Clone)]
101pub struct VsockConfig {
102    /// Guest CID (Context Identifier).
103    pub guest_cid: u64,
104}
105
106impl Default for VsockConfig {
107    fn default() -> Self {
108        Self {
109            guest_cid: 3, // First available guest CID
110        }
111    }
112}
113
114/// `VirtIO` vsock device.
115///
116/// Enables socket communication between host (CID 2) and guest using
117/// virtio transport.
118pub struct VirtioVsock {
119    config: VsockConfig,
120    features: u64,
121    acked_features: u64,
122    /// Backend for host-side socket handling.
123    backend: Option<Arc<Mutex<dyn VsockBackend>>>,
124    /// Active connections.
125    connections: RwLock<HashMap<(u32, u32), VsockConnection>>,
126    /// Queue 0: RX (host -> guest).
127    rx_queue: Option<VirtQueue>,
128    /// Queue 1: TX (guest -> host).
129    tx_queue: Option<VirtQueue>,
130    /// Queue 2: Event (control events).
131    event_queue: Option<VirtQueue>,
132    /// Host-side connection fds keyed by guest port.
133    /// Used by the guest-memory `process_queue` path to forward data
134    /// between host sockets and guest vsock queues.
135    host_connections: HashMap<u32, std::os::unix::io::RawFd>,
136    /// Last processed avail index for TX queue (guest-memory path).
137    last_avail_idx_tx: usize,
138    /// Last processed avail index for RX queue (guest-memory path).
139    last_avail_idx_rx: usize,
140    /// Guest memory + IRQ context. Bound at registration time on the
141    /// HV backend; remains `None` on the VZ backend (which does not use
142    /// the custom-VMM `poll_rx_injection` path).
143    ctx: Option<DeviceCtx>,
144    /// Trait-object view of the host-side connection manager. Used by
145    /// `process_queue` (TX path) so tests can supply a mock implementing
146    /// `VsockHostConnections` without dragging in the concrete manager.
147    conns: Option<Arc<Mutex<dyn VsockHostConnections>>>,
148    /// Concrete view of the host-side connection manager. Required by
149    /// `poll_rx_injection`, which calls non-trait methods (`backend_rxq`,
150    /// `connections_with_pending_rx`, `get`/`get_mut`/`remove`,
151    /// `enqueue_rw`/`enqueue_reset`, `peek`/`dequeue`/`pending` on
152    /// `RxOps`, etc.). Always set alongside `conns` in production via
153    /// `bind_connection_manager`; left `None` in unit-test contexts.
154    conn_mgr: Option<Arc<Mutex<VsockConnectionManager>>>,
155}
156
157impl VirtioVsock {
158    /// Feature: Stream socket.
159    pub const FEATURE_STREAM: u64 = 1 << 0;
160    /// Feature: Seqpacket socket.
161    pub const FEATURE_SEQPACKET: u64 = 1 << 1;
162    /// VirtIO version 1 compliance (required for modern MMIO transport).
163    pub const FEATURE_VERSION_1: u64 = 1 << virtio_bindings::virtio_config::VIRTIO_F_VERSION_1;
164
165    /// Well-known CID for host.
166    pub const HOST_CID: u64 = HOST_CID;
167    /// Reserved CID.
168    pub const RESERVED_CID: u64 = RESERVED_CID;
169
170    /// Creates a new vsock device.
171    #[must_use]
172    pub fn new(config: VsockConfig) -> Self {
173        Self {
174            config,
175            features: Self::FEATURE_STREAM
176                | Self::FEATURE_VERSION_1
177                | arcbox_virtio_core::queue::VIRTIO_F_EVENT_IDX,
178            acked_features: 0,
179            backend: None,
180            connections: RwLock::new(HashMap::new()),
181            rx_queue: None,
182            tx_queue: None,
183            event_queue: None,
184            host_connections: HashMap::new(),
185            last_avail_idx_tx: 0,
186            last_avail_idx_rx: 0,
187            ctx: None,
188            conns: None,
189            conn_mgr: None,
190        }
191    }
192
193    /// Creates a vsock device with a backend.
194    #[must_use]
195    pub fn with_backend<B: VsockBackend + 'static>(config: VsockConfig, backend: B) -> Self {
196        Self {
197            config,
198            features: Self::FEATURE_STREAM
199                | Self::FEATURE_VERSION_1
200                | arcbox_virtio_core::queue::VIRTIO_F_EVENT_IDX,
201            acked_features: 0,
202            backend: Some(Arc::new(Mutex::new(backend))),
203            connections: RwLock::new(HashMap::new()),
204            rx_queue: None,
205            tx_queue: None,
206            event_queue: None,
207            host_connections: HashMap::new(),
208            last_avail_idx_tx: 0,
209            last_avail_idx_rx: 0,
210            ctx: None,
211            conns: None,
212            conn_mgr: None,
213        }
214    }
215
216    /// Sets the backend.
217    pub fn set_backend<B: VsockBackend + 'static>(&mut self, backend: B) {
218        self.backend = Some(Arc::new(Mutex::new(backend)));
219    }
220
221    /// Binds the device's `DeviceCtx` (guest memory + IRQ trigger).
222    /// Required by the custom-VMM `poll_rx_injection` hot path.
223    pub fn bind_ctx(&mut self, ctx: DeviceCtx) {
224        self.ctx = Some(ctx);
225    }
226
227    /// Binds a trait-object view of the host-side connection manager.
228    /// Required by `process_queue(1, ...)` (TX path). Tests set this
229    /// directly with a mock; production callers use
230    /// `bind_connection_manager` which also sets the concrete view.
231    pub fn bind_connections(&mut self, conns: Arc<Mutex<dyn VsockHostConnections>>) {
232        self.conns = Some(conns);
233    }
234
235    /// Binds the concrete `VsockConnectionManager`. Required by
236    /// `poll_rx_injection`, which uses non-trait methods. Stores both
237    /// the trait-object view (for `process_queue`) and the concrete
238    /// view (for `poll_rx_injection`) — same `Arc`, two lenses.
239    pub fn bind_connection_manager(&mut self, mgr: Arc<Mutex<VsockConnectionManager>>) {
240        self.conns = Some(mgr.clone());
241        self.conn_mgr = Some(mgr);
242    }
243
244    /// Returns a clone of the trait-object connection manager Arc.
245    pub fn connections(&self) -> Option<Arc<Mutex<dyn VsockHostConnections>>> {
246        self.conns.clone()
247    }
248
249    /// Returns the guest CID.
250    #[must_use]
251    pub const fn guest_cid(&self) -> u64 {
252        self.config.guest_cid
253    }
254
255    /// Handles a connection request from guest.
256    pub fn handle_connect(&self, src_port: u32, dst_port: u32) -> Result<()> {
257        let local = VsockAddr::new(self.config.guest_cid, src_port);
258        let remote = VsockAddr::new(Self::HOST_CID, dst_port);
259
260        let mut conn = VsockConnection::new(local, remote);
261        conn.state = ConnectionState::Connecting;
262
263        if let Some(ref backend) = self.backend {
264            backend.lock().unwrap().on_connect(local)?;
265            conn.state = ConnectionState::Connected;
266        }
267
268        self.connections
269            .write()
270            .unwrap()
271            .insert((src_port, dst_port), conn);
272        tracing::debug!(
273            "Vsock connect: {}:{} -> {}:{}",
274            self.config.guest_cid,
275            src_port,
276            Self::HOST_CID,
277            dst_port
278        );
279
280        Ok(())
281    }
282
283    /// Handles data from guest.
284    pub fn handle_send(&self, src_port: u32, dst_port: u32, data: &[u8]) -> Result<usize> {
285        let local = VsockAddr::new(self.config.guest_cid, src_port);
286
287        if let Some(ref backend) = self.backend {
288            backend.lock().unwrap().on_send(local, data)
289        } else {
290            let mut conns = self.connections.write().unwrap();
291            if let Some(conn) = conns.get_mut(&(src_port, dst_port)) {
292                conn.enqueue_tx(data);
293                Ok(data.len())
294            } else {
295                Err(VirtioError::InvalidOperation("Connection not found".into()))
296            }
297        }
298    }
299
300    /// Handles receive request from guest.
301    pub fn handle_recv(&self, src_port: u32, dst_port: u32, buf: &mut [u8]) -> Result<usize> {
302        let local = VsockAddr::new(self.config.guest_cid, src_port);
303
304        if let Some(ref backend) = self.backend {
305            backend.lock().unwrap().on_recv(local, buf)
306        } else {
307            let mut conns = self.connections.write().unwrap();
308            if let Some(conn) = conns.get_mut(&(src_port, dst_port)) {
309                let data = conn.dequeue_rx(buf.len());
310                buf[..data.len()].copy_from_slice(&data);
311                Ok(data.len())
312            } else {
313                Err(VirtioError::InvalidOperation("Connection not found".into()))
314            }
315        }
316    }
317
318    /// Handles connection close from guest.
319    pub fn handle_close(&self, src_port: u32, dst_port: u32) -> Result<()> {
320        let local = VsockAddr::new(self.config.guest_cid, src_port);
321
322        if let Some(ref backend) = self.backend {
323            backend.lock().unwrap().on_close(local)?;
324        }
325
326        self.connections
327            .write()
328            .unwrap()
329            .remove(&(src_port, dst_port));
330        tracing::debug!("Vsock close: {}:{}", self.config.guest_cid, src_port);
331
332        Ok(())
333    }
334
335    /// Returns the number of active connections.
336    #[must_use]
337    pub fn connection_count(&self) -> usize {
338        self.connections.read().unwrap().len()
339    }
340
341    /// Returns a mutable reference to the TX queue.
342    pub fn tx_queue_mut(&mut self) -> Option<&mut VirtQueue> {
343        self.tx_queue.as_mut()
344    }
345
346    /// Returns a mutable reference to the RX queue.
347    pub fn rx_queue_mut(&mut self) -> Option<&mut VirtQueue> {
348        self.rx_queue.as_mut()
349    }
350
351    /// Handles a TX packet from the guest, forwarding data to host fds.
352    fn handle_tx_packet_with_fds(
353        &self,
354        hdr: &VsockHeader,
355        payload: &[u8],
356        connections: Option<&mut dyn VsockHostConnections>,
357    ) {
358        // Copy packed fields to locals to avoid unaligned reference UB.
359        let src_cid = { hdr.src_cid };
360        let dst_cid = { hdr.dst_cid };
361        let src_port = { hdr.src_port };
362        let dst_port = { hdr.dst_port };
363        let buf_alloc = { hdr.buf_alloc };
364        let fwd_cnt = { hdr.fwd_cnt };
365        let flags = { hdr.flags };
366
367        match hdr.operation() {
368            Some(VsockOp::Request) => {
369                tracing::debug!(
370                    "Vsock TX: OP_REQUEST src={}:{} dst={}:{}",
371                    src_cid,
372                    src_port,
373                    dst_cid,
374                    dst_port,
375                );
376            }
377            Some(VsockOp::Response) => {
378                // Guest accepted a host-initiated connection.
379                // src_port = guest port, dst_port = host ephemeral port.
380                tracing::info!(
381                    "Vsock TX: OP_RESPONSE — connection established (guest_port={}, host_port={})",
382                    src_port,
383                    dst_port,
384                );
385                if let Some(conns) = connections {
386                    conns.update_peer_credit(src_port, dst_port, buf_alloc, fwd_cnt);
387                    conns.mark_connected(src_port, dst_port);
388                }
389            }
390            Some(VsockOp::Rw) => {
391                // Guest sends data. src_port = guest port, dst_port = host port.
392                if let Some(conns) = connections {
393                    conns.update_peer_credit(src_port, dst_port, buf_alloc, fwd_cnt);
394                    if let Some(fd) = conns.fd_for(src_port, dst_port) {
395                        if !payload.is_empty() {
396                            let total = payload.len();
397                            let forwarded = write_all_with_backoff(fd, payload);
398                            if forwarded > 0 {
399                                tracing::debug!(
400                                    "Vsock TX: OP_RW guest_port={} host_port={} -> fd {fd}, {}/{} bytes",
401                                    src_port,
402                                    dst_port,
403                                    forwarded,
404                                    total,
405                                );
406                                // Advance fwd_cnt by the byte count we actually
407                                // delivered to the host socket. If the write
408                                // loop gave up due to sustained EAGAIN or a
409                                // hard error, `forwarded` will be < total and
410                                // guest credit accounting will reflect that
411                                // (fewer acks → guest backs off).
412                                #[allow(clippy::cast_possible_truncation)]
413                                {
414                                    conns.advance_fwd_cnt(src_port, dst_port, forwarded as u32);
415                                }
416                            }
417                            if forwarded < total {
418                                tracing::warn!(
419                                    "Vsock TX: truncated write guest_port={} host_port={}: only {}/{} bytes forwarded (ABX-365)",
420                                    src_port,
421                                    dst_port,
422                                    forwarded,
423                                    total,
424                                );
425                            }
426                        }
427                    } else {
428                        tracing::warn!(
429                            "Vsock TX: OP_RW no host fd for guest_port={} host_port={}",
430                            src_port,
431                            dst_port,
432                        );
433                    }
434                }
435            }
436            Some(VsockOp::Shutdown) => {
437                tracing::debug!(
438                    "Vsock TX: OP_SHUTDOWN guest_port={} host_port={} flags=0x{:x}",
439                    src_port,
440                    dst_port,
441                    flags,
442                );
443                if let Some(conns) = connections {
444                    // Dispatch on the shutdown flags — a half-close (only
445                    // F_RECEIVE or only F_SEND) should preserve the fd so
446                    // either side can still drain in-flight data.
447                    conns.handle_shutdown(src_port, dst_port, flags);
448                }
449            }
450            Some(VsockOp::Rst) => {
451                tracing::debug!(
452                    "Vsock TX: OP_RST guest_port={} host_port={}",
453                    src_port,
454                    dst_port,
455                );
456                if let Some(conns) = connections {
457                    conns.remove_connection(src_port, dst_port);
458                }
459            }
460            Some(VsockOp::CreditUpdate) => {
461                tracing::trace!(
462                    "Vsock TX: OP_CREDIT_UPDATE guest_port={} host_port={} buf_alloc={} fwd_cnt={}",
463                    src_port,
464                    dst_port,
465                    buf_alloc,
466                    fwd_cnt,
467                );
468                if let Some(conns) = connections {
469                    conns.update_peer_credit(src_port, dst_port, buf_alloc, fwd_cnt);
470                }
471            }
472            Some(VsockOp::CreditRequest) => {
473                tracing::trace!(
474                    "Vsock TX: OP_CREDIT_REQUEST guest_port={} host_port={}",
475                    src_port,
476                    dst_port,
477                );
478                if let Some(conns) = connections {
479                    conns.update_peer_credit(src_port, dst_port, buf_alloc, fwd_cnt);
480                    conns.enqueue_credit_update(src_port, dst_port);
481                }
482            }
483            _ => {}
484        }
485    }
486
487    /// Registers a host-side fd for a guest vsock port.
488    /// When the guest sends data to this port, it will be written to the fd.
489    /// When the fd has data, it will be injected into the guest RX queue.
490    pub fn add_host_connection(&mut self, guest_port: u32, fd: std::os::unix::io::RawFd) {
491        tracing::info!("Vsock: host connection for guest port {guest_port} -> fd {fd}");
492        self.host_connections.insert(guest_port, fd);
493    }
494
495    /// Process pending TX queue packets from guest.
496    ///
497    /// Pops available descriptors from the TX virtqueue, parses vsock headers,
498    /// and dispatches each packet based on its operation code. Returns a list
499    /// of completed descriptor heads and their written lengths, suitable for
500    /// `push_used_batch()`.
501    ///
502    /// # Errors
503    ///
504    /// Returns an error if the TX queue is not ready or packet processing fails.
505    pub fn process_tx_queue(&mut self, memory: &mut [u8]) -> Result<Vec<(u16, u32)>> {
506        // Phase 1: Collect raw descriptor data from the TX queue.
507        let mut raw_packets: Vec<(u16, Vec<u8>)> = Vec::new();
508
509        {
510            let queue = self
511                .tx_queue
512                .as_mut()
513                .ok_or_else(|| VirtioError::NotReady("TX queue not ready".into()))?;
514
515            while let Some((head_idx, chain)) = queue.pop_avail() {
516                let mut data = Vec::new();
517
518                for desc in chain {
519                    if !desc.is_write_only() {
520                        // Read-only buffers contain the guest-produced packet.
521                        let start = desc.addr as usize;
522                        let end = start + desc.len as usize;
523                        if end <= memory.len() {
524                            data.extend_from_slice(&memory[start..end]);
525                        }
526                    }
527                }
528
529                raw_packets.push((head_idx, data));
530            }
531        }
532
533        // Phase 2: Parse and dispatch each packet.
534        let mut completions = Vec::new();
535        // Collect RX packets to inject after releasing the connections lock.
536        let mut rx_inject: Vec<(VsockHeader, Vec<u8>)> = Vec::new();
537
538        for (head_idx, data) in &raw_packets {
539            if data.len() < VsockHeader::SIZE {
540                tracing::warn!(
541                    "Vsock TX: descriptor {} too short ({} bytes), skipping",
542                    head_idx,
543                    data.len()
544                );
545                completions.push((*head_idx, 0u32));
546                continue;
547            }
548
549            let header = match VsockHeader::from_bytes(&data[..VsockHeader::SIZE]) {
550                Some(h) => h,
551                None => {
552                    tracing::warn!(
553                        "Vsock TX: failed to parse header for descriptor {}",
554                        head_idx
555                    );
556                    completions.push((*head_idx, 0u32));
557                    continue;
558                }
559            };
560
561            let payload_len = { header.len } as usize;
562            let payload = if payload_len > 0 && data.len() > VsockHeader::SIZE {
563                let avail = data.len() - VsockHeader::SIZE;
564                &data[VsockHeader::SIZE..VsockHeader::SIZE + payload_len.min(avail)]
565            } else {
566                &[] as &[u8]
567            };
568
569            let src_port = { header.src_port };
570            let dst_port = { header.dst_port };
571
572            match header.operation() {
573                Some(VsockOp::Request) => {
574                    tracing::debug!(
575                        "Vsock TX: OP_REQUEST from port {} to port {}",
576                        src_port,
577                        dst_port
578                    );
579                    match self.handle_connect(src_port, dst_port) {
580                        Ok(()) => {
581                            // Build a RESPONSE header to inject into the RX queue.
582                            let resp = VsockHeader::new(
583                                VsockAddr::new(Self::HOST_CID, dst_port),
584                                VsockAddr::new(self.config.guest_cid, src_port),
585                                VsockOp::Response,
586                            );
587                            rx_inject.push((resp, Vec::new()));
588                        }
589                        Err(e) => {
590                            tracing::warn!("Vsock TX: connect failed: {}", e);
591                            // Send RST back to the guest.
592                            let rst = VsockHeader::new(
593                                VsockAddr::new(Self::HOST_CID, dst_port),
594                                VsockAddr::new(self.config.guest_cid, src_port),
595                                VsockOp::Rst,
596                            );
597                            rx_inject.push((rst, Vec::new()));
598                        }
599                    }
600                }
601                Some(VsockOp::Response) => {
602                    // Guest acknowledging a host-initiated connection.
603                    tracing::debug!(
604                        "Vsock TX: OP_RESPONSE from port {} to port {}",
605                        src_port,
606                        dst_port
607                    );
608                    let mut conns = self.connections.write().unwrap();
609                    if let Some(conn) = conns.get_mut(&(src_port, dst_port)) {
610                        conn.state = ConnectionState::Connected;
611                    }
612                }
613                Some(VsockOp::Rw) => {
614                    tracing::trace!(
615                        "Vsock TX: OP_RW {} bytes from port {} to port {}",
616                        payload.len(),
617                        src_port,
618                        dst_port
619                    );
620                    if let Err(e) = self.handle_send(src_port, dst_port, payload) {
621                        tracing::warn!("Vsock TX: send failed: {}", e);
622                    }
623                }
624                Some(VsockOp::Shutdown) => {
625                    tracing::debug!(
626                        "Vsock TX: OP_SHUTDOWN from port {} to port {}",
627                        src_port,
628                        dst_port
629                    );
630                    if let Err(e) = self.handle_close(src_port, dst_port) {
631                        tracing::warn!("Vsock TX: close failed: {}", e);
632                    }
633                    // Confirm with RST.
634                    let rst = VsockHeader::new(
635                        VsockAddr::new(Self::HOST_CID, dst_port),
636                        VsockAddr::new(self.config.guest_cid, src_port),
637                        VsockOp::Rst,
638                    );
639                    rx_inject.push((rst, Vec::new()));
640                }
641                Some(VsockOp::Rst) => {
642                    tracing::debug!(
643                        "Vsock TX: OP_RST from port {} to port {}",
644                        src_port,
645                        dst_port
646                    );
647                    let _ = self.handle_close(src_port, dst_port);
648                }
649                Some(VsockOp::CreditUpdate) => {
650                    let buf_alloc = { header.buf_alloc };
651                    let fwd_cnt = { header.fwd_cnt };
652                    tracing::trace!(
653                        "Vsock TX: OP_CREDIT_UPDATE port {} buf_alloc={} fwd_cnt={}",
654                        src_port,
655                        buf_alloc,
656                        fwd_cnt
657                    );
658                    let mut conns = self.connections.write().unwrap();
659                    if let Some(conn) = conns.get_mut(&(src_port, dst_port)) {
660                        conn.update_peer_credit(buf_alloc, fwd_cnt);
661                    }
662                }
663                Some(VsockOp::CreditRequest) => {
664                    tracing::trace!(
665                        "Vsock TX: OP_CREDIT_REQUEST from port {} to port {}",
666                        src_port,
667                        dst_port
668                    );
669                    // Respond with our credit state.
670                    let conns = self.connections.read().unwrap();
671                    if let Some(conn) = conns.get(&(src_port, dst_port)) {
672                        let mut update = VsockHeader::new(
673                            VsockAddr::new(Self::HOST_CID, dst_port),
674                            VsockAddr::new(self.config.guest_cid, src_port),
675                            VsockOp::CreditUpdate,
676                        );
677                        update.buf_alloc = conn.buf_alloc;
678                        update.fwd_cnt = conn.fwd_cnt;
679                        rx_inject.push((update, Vec::new()));
680                    }
681                }
682                Some(VsockOp::Invalid) | None => {
683                    let raw_op = { header.op };
684                    tracing::warn!(
685                        "Vsock TX: unknown/invalid op {} from port {}",
686                        raw_op,
687                        src_port
688                    );
689                }
690            }
691
692            completions.push((*head_idx, data.len() as u32));
693        }
694
695        // Phase 3: Inject any pending RX response packets.
696        for (hdr, payload) in rx_inject {
697            if let Err(e) = self.inject_rx_packet(&hdr, &payload, memory) {
698                tracing::warn!("Vsock: failed to inject RX packet: {}", e);
699            }
700        }
701
702        Ok(completions)
703    }
704
705    /// Process a specific virtqueue by index.
706    ///
707    /// Queue indices follow the VirtIO vsock specification:
708    /// - 0: RX (host -> guest) — processed externally via `inject_rx_packet`
709    /// - 1: TX (guest -> host) — dispatched here
710    /// - 2: Event queue       — not yet implemented
711    ///
712    /// # Errors
713    ///
714    /// Returns an error if processing fails.
715    pub fn process_queue(&mut self, queue_idx: u16, memory: &mut [u8]) -> Result<Vec<(u16, u32)>> {
716        match queue_idx {
717            1 => self.process_tx_queue(memory),
718            _ => Ok(Vec::new()),
719        }
720    }
721
722    /// Injects a response packet into the guest RX queue.
723    ///
724    /// Pops an available descriptor from the RX queue, writes the vsock header
725    /// and optional payload into guest memory via the descriptor chain, then
726    /// marks it as used. The MMIO/interrupt handler is responsible for
727    /// signalling the guest after this call.
728    ///
729    /// # Errors
730    ///
731    /// Returns an error if the RX queue is not ready or no descriptors are
732    /// available.
733    pub fn inject_rx_packet(
734        &mut self,
735        header: &VsockHeader,
736        data: &[u8],
737        memory: &mut [u8],
738    ) -> Result<()> {
739        let queue = self
740            .rx_queue
741            .as_mut()
742            .ok_or_else(|| VirtioError::NotReady("RX queue not ready".into()))?;
743
744        let (head_idx, chain) = queue
745            .pop_avail()
746            .ok_or_else(|| VirtioError::InvalidQueue("No available RX descriptors".into()))?;
747
748        let header_bytes = header.to_bytes();
749        let total_len = header_bytes.len() + data.len();
750        let mut frame = Vec::with_capacity(total_len);
751        frame.extend_from_slice(&header_bytes);
752        frame.extend_from_slice(data);
753
754        let mut written = 0usize;
755        for desc in chain {
756            if !desc.is_write_only() {
757                continue;
758            }
759            let start = desc.addr as usize;
760            let remaining = frame.len().saturating_sub(written);
761            let to_write = remaining.min(desc.len as usize);
762            if to_write == 0 {
763                continue;
764            }
765            let end = start + to_write;
766            if end > memory.len() {
767                return Err(VirtioError::MemoryError(
768                    "RX descriptor points outside guest memory".into(),
769                ));
770            }
771            memory[start..end].copy_from_slice(&frame[written..written + to_write]);
772            written += to_write;
773        }
774
775        queue.push_used(head_idx, written as u32);
776        Ok(())
777    }
778}