From e11b37388275a5c23906175308f8b2a68b3ecd4a Mon Sep 17 00:00:00 2001 From: Vrinda Kumar Date: Mon, 20 Jul 2026 16:48:14 +0530 Subject: [PATCH] fix: report actionable diagnostic for overlong unix socket paths --- src/ipc.rs | 75 ++++++++++++++++++++++++++++++++++++++ src/server/autodetect.rs | 17 +++++++++ src/server/socket_paths.rs | 13 +++++++ 3 files changed, 105 insertions(+) diff --git a/src/ipc.rs b/src/ipc.rs index e8a673d0..bf6b58fc 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -27,6 +27,8 @@ pub(crate) struct SocketFileIdentity { } pub(crate) fn connect_local_stream(path: &Path) -> io::Result { + validate_socket_path(path)?; + #[cfg(unix)] { use interprocess::local_socket::{prelude::*, GenericFilePath}; @@ -46,6 +48,8 @@ pub(crate) fn connect_local_stream(path: &Path) -> io::Result { } pub(crate) fn bind_local_listener(path: &Path) -> io::Result { + validate_socket_path(path)?; + #[cfg(unix)] { use interprocess::local_socket::{prelude::*, GenericFilePath, ListenerOptions}; @@ -76,6 +80,8 @@ pub(crate) fn prepare_socket_path( path: &Path, busy_message: impl FnOnce(&Path) -> String, ) -> io::Result<()> { + validate_socket_path(path)?; + if let Some(parent) = path.parent() { fs::create_dir_all(parent)?; } @@ -273,6 +279,42 @@ fn windows_socket_marker() -> String { format!("{}:{now}", std::process::id()) } +#[cfg(unix)] +pub(crate) fn max_unix_socket_path_len() -> usize { + let un: libc::sockaddr_un = unsafe { std::mem::zeroed() }; + std::mem::size_of_val(&un.sun_path) - 1 +} + +pub(crate) fn validate_socket_path_with_limit(path: &Path, max_len: usize) -> io::Result<()> { + #[cfg(unix)] + let len = std::os::unix::ffi::OsStrExt::as_bytes(path.as_os_str()).len(); + #[cfg(not(unix))] + let len = path.to_string_lossy().len(); + + if len > max_len { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!( + "socket path exceeds Unix domain socket limit ({len} bytes > {max_len} bytes): {}; set a shorter NAGI_CONFIG_PATH or XDG_CONFIG_HOME", + path.display() + ), + )); + } + Ok(()) +} + +pub(crate) fn validate_socket_path(path: &Path) -> io::Result<()> { + #[cfg(unix)] + { + validate_socket_path_with_limit(path, max_unix_socket_path_len()) + } + #[cfg(not(unix))] + { + let _ = path; + Ok(()) + } +} + #[cfg(unix)] pub(crate) fn restrict_socket_permissions(path: &Path, mode: u32) -> io::Result<()> { let mut permissions = fs::metadata(path)?.permissions(); @@ -293,6 +335,39 @@ mod tests { #[cfg(windows)] use std::path::PathBuf; + #[test] + fn validate_socket_path_with_limit_rejects_overlong_path() { + let path = Path::new("/tmp/some/very/long/nested/path/that/exceeds/the/unix/domain/socket/limit/nagi.sock"); + let err = validate_socket_path_with_limit(path, 103).unwrap_err(); + let message = err.to_string(); + + assert_eq!(err.kind(), io::ErrorKind::InvalidInput); + assert!(message.contains("socket path exceeds Unix domain socket limit")); + assert!(message.contains(path.to_str().unwrap())); + assert!(message.contains("set a shorter NAGI_CONFIG_PATH or XDG_CONFIG_HOME")); + } + + #[test] + fn validate_socket_path_with_limit_accepts_short_path() { + let path = Path::new("/tmp/nagi.sock"); + assert!(validate_socket_path_with_limit(path, 103).is_ok()); + } + + #[cfg(unix)] + #[test] + fn validate_socket_path_enforces_platform_limit() { + let max_len = max_unix_socket_path_len(); + let valid_path_str = format!("/tmp/{}", "a".repeat(max_len - 5)); + assert!(validate_socket_path(Path::new(&valid_path_str)).is_ok()); + + let overlong_path_str = format!("/tmp/{}", "a".repeat(max_len)); + let err = validate_socket_path(Path::new(&overlong_path_str)).unwrap_err(); + assert_eq!(err.kind(), io::ErrorKind::InvalidInput); + assert!(err + .to_string() + .contains("set a shorter NAGI_CONFIG_PATH or XDG_CONFIG_HOME")); + } + #[test] fn stale_socket_connect_errors_keep_unix_would_block_strict() { assert!(stale_socket_connect_error(io::ErrorKind::ConnectionRefused)); diff --git a/src/server/autodetect.rs b/src/server/autodetect.rs index 3aad1718..b3719014 100644 --- a/src/server/autodetect.rs +++ b/src/server/autodetect.rs @@ -246,6 +246,7 @@ fn build_server_daemon_command(exe: PathBuf) -> Command { /// or the timeout elapses. Returns an error if the server doesn't become /// ready within the timeout. pub fn wait_for_server_socket(socket_path: &Path, timeout: Duration) -> io::Result<()> { + crate::ipc::validate_socket_path(socket_path)?; let deadline = std::time::Instant::now() + timeout; while std::time::Instant::now() < deadline { @@ -289,7 +290,10 @@ pub fn wait_for_server_socket(socket_path: &Path, timeout: Duration) -> io::Resu /// 2. If no server → spawn server daemon → wait for socket readiness /// 3. Run the thin client (which connects to the server) pub fn auto_detect_launch() -> io::Result<()> { + let api_path = crate::api::socket_path(); let socket_path = client_socket_path(); + crate::ipc::validate_socket_path(&api_path)?; + crate::ipc::validate_socket_path(&socket_path)?; info!(path = %socket_path.display(), "auto-detect launch starting"); if is_server_listening_at(&socket_path) { @@ -580,4 +584,17 @@ test "$sid" = "$$" crate::session::clear_explicit_session_for_test(); let _ = std::fs::remove_dir_all(dir); } + + #[cfg(unix)] + #[test] + fn wait_for_server_socket_fails_fast_on_overlong_path() { + let max_len = crate::ipc::max_unix_socket_path_len(); + let path = PathBuf::from(format!("/tmp/{}", "a".repeat(max_len))); + + let err = wait_for_server_socket(&path, Duration::from_secs(15)).unwrap_err(); + assert_eq!(err.kind(), io::ErrorKind::InvalidInput); + assert!(err + .to_string() + .contains("set a shorter NAGI_CONFIG_PATH or XDG_CONFIG_HOME")); + } } diff --git a/src/server/socket_paths.rs b/src/server/socket_paths.rs index c9fc70d9..105c6878 100644 --- a/src/server/socket_paths.rs +++ b/src/server/socket_paths.rs @@ -177,4 +177,17 @@ mod tests { let _ = fs::remove_dir_all(&dir); } + + #[cfg(unix)] + #[test] + fn prepare_socket_path_rejects_overlong_path() { + let max_len = crate::ipc::max_unix_socket_path_len(); + let overlong = PathBuf::from(format!("/tmp/{}", "a".repeat(max_len))); + + let err = prepare_socket_path(&overlong).unwrap_err(); + assert_eq!(err.kind(), io::ErrorKind::InvalidInput); + assert!(err + .to_string() + .contains("set a shorter NAGI_CONFIG_PATH or XDG_CONFIG_HOME")); + } }