Skip to main content

tiny_vsock/
lib.rs

1// SPDX-License-Identifier: MIT
2
3//! Tiny vsock is a Rust library that provides an abstraction over vsock (`AF_VSOCK`) sockets
4//! for communication between hosts and virtual machines or enclaves. It offers a simple API
5//! for creating, connecting, binding, listening, accepting, sending, and receiving data through
6//! vsock sockets. The library is designed to be efficient and easy to use, with built-in retry
7//! mechanisms for connection attempts and chunked data transfer for optimal performance.
8//!
9//! # Features
10//! - `std-io`: Enables implementation of the standard `Read` and `Write` traits for `Vsock`,
11//!   allowing it to be used with standard Rust I/O patterns.
12//!
13//! # Examples
14//!
15//! - [`enclave-echo-service`](https://github.com/orlowskilp/tiny-vsock/blob/master/examples/enclave-echo-service.rs)
16//!   — binds, accepts one
17//!   connection, receives data, and echoes it back; run inside the enclave
18//! - [`parent-echo-client`](https://github.com/orlowskilp/tiny-vsock/blob/master/examples/parent-echo-client.rs)
19//!   — connects to the enclave
20//!   service, sends a message, and reads the echo; run on the parent instance
21//! - [`std_io_echo`](https://github.com/orlowskilp/tiny-vsock/blob/master/examples/std_io_echo.rs)
22//!   — demonstrates `std::io::Read` /
23//!   `std::io::Write` via `BufReader` and `flush`; requires `--features std-io`
24
25use anyhow::{anyhow, Result};
26#[cfg(feature = "std-io")]
27use nix::sys::socket::Shutdown;
28use nix::{
29    sys::socket::{self, AddressFamily, Backlog, MsgFlags, SockFlag, SockType, VsockAddr},
30    Error as NixError,
31};
32#[cfg(feature = "std-io")]
33use std::io::{Error as IoError, Read, Result as IoResult, Write};
34use std::{
35    os::{
36        fd::{AsFd, BorrowedFd, FromRawFd, OwnedFd},
37        unix::io::{AsRawFd, RawFd},
38    },
39    result::Result as StdResult,
40    thread::sleep,
41    time::Duration,
42};
43
44/// Abstraction over a vsock (`AF_VSOCK`) socket for communication between hosts and virtual machines
45/// or enclaves.
46pub struct Vsock {
47    /// Vsock socket file descriptor.
48    socket_fd: OwnedFd,
49}
50
51impl AsRawFd for Vsock {
52    /// Return the raw file descriptor of the Vsock socket.
53    fn as_raw_fd(&self) -> RawFd {
54        self.socket_fd.as_raw_fd()
55    }
56}
57
58impl AsFd for Vsock {
59    /// Return the Vsock file descriptor as a `BorrowedFd` for use with nix socket operations.
60    fn as_fd(&self) -> BorrowedFd<'_> {
61        self.socket_fd.as_fd()
62    }
63}
64
65impl Vsock {
66    /// Maximum number of attempts to connect to a Vsock socket before giving up.
67    const CONNECT_ATTEMPTS: usize = 5;
68    /// CID address to bind to for accepting connections from any CID. This is a convention
69    /// in vsock
70    pub const ANY_CID_ADDR: u32 = u32::MAX;
71    /// CID address used for communication with the Nitro Enclave parent instance. This is a
72    /// convention in AWS Nitro Enclaves and is not meant to be used for general vsock
73    /// communication outside of Nitro Enclaves.
74    pub const PARENT_NE_CID_ADDR: u32 = 3;
75
76    /// Idiomatic method to create a new Vsock instance from a given socket file descriptor. Not meant
77    /// for public use.
78    fn new(socket_fd: OwnedFd) -> Self {
79        Vsock { socket_fd }
80    }
81
82    /// Shorthand method to create a new Vsock socket. Not meant for public use.
83    fn socket() -> Result<OwnedFd> {
84        socket::socket(AddressFamily::Vsock, SockType::Stream, SockFlag::empty(), None).map_err(|err| {
85            tracing::error!("Vsock: Create socket failed: {err:#?}");
86            anyhow!(err)
87        })
88    }
89
90    /// Connect to a Vsock socket with a given CID and port and return a vsock handle.
91    ///
92    /// # Arguments
93    ///
94    /// * `cid` - CID address to connect to.
95    /// * `port` - Port to connect to.
96    ///
97    /// # Returns
98    ///
99    /// A `Vsock` instance representing the connected socket or an error if the connection
100    /// fails after 5 attempts. The method implements an exponential backoff strategy for
101    /// retrying failed connection attempts.
102    pub fn connect(cid: u32, port: u32) -> Result<Self> {
103        Self::connect_with_max_attempts(cid, port, Self::CONNECT_ATTEMPTS)
104    }
105
106    /// Connect to a Vsock socket with a given CID and port and return a vsock handle.
107    ///
108    /// # Arguments
109    ///
110    /// * `cid` - CID address to connect to.
111    /// * `port` - Port to connect to.
112    /// * `max_attempts` - Maximum number of attempts to connect before giving up.
113    ///
114    /// # Returns
115    ///
116    /// A `Vsock` instance representing the connected socket or an error if the connection
117    /// fails after the specified number of attempts. The method implements an exponential
118    /// backoff strategy for retrying failed connection attempts.
119    pub fn connect_with_max_attempts(cid: u32, port: u32, max_attempts: usize) -> Result<Self> {
120        for i in 0..max_attempts {
121            let vsock = Self::new(Self::socket()?);
122            match socket::connect(vsock.as_raw_fd(), &VsockAddr::new(cid, port)) {
123                Ok(_) => return Ok(vsock),
124                Err(err) => {
125                    tracing::warn!("Vsock: Connect attempt {} failed: {err:#?}, retrying...", i + 1)
126                }
127            }
128            // Exponentially backoff before retrying to connect to the socket
129            sleep(Duration::from_secs(1 << i));
130        }
131
132        tracing::error!("Vsock: Connect failed after {max_attempts} attempts");
133        Err(anyhow!("Vsock: Connect failed"))
134    }
135
136    /// Bind to a Vsock socket with a given port and return a vsock handle.
137    ///
138    /// # Arguments
139    ///
140    /// * `port` - Port to bind to.
141    ///
142    /// # Returns
143    ///
144    /// A `Vsock` instance representing the bound socket, or an error if the bind operation fails.
145    pub fn bind(port: u32) -> Result<Self> {
146        let socket_fd = Vsock::socket()?;
147        let sock_addr = VsockAddr::new(Self::ANY_CID_ADDR, port);
148        socket::bind(socket_fd.as_raw_fd(), &sock_addr)
149            .map_err(|err| {
150                tracing::error!("Vsock: Bind failed: {err:#?}");
151                anyhow!(err)
152            })
153            .map(|_| {
154                tracing::debug!("Vsock: Bound to port {port}");
155                Self::new(socket_fd)
156            })
157    }
158
159    /// Listen for incoming connections on a Vsock socket.
160    pub fn listen(&self) -> Result<()> {
161        const MAX_QUEUE_LEN: i32 = 128;
162        socket::listen(&self.as_fd(), Backlog::new(MAX_QUEUE_LEN)?).map_err(|err| {
163            tracing::error!("Vsock: Listen failed: {err:#?}");
164            anyhow!(err)
165        })
166    }
167
168    /// Accept an incoming connection on a Vsock socket and return a new Vsock instance
169    /// representing the accepted connection.
170    pub fn accept(&self) -> Result<Self> {
171        socket::accept(self.as_raw_fd())
172            .map_err(|err| {
173                tracing::error!("Vsock: Accept failed: {err:#?}");
174                anyhow!(err)
175            })
176            .map(|raw_fd| {
177                // Safety: We own the raw fd returned by accept
178                unsafe { Self::new(OwnedFd::from_raw_fd(raw_fd)) }
179            })
180    }
181
182    /// Send a slice of bytes through a Vsock socket. The method makes assumptions about the
183    /// transport chunk size to optimize performance.
184    ///
185    /// # Arguments
186    ///
187    /// * `data` - Slice of bytes to be sent through the Vsock socket entirely.
188    /// * `chunk_size` - Size of each chunk to send through the socket.
189    ///
190    /// # Returns
191    ///
192    /// Empty result indicating success or error if the operation fails.
193    pub fn send(&self, data: &[u8], chunk_size: usize) -> Result<()> {
194        Self::send_loop(data, chunk_size, |chunk| socket::send(self.as_raw_fd(), chunk, MsgFlags::empty()))
195    }
196
197    /// Core send loop parameterized over the underlying transport operation.
198    ///
199    /// Extracted to enable unit testing of the chunking and position-tracking logic without
200    /// a real socket file descriptor. The public `send` method delegates here, passing a
201    /// closure that calls `socket::send`.
202    ///
203    /// # Arguments
204    ///
205    /// * `data` - Slice of bytes to be sent entirely.
206    /// * `chunk_size` - Maximum bytes handed to `transport` per call.
207    /// * `transport` - Called repeatedly with successive slices of `data`; returns the number
208    ///   of bytes consumed, `0` for a remote close, or a [`NixError`] on failure.
209    fn send_loop(
210        data: &[u8], chunk_size: usize, mut transport: impl FnMut(&[u8]) -> StdResult<usize, NixError>,
211    ) -> Result<()> {
212        let mut position = 0;
213        loop {
214            let left = position;
215            let right = left + chunk_size.min(data.len() - left);
216            position += match transport(&data[left..right]) {
217                Ok(0) => {
218                    tracing::warn!("Vsock: Remote closed connection, total bytes sent: {position}");
219                    break Ok(());
220                }
221                Ok(data_len) => {
222                    tracing::trace!("Vsock: Bytes sent: {data_len}");
223                    data_len
224                }
225                // Interrupt signal: non-critical retry
226                Err(NixError::EINTR) => {
227                    tracing::warn!("Vsock: Send interrupted by EINTR, retrying...");
228                    continue;
229                }
230                Err(err) => {
231                    tracing::error!("Vsock: Send failed: {err:#?}");
232                    break Err(anyhow!(err));
233                }
234            };
235            if position == data.len() {
236                tracing::debug!("Vsock: Send completed, total bytes sent: {position}");
237                break Ok(());
238            }
239            if position > data.len() {
240                tracing::error!("Vsock: Send exceeded data length");
241                break Err(anyhow!("Vsock: Send exceeded data length"));
242            }
243        }
244    }
245
246    /// Receive bytes from a Vsock socket. The method reads data in chunks up to the specified
247    /// maximum buffer size. The chunk size can be configured for optimal performance.
248    ///
249    /// # Arguments
250    ///
251    /// * `max_data_size` - Total capacity of the receive buffer in bytes; must be greater than or
252    ///   equal to `chunk_size`.
253    /// * `chunk_size` - Size of each chunk to read from the socket in bytes.
254    ///
255    /// # Returns
256    ///
257    /// A `Vec<u8>` containing the received bytes on success, truncated to the number of bytes
258    /// actually received. Returns an error if `max_data_size` is less than `chunk_size`, if the
259    /// buffer fills completely before the peer closes the connection, or if the underlying socket
260    /// operation fails.
261    pub fn receive(&self, max_data_size: usize, chunk_size: usize) -> Result<Vec<u8>> {
262        if max_data_size < chunk_size {
263            tracing::error!("Vsock: Buffer length less than chunk size: {max_data_size} < {chunk_size}");
264            return Err(anyhow!("Vsock: Buffer too small"));
265        }
266        Self::receive_loop(max_data_size, chunk_size, |buf| socket::recv(self.as_raw_fd(), buf, MsgFlags::empty()))
267    }
268
269    /// Core receive loop parameterized over the underlying transport operation.
270    ///
271    /// Extracted to enable unit testing of the chunking and position-tracking logic without
272    /// a real socket file descriptor. The public `receive` method validates arguments and then
273    /// delegates here, passing a closure that calls `socket::recv`.
274    ///
275    /// # Arguments
276    ///
277    /// * `max_data_size` - Total capacity of the receive buffer; must be `>= chunk_size` (the
278    ///   caller is responsible for enforcing this precondition before calling `receive_loop`).
279    /// * `chunk_size` - Maximum bytes requested from `transport` per call.
280    /// * `transport` - Called repeatedly with a mutable slice of the buffer; returns the number
281    ///   of bytes written, `0` for a remote close, or a [`NixError`] on failure.
282    fn receive_loop(
283        max_data_size: usize, chunk_size: usize, mut transport: impl FnMut(&mut [u8]) -> StdResult<usize, NixError>,
284    ) -> Result<Vec<u8>> {
285        let mut buffer = vec![0u8; max_data_size];
286        let mut position = 0;
287        loop {
288            let left = position;
289            let right = left + chunk_size.min(max_data_size - left);
290            let recv_data_len = match transport(&mut buffer[left..right]) {
291                Ok(0) => {
292                    tracing::warn!("Vsock: Remote closed connection, total bytes received: {position}");
293                    break Ok(buffer[..position].to_vec());
294                }
295                Ok(data_len) => {
296                    tracing::trace!("Vsock: Bytes received: {data_len}");
297                    data_len
298                }
299                // Interrupt signal: non-critical retry
300                Err(NixError::EINTR) => {
301                    tracing::warn!("Vsock: Recv interrupted by EINTR, retrying...");
302                    continue;
303                }
304                Err(err) => {
305                    tracing::error!("Vsock: Recv failed: {err:#?}");
306                    break Err(anyhow!(err));
307                }
308            };
309            position += recv_data_len;
310            if recv_data_len < chunk_size {
311                tracing::debug!("Vsock: Recv completed, total bytes received: {position}");
312                break Ok(buffer[..position].to_vec());
313            }
314            if position >= max_data_size {
315                tracing::error!("Vsock: Recv buffer full");
316                break Err(anyhow!("Vsock: Recv buffer full"));
317            }
318        }
319    }
320}
321
322#[cfg(feature = "std-io")]
323impl Write for Vsock {
324    /// Write a slice of bytes to the Vsock socket. The method assumes that the entire buffer can be sent
325    /// in one call for optimal performance. For larger buffers, the `send` method with chunking should
326    /// be used instead.
327    fn write(&mut self, buf: &[u8]) -> IoResult<usize> {
328        loop {
329            match socket::send(self.as_raw_fd(), buf, MsgFlags::empty()) {
330                Ok(0) => {
331                    tracing::warn!("Vsock: Remote closed connection, total bytes sent: 0");
332                    break Ok(0);
333                }
334                Ok(data_len) => {
335                    tracing::trace!("Vsock: Bytes sent: {data_len}");
336                    break Ok(data_len);
337                }
338                // Interrupt signal: non-critical retry
339                Err(NixError::EINTR) => {
340                    tracing::warn!("Vsock: Send interrupted by EINTR, retrying...");
341                    continue;
342                }
343                Err(errno) => {
344                    tracing::error!("Vsock: Send failed: {errno:#?}");
345                    break Err(IoError::other(errno));
346                }
347            }
348        }
349    }
350
351    /// Shut down the write side of the Vsock socket, signaling EOF to the peer.
352    ///
353    /// This allows the peer's `read_to_end` or `read_to_string` calls to return cleanly.
354    /// After this call, any further attempt to write to the socket will fail.
355    ///
356    /// **Note**: This operation is irreversible — the socket cannot be written to afterwards.
357    fn flush(&mut self) -> IoResult<()> {
358        socket::shutdown(self.as_raw_fd(), Shutdown::Write).map_err(|err| {
359            tracing::error!("Vsock: Shutdown write failed: {err:?}");
360            IoError::other(err)
361        })
362    }
363}
364
365#[cfg(feature = "std-io")]
366impl Read for Vsock {
367    /// Read bytes from the Vsock socket into a provided buffer. The method assumes that the buffer is large
368    /// enough to hold the incoming data for optimal performance. For larger buffers, the `receive` method
369    /// with chunking and data size cap should be used instead.
370    fn read(&mut self, buf: &mut [u8]) -> IoResult<usize> {
371        loop {
372            match socket::recv(self.as_raw_fd(), buf, MsgFlags::empty()) {
373                Ok(0) => {
374                    tracing::warn!("Vsock: Remote closed connection, total bytes received: 0");
375                    break Ok(0);
376                }
377                Ok(data_len) => {
378                    tracing::trace!("Vsock: Bytes received: {data_len}");
379                    break Ok(data_len);
380                }
381                // Interrupt signal: non-critical retry
382                Err(NixError::EINTR) => {
383                    tracing::warn!("Vsock: Recv interrupted by EINTR, retrying...");
384                    continue;
385                }
386                Err(errno) => {
387                    tracing::error!("Vsock: Recv failed: {errno:#?}");
388                    break Err(IoError::other(errno));
389                }
390            }
391        }
392    }
393}
394
395#[cfg(test)]
396mod tests {
397    use super::*;
398    use nix::errno::Errno;
399
400    // ── receive() input validation ────────────────────────────────────────────
401
402    // The validation check in `receive` is pure Rust and runs before any vsock
403    // syscall. We can reach it by wrapping a real (but non-vsock) OS file
404    // descriptor obtained from `nix::unistd::pipe`, which is always available.
405    // The `Vsock` wrapper is dropped at the end of each test, closing its fd.
406    fn make_pipe_vsock() -> Vsock {
407        use nix::unistd::pipe;
408        // `pipe()` returns (read_end, write_end); we use the read end.
409        let (read_fd, write_fd) = pipe().expect("pipe() failed in test");
410        // Explicitly drop the write end so it doesn't leak.
411        drop(write_fd);
412        Vsock::new(read_fd)
413    }
414
415    #[test]
416    #[should_panic(expected = "Buffer too small")]
417    fn receive_returns_error_when_max_size_less_than_chunk_size_fail() {
418        const MAX_DATA_SIZE: usize = 16;
419        const CHUNK_SIZE: usize = 32;
420        let vsock = make_pipe_vsock();
421        vsock.receive(MAX_DATA_SIZE, CHUNK_SIZE).unwrap();
422    }
423
424    #[test]
425    #[should_panic(expected = "Buffer too small")]
426    fn receive_returns_error_when_max_size_equals_zero_and_chunk_size_nonzero_fail() {
427        const MAX_DATA_SIZE: usize = 0;
428        const MIN_NONZERO_CHUNK_SIZE: usize = 1;
429        let vsock = make_pipe_vsock();
430        vsock.receive(MAX_DATA_SIZE, MIN_NONZERO_CHUNK_SIZE).unwrap();
431    }
432
433    #[test]
434    #[should_panic(expected = "Buffer too small")]
435    fn receive_returns_error_when_max_size_one_less_than_chunk_size_fail() {
436        const CHUNK_SIZE: usize = 1024;
437        const MAX_DATA_SIZE: usize = CHUNK_SIZE - 1;
438        let vsock = make_pipe_vsock();
439        vsock.receive(MAX_DATA_SIZE, CHUNK_SIZE).unwrap();
440    }
441
442    // ── connect_with_max_attempts() zero-iteration path ───────────────────────
443
444    #[test]
445    #[should_panic(expected = "Connect failed")]
446    fn connect_with_zero_attempts_returns_error_immediately_fail() {
447        const PORT: u32 = 12345;
448        Vsock::connect_with_max_attempts(u32::MAX, PORT, 0).unwrap();
449    }
450
451    // ── send_loop() unit tests (fake transport) ───────────────────────────────
452
453    #[test]
454    fn send_loop_delivers_entire_payload_in_one_chunk_ok() {
455        const CHUNK_SIZE: usize = 64;
456        let data = b"hello";
457        let mut received: Vec<u8> = Vec::new();
458        Vsock::send_loop(data, CHUNK_SIZE, |chunk| {
459            received.extend_from_slice(chunk);
460            Ok(chunk.len())
461        })
462        .unwrap();
463        assert_eq!(received, data);
464    }
465
466    #[test]
467    fn send_loop_splits_payload_across_multiple_chunks_ok() {
468        const CHUNK_SIZE: usize = 3;
469        const PAYLOAD_LEN: u8 = 10;
470        let data: Vec<u8> = (0u8..PAYLOAD_LEN).collect();
471        let mut calls: Vec<Vec<u8>> = Vec::new();
472        Vsock::send_loop(&data, CHUNK_SIZE, |chunk| {
473            calls.push(chunk.to_vec());
474            Ok(chunk.len())
475        })
476        .unwrap();
477        // 10 bytes / chunk_size 3  →  slices of 3, 3, 3, 1
478        assert_eq!(calls.len(), 4);
479        assert_eq!(calls[0], &data[0..3]);
480        assert_eq!(calls[1], &data[3..6]);
481        assert_eq!(calls[2], &data[6..9]);
482        assert_eq!(calls[3], &data[9..10]);
483    }
484
485    #[test]
486    fn send_loop_retries_on_eintr_and_still_completes_ok() {
487        const CHUNK_SIZE: usize = 64;
488        let data = b"retry";
489        let mut call_count = 0usize;
490        // Return EINTR on the first call, succeed on the second.
491        Vsock::send_loop(data, CHUNK_SIZE, |chunk| {
492            call_count += 1;
493            if call_count == 1 {
494                Err(Errno::EINTR)
495            } else {
496                Ok(chunk.len())
497            }
498        })
499        .unwrap();
500        assert_eq!(call_count, 2);
501    }
502
503    #[test]
504    fn send_loop_stops_cleanly_when_transport_returns_zero_ok() {
505        const CHUNK_SIZE: usize = 64;
506        let data = b"goodbye";
507        let mut call_count = 0usize;
508        Vsock::send_loop(data, CHUNK_SIZE, |_| {
509            call_count += 1;
510            Ok(0) // peer closed
511        })
512        .unwrap();
513        // A zero-byte send is treated as a graceful close, not an error.
514        assert_eq!(call_count, 1);
515    }
516
517    #[test]
518    #[should_panic(expected = "ECONNRESET")]
519    fn send_loop_propagates_transport_error_fail() {
520        const CHUNK_SIZE: usize = 64;
521        let data = b"oops";
522        Vsock::send_loop(data, CHUNK_SIZE, |_| Err(Errno::ECONNRESET)).unwrap();
523    }
524
525    #[test]
526    fn send_loop_with_empty_data_calls_transport_once_with_empty_slice_ok() {
527        // When data is empty: data.len() == 0, so left == right == 0 on the
528        // first (and only) iteration. The transport receives &[] and returns
529        // Ok(0), which triggers the "remote closed" branch — Ok(()) is returned.
530        const CHUNK_SIZE: usize = 8;
531        let data: &[u8] = b"";
532        let mut call_count = 0usize;
533        Vsock::send_loop(data, CHUNK_SIZE, |chunk| {
534            call_count += 1;
535            assert!(chunk.is_empty(), "transport must receive an empty slice");
536            Ok(chunk.len()) // 0 → remote-closed branch
537        })
538        .unwrap();
539        assert_eq!(call_count, 1);
540    }
541
542    // ── receive_loop() unit tests (fake transport) ────────────────────────────
543
544    #[test]
545    fn receive_loop_collects_single_partial_chunk_on_short_read_ok() {
546        // A `recv_data_len < chunk_size` read signals end-of-stream.
547        const MAX_DATA_SIZE: usize = 64;
548        const CHUNK_SIZE: usize = 16;
549        let payload = b"hi";
550        let result = Vsock::receive_loop(MAX_DATA_SIZE, CHUNK_SIZE, |buf| {
551            let n = payload.len().min(buf.len());
552            buf[..n].copy_from_slice(&payload[..n]);
553            Ok(n) // n < chunk_size → loop exits
554        });
555        assert_eq!(result.unwrap(), payload);
556    }
557
558    #[test]
559    fn receive_loop_reassembles_multiple_full_chunks_followed_by_short_read_ok() {
560        // Three full chunks of 4 bytes each, then a short final read.
561        const MAX_DATA_SIZE: usize = 64;
562        const CHUNK_SIZE: usize = 4;
563        const PAYLOAD_LEN: u8 = 13;
564        let payload: Vec<u8> = (0u8..PAYLOAD_LEN).collect();
565        let mut pos = 0usize;
566        let result = Vsock::receive_loop(MAX_DATA_SIZE, CHUNK_SIZE, |buf| {
567            let remaining = payload.len() - pos;
568            let n = remaining.min(buf.len());
569            buf[..n].copy_from_slice(&payload[pos..pos + n]);
570            pos += n;
571            Ok(n)
572        });
573        assert_eq!(result.unwrap(), payload);
574    }
575
576    #[test]
577    fn receive_loop_stops_cleanly_on_remote_close_returning_zero_ok() {
578        const MAX_DATA_SIZE: usize = 64;
579        const CHUNK_SIZE: usize = 8;
580        let result = Vsock::receive_loop(MAX_DATA_SIZE, CHUNK_SIZE, |_| {
581            Ok(0) // Zero bytes received before remote closed → empty vec.
582        });
583        assert_eq!(result.unwrap(), b"");
584    }
585
586    #[test]
587    fn receive_loop_retries_on_eintr_and_still_completes_ok() {
588        const MAX_DATA_SIZE: usize = 64;
589        const CHUNK_SIZE: usize = 16;
590        let payload = b"interrupt";
591        let mut call_count = 0usize;
592        let result = Vsock::receive_loop(MAX_DATA_SIZE, CHUNK_SIZE, |buf| {
593            call_count += 1;
594            if call_count == 1 {
595                Err(Errno::EINTR)
596            } else {
597                let n = payload.len().min(buf.len());
598                buf[..n].copy_from_slice(&payload[..n]);
599                Ok(n)
600            }
601        });
602        let result = result.unwrap();
603        assert_eq!(call_count, 2);
604        assert_eq!(result, payload);
605    }
606
607    #[test]
608    #[should_panic(expected = "ECONNRESET")]
609    fn receive_loop_returns_error_when_transport_fails_fail() {
610        const MAX_DATA_SIZE: usize = 64;
611        const CHUNK_SIZE: usize = 8;
612        Vsock::receive_loop(MAX_DATA_SIZE, CHUNK_SIZE, |_| Err(Errno::ECONNRESET)).unwrap();
613    }
614
615    #[test]
616    #[should_panic(expected = "Recv buffer full")]
617    fn receive_loop_returns_buffer_full_error_when_capacity_exhausted_fail() {
618        // Transport always returns a full chunk, so position will reach max_data_size.
619        let max_data_size = 8;
620        let chunk_size = 4;
621        Vsock::receive_loop(max_data_size, chunk_size, |buf| Ok(buf.len())).unwrap();
622    }
623
624    #[test]
625    fn receive_loop_last_chunk_is_clamped_to_remaining_capacity_ok() {
626        // max_data_size = 10, chunk_size = 4 → chunks of 4, 4, 2.
627        // Verify the final slice handed to transport has length 2, not 4.
628        let max_data_size = 10usize;
629        let chunk_size = 4usize;
630        let mut chunk_lengths: Vec<usize> = Vec::new();
631        // Transport fills each chunk fully (simulating a full read) except the last
632        // where the slice is already smaller than chunk_size, so the loop exits.
633        let result = Vsock::receive_loop(max_data_size, chunk_size, |buf| {
634            chunk_lengths.push(buf.len());
635            Ok(buf.len())
636        });
637        // After two full chunks (8 bytes), the third window is 2 bytes — shorter
638        // than chunk_size — so the loop detects buffer full and returns an error
639        // (position 8 >= max_data_size 10 is false after chunk 2; the third call
640        // fills 2 bytes returning 2 < 4, so the short-read branch fires first).
641        // What matters for this test: the third chunk is 2 bytes, not 4.
642        assert!(chunk_lengths.len() >= 3);
643        assert_eq!(chunk_lengths[2], 2);
644        // Result is Ok because the short-read branch fires before buffer-full.
645        result.unwrap();
646    }
647}