From 04ff278003e139c7be2ba3ce4b3cee7c16542759 Mon Sep 17 00:00:00 2001 From: Emil Lindqvist Date: Sat, 16 May 2026 17:00:19 +0200 Subject: [PATCH 1/4] feat(examples): add vsock_server regression test This relates to hermit-os/kernel#2433 --- Cargo.toml | 1 + examples/vsock_server/Cargo.toml | 20 +++ examples/vsock_server/src/main.rs | 42 +++++ examples/vsock_server/src/vsock.rs | 267 +++++++++++++++++++++++++++++ 4 files changed, 330 insertions(+) create mode 100644 examples/vsock_server/Cargo.toml create mode 100644 examples/vsock_server/src/main.rs create mode 100644 examples/vsock_server/src/vsock.rs diff --git a/Cargo.toml b/Cargo.toml index 3cb7d25b0a..9bec3aa135 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ members = [ "examples/wasm-test", "examples/hermit-wasm", "examples/vsock", + "examples/vsock_server", "hermit", "hermit-abi", ] diff --git a/examples/vsock_server/Cargo.toml b/examples/vsock_server/Cargo.toml new file mode 100644 index 0000000000..97330a6125 --- /dev/null +++ b/examples/vsock_server/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "vsock_server" +version = "0.1.0" +edition = "2021" + +[dependencies] + +[target.'cfg(target_os = "hermit")'.dependencies.hermit-abi] +path = "../../hermit-abi" + +[target.'cfg(target_os = "hermit")'.dependencies.hermit] +path = "../../hermit" +default-features = false + +[features] +default = ["pci", "pci-ids", "acpi", "vsock"] +pci = ["hermit/pci"] +pci-ids = ["hermit/pci-ids"] +acpi = ["hermit/acpi"] +vsock = ["hermit/vsock"] diff --git a/examples/vsock_server/src/main.rs b/examples/vsock_server/src/main.rs new file mode 100644 index 0000000000..ca109ed299 --- /dev/null +++ b/examples/vsock_server/src/main.rs @@ -0,0 +1,42 @@ +//! Regression test for hermit-os/kernel#2433: +//! vsock listener cannot accept a second connection. +//! +//! The VM runs a server that accepts CONNECTIONS sequential connections, +//! reads "ping", writes "pong", and closes each one. The host-side client +//! is driven by xtask (see xtask/src/ci/qemu.rs test_vsock_server). +use std::io::{Read, Write}; + +#[cfg(target_os = "hermit")] +use hermit as _; + +mod vsock; + +use vsock::VsockListener; + +const PORT: u32 = 9975; +pub const CONNECTIONS: usize = 2; + +fn main() { + println!("vsock_server_test: waiting for {CONNECTIONS} sequential connections on port {PORT}"); + + let listener = VsockListener::bind(PORT).expect("bind failed"); + println!("[server] listening on port {PORT}"); + + for i in 1..=CONNECTIONS { + println!("[server] waiting for connection {i}/{CONNECTIONS}"); + let (mut stream, _addr) = listener.accept().expect("accept failed"); + println!("[server] accepted connection {i}"); + + let mut buf = [0u8; 64]; + let n = stream.read(&mut buf).expect("read failed"); + let msg = std::str::from_utf8(&buf[..n]).unwrap_or(""); + println!("[server] received: {msg:?}"); + assert_eq!(msg, "ping", "connection {i}: unexpected message"); + + stream.write_all(b"pong").expect("write failed"); + println!("[server] sent pong for connection {i}"); + // stream drops here, closing the connection + } + + println!("vsock_server_test: PASSED"); +} diff --git a/examples/vsock_server/src/vsock.rs b/examples/vsock_server/src/vsock.rs new file mode 100644 index 0000000000..668ab0cd08 --- /dev/null +++ b/examples/vsock_server/src/vsock.rs @@ -0,0 +1,267 @@ +#![allow(dead_code)] + +/// Virtio socket support for Rust. Implements VsockListener and VsockStream +/// which are analogous to the `std::net::TcpListener` and `std::net::TcpStream` +/// types. +/// +/// The implementation is derived from https://github.com/rust-vsock/vsock-rs +/// and adapted for HermitOS. +use std::io::{self, Read, Result, Write}; +use std::mem::size_of; +use std::os::fd::AsRawFd; +#[cfg(target_os = "hermit")] +use std::os::hermit::io::{FromRawFd, OwnedFd, RawFd}; +#[cfg(unix)] +use std::os::unix::io::{FromRawFd, OwnedFd, RawFd}; + +#[cfg(target_os = "hermit")] +use hermit_abi::{ + accept, bind, close, connect, listen, read, sa_family_t, sockaddr, sockaddr_vm, socket, + socklen_t, write, AF_VSOCK, SOCK_STREAM, VMADDR_CID_ANY, +}; +#[cfg(unix)] +use libc::{ + accept, bind, c_void, close, connect, listen, read, sa_family_t, sockaddr, sockaddr_vm, socket, + socklen_t, write, AF_VSOCK, SOCK_STREAM, VMADDR_CID_ANY, +}; + +#[derive(Copy, Clone)] +#[repr(transparent)] +pub struct VsockAddr(pub sockaddr_vm); + +impl VsockAddr { + pub fn new(cid: u32, port: u32) -> Self { + #[cfg(target_os = "hermit")] + let vsock_addr_len: socklen_t = size_of::().try_into().unwrap(); + let vsock_addr = sockaddr_vm { + #[cfg(target_os = "hermit")] + svm_len: vsock_addr_len.try_into().unwrap(), + svm_reserved1: 0, + svm_family: AF_VSOCK as sa_family_t, + svm_cid: cid, + svm_port: port, + svm_zero: [0; 4], + }; + + Self(vsock_addr) + } +} + +#[doc(hidden)] +pub trait IsNegative { + fn is_negative(&self) -> bool; + #[allow(dead_code)] + fn negate(&self) -> i32; +} + +macro_rules! impl_is_negative { + ($($t:ident)*) => ($(impl IsNegative for $t { + fn is_negative(&self) -> bool { + *self < 0 + } + + fn negate(&self) -> i32 { + i32::try_from(-(*self)).unwrap() + } + })*) +} + +impl IsNegative for i32 { + fn is_negative(&self) -> bool { + *self < 0 + } + + fn negate(&self) -> i32 { + -(*self) + } +} +impl_is_negative! { i8 i16 i64 isize } + +#[cfg(unix)] +fn check(res: T) -> io::Result { + if res.is_negative() { + Err(std::io::Error::last_os_error()) + } else { + Ok(res) + } +} + +#[cfg(target_os = "hermit")] +fn check + std::cmp::PartialOrd + IsNegative>( + res: T, +) -> io::Result { + if res.is_negative() { + let e = match res.negate() { + hermit_abi::errno::EACCES => std::io::ErrorKind::PermissionDenied, + hermit_abi::errno::EADDRINUSE => std::io::ErrorKind::AddrInUse, + hermit_abi::errno::EADDRNOTAVAIL => std::io::ErrorKind::AddrNotAvailable, + hermit_abi::errno::EAGAIN => std::io::ErrorKind::WouldBlock, + hermit_abi::errno::ECONNABORTED => std::io::ErrorKind::ConnectionAborted, + hermit_abi::errno::ECONNREFUSED => std::io::ErrorKind::ConnectionRefused, + hermit_abi::errno::ECONNRESET => std::io::ErrorKind::ConnectionReset, + hermit_abi::errno::EEXIST => std::io::ErrorKind::AlreadyExists, + hermit_abi::errno::EINTR => std::io::ErrorKind::Interrupted, + hermit_abi::errno::EINVAL => std::io::ErrorKind::InvalidInput, + hermit_abi::errno::ENOENT => std::io::ErrorKind::NotFound, + hermit_abi::errno::ENOTCONN => std::io::ErrorKind::NotConnected, + hermit_abi::errno::EPERM => std::io::ErrorKind::PermissionDenied, + hermit_abi::errno::EPIPE => std::io::ErrorKind::BrokenPipe, + hermit_abi::errno::ETIMEDOUT => std::io::ErrorKind::TimedOut, + _ => { + println!("Unknown error number {}", res.negate()); + std::io::ErrorKind::InvalidInput + } + }; + Err(std::io::Error::from(e)) + } else { + Ok(res) + } +} + +/// A virtio socket server, listening for connections. +#[derive(Debug)] +pub struct VsockListener { + fd: OwnedFd, +} + +impl VsockListener { + /// Create a new VsockListener which is bound and listening on the socket address. + pub fn bind(port: u32) -> io::Result { + unsafe { + let saddr = sockaddr_vm { + #[cfg(target_os = "hermit")] + svm_len: std::mem::size_of::().try_into().unwrap(), + svm_reserved1: 0, + svm_family: AF_VSOCK.try_into().unwrap(), + svm_cid: VMADDR_CID_ANY, + svm_port: port, + svm_zero: [0; 4], + }; + let fd = socket(AF_VSOCK, SOCK_STREAM, 0); + + check(bind( + fd, + &saddr as *const _ as *const sockaddr, + std::mem::size_of::().try_into().unwrap(), + ))?; + + // rust stdlib uses a 128 connection backlog + check(listen(fd, 128))?; + + Ok(VsockListener { + fd: OwnedFd::from_raw_fd(fd), + }) + } + } + + /// Accept a new incoming connection from this listener. + pub fn accept(&self) -> io::Result<(VsockStream, VsockAddr)> { + let mut vsock_addr_len: socklen_t = size_of::().try_into().unwrap(); + let mut vsock_addr = sockaddr_vm { + #[cfg(target_os = "hermit")] + svm_len: vsock_addr_len.try_into().unwrap(), + svm_reserved1: 0, + svm_family: AF_VSOCK as sa_family_t, + svm_cid: 0, + svm_port: 0, + svm_zero: [0; 4], + }; + + let fd = unsafe { + check(accept( + self.fd.as_raw_fd(), + &mut vsock_addr as *mut _ as *mut sockaddr, + &mut vsock_addr_len as *mut u32, + ))? + }; + + Ok((VsockStream::new(fd), VsockAddr(vsock_addr))) + } +} + +impl Drop for VsockListener { + fn drop(&mut self) { + unsafe { + let _ = close(self.fd.as_raw_fd()); + } + } +} + +pub struct VsockStream { + fd: OwnedFd, +} + +impl VsockStream { + pub fn new(fd: RawFd) -> Self { + Self { + fd: unsafe { FromRawFd::from_raw_fd(fd) }, + } + } + + pub fn connect(addr: VsockAddr) -> io::Result { + let len: socklen_t = size_of::().try_into().unwrap(); + let fd = unsafe { socket(AF_VSOCK, SOCK_STREAM, 0) }; + + unsafe { + check(connect( + fd.as_raw_fd(), + &addr.0 as *const _ as *const sockaddr, + len, + ))? + }; + + Ok(VsockStream::new(fd)) + } +} + +impl Read for VsockStream { + #[cfg(target_os = "hermit")] + fn read(&mut self, buf: &mut [u8]) -> Result { + let result = unsafe { check(read(self.fd.as_raw_fd(), buf.as_mut_ptr(), buf.len()))? }; + Ok(result.try_into().unwrap()) + } + + #[cfg(unix)] + fn read(&mut self, buf: &mut [u8]) -> Result { + let result = unsafe { + check(read( + self.fd.as_raw_fd(), + buf.as_mut_ptr() as *mut c_void, + buf.len(), + ))? + }; + Ok(result.try_into().unwrap()) + } +} + +impl Write for VsockStream { + #[cfg(target_os = "hermit")] + fn write(&mut self, buf: &[u8]) -> Result { + let result = unsafe { check(write(self.fd.as_raw_fd(), buf.as_ptr(), buf.len()))? }; + Ok(result.try_into().unwrap()) + } + + #[cfg(unix)] + fn write(&mut self, buf: &[u8]) -> Result { + let result: isize = unsafe { + check(write( + self.fd.as_raw_fd(), + buf.as_ptr() as *const c_void, + buf.len(), + ))? + }; + Ok(result.try_into().unwrap()) + } + + fn flush(&mut self) -> Result<()> { + Ok(()) + } +} + +impl Drop for VsockStream { + fn drop(&mut self) { + unsafe { + let _ = close(self.fd.as_raw_fd()); + } + } +} From 0d2b1490b01c28de95a141f2b055c2320a84b17f Mon Sep 17 00:00:00 2001 From: Emil Lindqvist Date: Sat, 16 May 2026 22:11:29 +0200 Subject: [PATCH 2/4] fix(examples): Fix formatting --- examples/vsock_server/src/main.rs | 34 +++++++++++++++---------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/examples/vsock_server/src/main.rs b/examples/vsock_server/src/main.rs index ca109ed299..a4237d2216 100644 --- a/examples/vsock_server/src/main.rs +++ b/examples/vsock_server/src/main.rs @@ -17,26 +17,26 @@ const PORT: u32 = 9975; pub const CONNECTIONS: usize = 2; fn main() { - println!("vsock_server_test: waiting for {CONNECTIONS} sequential connections on port {PORT}"); + println!("vsock_server_test: waiting for {CONNECTIONS} sequential connections on port {PORT}"); - let listener = VsockListener::bind(PORT).expect("bind failed"); - println!("[server] listening on port {PORT}"); + let listener = VsockListener::bind(PORT).expect("bind failed"); + println!("[server] listening on port {PORT}"); - for i in 1..=CONNECTIONS { - println!("[server] waiting for connection {i}/{CONNECTIONS}"); - let (mut stream, _addr) = listener.accept().expect("accept failed"); - println!("[server] accepted connection {i}"); + for i in 1..=CONNECTIONS { + println!("[server] waiting for connection {i}/{CONNECTIONS}"); + let (mut stream, _addr) = listener.accept().expect("accept failed"); + println!("[server] accepted connection {i}"); - let mut buf = [0u8; 64]; - let n = stream.read(&mut buf).expect("read failed"); - let msg = std::str::from_utf8(&buf[..n]).unwrap_or(""); - println!("[server] received: {msg:?}"); - assert_eq!(msg, "ping", "connection {i}: unexpected message"); + let mut buf = [0u8; 64]; + let n = stream.read(&mut buf).expect("read failed"); + let msg = std::str::from_utf8(&buf[..n]).unwrap_or(""); + println!("[server] received: {msg:?}"); + assert_eq!(msg, "ping", "connection {i}: unexpected message"); - stream.write_all(b"pong").expect("write failed"); - println!("[server] sent pong for connection {i}"); - // stream drops here, closing the connection - } + stream.write_all(b"pong").expect("write failed"); + println!("[server] sent pong for connection {i}"); + // stream drops here, closing the connection + } - println!("vsock_server_test: PASSED"); + println!("vsock_server_test: PASSED"); } From 0a7b10f25847fee3fad9842e74d027e56b558437 Mon Sep 17 00:00:00 2001 From: Emil Lindqvist Date: Sat, 16 May 2026 22:12:53 +0200 Subject: [PATCH 3/4] Add Cargo.lock --- Cargo.lock | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 9815ad1fb0..0724823907 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2797,6 +2797,14 @@ dependencies = [ "libc", ] +[[package]] +name = "vsock_server" +version = "0.1.0" +dependencies = [ + "hermit", + "hermit-abi 0.5.2", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" From b79610d141acb2003e560275e47162ecd05261de Mon Sep 17 00:00:00 2001 From: Emil Lindqvist Date: Sat, 16 May 2026 22:16:29 +0200 Subject: [PATCH 4/4] fix(examples): Add missing libc dependency --- Cargo.lock | 1 + examples/vsock_server/Cargo.toml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 0724823907..e7205a7a4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2803,6 +2803,7 @@ version = "0.1.0" dependencies = [ "hermit", "hermit-abi 0.5.2", + "libc", ] [[package]] diff --git a/examples/vsock_server/Cargo.toml b/examples/vsock_server/Cargo.toml index 97330a6125..dcc0bd0130 100644 --- a/examples/vsock_server/Cargo.toml +++ b/examples/vsock_server/Cargo.toml @@ -5,6 +5,9 @@ edition = "2021" [dependencies] +[target.'cfg(unix)'.dependencies] +libc = { version = "0.2" } + [target.'cfg(target_os = "hermit")'.dependencies.hermit-abi] path = "../../hermit-abi"