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}