diff --git a/src/devices/src/virtio/net/unixgram.rs b/src/devices/src/virtio/net/unixgram.rs index 600b6b41b..300aae4e9 100644 --- a/src/devices/src/virtio/net/unixgram.rs +++ b/src/devices/src/virtio/net/unixgram.rs @@ -6,6 +6,8 @@ use nix::sys::socket::{ use nix::unistd::unlink; use std::os::fd::{AsRawFd, OwnedFd, RawFd}; use std::path::PathBuf; +use std::process; +use std::sync::atomic::{AtomicU32, Ordering}; use super::backend::{ConnectError, NetBackend, ReadError, WriteError}; use super::write_virtio_net_hdr; @@ -13,6 +15,15 @@ use super::write_virtio_net_hdr; use super::{MAX_BUFFER_SIZE, VNET_HDR_LEN}; const VFKIT_MAGIC: [u8; 4] = *b"VFKT"; + +/// Per-process counter to generate unique local unixgram socket filenames. +/// +/// The local socket is placed in the same directory as the peer using a short +/// PID+counter name. The peer filename always contains the machine name, so it +/// is longer than our fixed-format name for any reasonably-named machine, keeping +/// the local path within macOS's 104-byte unix socket limit. +static NET_SOCK_COUNTER: AtomicU32 = AtomicU32::new(0); + const DEFAULT_SOCKET_BUF_SIZE: usize = 7 * 1024 * 1024; // On macOS, with UNIX datagram sockets the send buffer is not used for queuing; @@ -76,8 +87,16 @@ impl Unixgram { ) .map_err(ConnectError::CreateSocket)?; let peer_addr = UnixAddr::new(&path).map_err(ConnectError::InvalidAddress)?; - let local_addr = UnixAddr::new(&PathBuf::from(format!("{}-krun.sock", path.display()))) - .map_err(ConnectError::InvalidAddress)?; + let socket_name = format!( + "krun-net-{}-{}.sock", + process::id(), + NET_SOCK_COUNTER.fetch_add(1, Ordering::Relaxed), + ); + let local_path = path + .parent() + .map(|dir| dir.join(&socket_name)) + .unwrap_or_else(|| std::env::temp_dir().join(&socket_name)); + let local_addr = UnixAddr::new(&local_path).map_err(ConnectError::InvalidAddress)?; if let Some(path) = local_addr.path() { _ = unlink(path); } diff --git a/tests/test_cases/src/lib.rs b/tests/test_cases/src/lib.rs index ee24b0d05..614e741e8 100644 --- a/tests/test_cases/src/lib.rs +++ b/tests/test_cases/src/lib.rs @@ -105,6 +105,10 @@ pub fn test_cases() -> Vec { TestCase::new("net-passt", Box::new(TestNet::new_passt())), TestCase::new("net-tap", Box::new(TestNet::new_tap())), TestCase::new("net-gvproxy", Box::new(TestNet::new_gvproxy())), + TestCase::new( + "net-gvproxy-long-path", + Box::new(TestNet::new_gvproxy_long_path()), + ), TestCase::new("net-vmnet-helper", Box::new(TestNet::new_vmnet_helper())), TestCase::new("multiport-console", Box::new(TestMultiportConsole)), #[cfg(any(feature = "host", target_os = "linux"))] diff --git a/tests/test_cases/src/test_net/gvproxy.rs b/tests/test_cases/src/test_net/gvproxy.rs index 2ed0cae74..bfb50aa54 100644 --- a/tests/test_cases/src/test_net/gvproxy.rs +++ b/tests/test_cases/src/test_net/gvproxy.rs @@ -135,13 +135,18 @@ pub(crate) fn should_run() -> ShouldRun { } } -pub(crate) fn setup_backend(ctx: u32, test_setup: &TestSetup) -> anyhow::Result<()> { +fn setup_backend_with_socket( + ctx: u32, + test_setup: &TestSetup, + socket_name: &str, + log_name: &str, +) -> anyhow::Result<()> { let tmp_dir = test_setup .tmp_dir .canonicalize() .unwrap_or_else(|_| test_setup.tmp_dir.clone()); - let socket_path = tmp_dir.join("gvproxy.sock"); - let gvproxy_log = tmp_dir.join("gvproxy.log"); + let socket_path = tmp_dir.join(socket_name); + let gvproxy_log = tmp_dir.join(log_name); let socket_path_str = socket_path .to_str() @@ -169,3 +174,33 @@ pub(crate) fn setup_backend(ctx: u32, test_setup: &TestSetup) -> anyhow::Result< } Ok(()) } + +pub(crate) fn setup_backend(ctx: u32, test_setup: &TestSetup) -> anyhow::Result<()> { + setup_backend_with_socket(ctx, test_setup, "gvproxy.sock", "gvproxy.log") +} + +/// Backend setup with a peer socket path long enough to have previously +/// triggered ENAMETOOLONG on macOS when the local bind address was derived +/// from the peer path by appending a suffix. +pub(crate) fn setup_backend_long_path(ctx: u32, test_setup: &TestSetup) -> anyhow::Result<()> { + // Build a peer socket filename so that the full path approaches the + // 104-byte macOS unix socket limit. Use base_len measured at runtime so + // the padding is correct regardless of the exact tmp_dir length. + // tmp_dir is typically "/tmp/libkrun-tests.XXXXXXXX" (~27 chars), or + // "/private/tmp/libkrun-tests.XXXXXXXX" (~35 chars) after canonicalize on macOS. + let tmp_dir = test_setup + .tmp_dir + .canonicalize() + .unwrap_or_else(|_| test_setup.tmp_dir.clone()); + let base_len = tmp_dir.to_str().map(|s| s.len()).unwrap_or(0); + const TARGET_PATH_LEN: usize = 96; + let prefix = "gvp-"; + let suffix = ".sock"; + let name_needed = TARGET_PATH_LEN.saturating_sub(base_len + 1); + let pad_len = name_needed + .saturating_sub(prefix.len() + suffix.len()) + .max(1); + let socket_name = format!("{}{}{}", prefix, "x".repeat(pad_len), suffix); + + setup_backend_with_socket(ctx, test_setup, &socket_name, "gvproxy-long-path.log") +} diff --git a/tests/test_cases/src/test_net/mod.rs b/tests/test_cases/src/test_net/mod.rs index 6fcb0559a..2066dc944 100644 --- a/tests/test_cases/src/test_net/mod.rs +++ b/tests/test_cases/src/test_net/mod.rs @@ -84,6 +84,20 @@ impl TestNet { cleanup: None, } } + + /// Gvproxy backend variant with a socket path ≥ 96 bytes, triggering the + /// ENAMETOOLONG bug when the local socket was derived from the peer path. + pub fn new_gvproxy_long_path() -> Self { + Self { + tcp_tester: TcpTester::new([192, 168, 127, 254].into(), 9004), + #[cfg(feature = "host")] + should_run: gvproxy::should_run, + #[cfg(feature = "host")] + setup_backend: gvproxy::setup_backend_long_path, + #[cfg(feature = "host")] + cleanup: None, + } + } } #[host]