From b1936e5a2c0995a3e97d4c564ce4ccea5fb5f3d6 Mon Sep 17 00:00:00 2001 From: "Anatoly Sergeev (miruji)" Date: Sat, 26 Sep 2026 21:04:41 +0300 Subject: [PATCH 1/3] Use Linux abstract namespace for OsIpcOneShotServer --- src/platform/unix/mod.rs | 73 ++++++++++++++++++++++++++++++++++------ 1 file changed, 63 insertions(+), 10 deletions(-) diff --git a/src/platform/unix/mod.rs b/src/platform/unix/mod.rs index 66ccfdfe..286cb91f 100644 --- a/src/platform/unix/mod.rs +++ b/src/platform/unix/mod.rs @@ -36,7 +36,9 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, LazyLock}; use std::thread; use std::time::{Duration, UNIX_EPOCH}; -use tempfile::{Builder, TempDir}; +#[cfg(not(target_os = "linux"))] +use tempfile::Builder; +use tempfile::TempDir; use thiserror::Error; const MAX_FDS_IN_CMSG: u32 = 64; @@ -92,6 +94,46 @@ unsafe fn new_sockaddr_un(path: *const c_char) -> (sockaddr_un, usize) { (sockaddr, mem::size_of::()) } +/// Builds a `sockaddr_un` for `name`. On Linux, a name prefixed with `@` +/// is bound in the abstract namespace (`sun_path[0] = 0`) instead of on +/// the filesystem — no directory or inode is ever created for it. Any +/// other name goes through the regular filesystem-backed encoding. +unsafe fn build_sockaddr(name: &str) -> Result<(sockaddr_un, usize), UnixError> { + #[cfg(target_os = "linux")] + if let Some(abstract_name) = name.strip_prefix('@') { + let mut sockaddr: sockaddr_un = mem::zeroed(); + sockaddr.sun_family = libc::AF_UNIX as sa_family_t; + // sun_path[0] = 0 (already zeroed) marks this abstract on Linux. + let bytes = abstract_name.as_bytes(); + let max_len = sockaddr.sun_path.len() - 1; // leave room for the leading NUL + if bytes.len() > max_len { + return Err(UnixError::last()); + } + for (i, b) in bytes.iter().enumerate() { + sockaddr.sun_path[i + 1] = *b as c_char; + } + let addr_len = size_of::() + 1 + bytes.len(); + return Ok((sockaddr, addr_len)); + } + let c_name = CString::new(name).unwrap(); + Ok(new_sockaddr_un(c_name.as_ptr())) +} + +/// Generates a random name for an abstract-namespace socket, prefixed +/// with `@` so `build_sockaddr` recognises it. +#[cfg(target_os = "linux")] +fn make_abstract_name() -> String { + let mut buf = [0u8; 16]; + unsafe { + libc::getrandom(buf.as_mut_ptr() as *mut c_void, buf.len(), 0); + } + let mut hex = String::with_capacity(32); + for b in buf { + hex.push_str(&format!("{:02x}", b)); + } + format!("@ipc-osn-{hex}") +} + static SYSTEM_SENDBUF_SIZE: LazyLock = LazyLock::new(|| { let (tx, _) = channel().expect("Failed to obtain a socket for checking maximum send size"); tx.get_system_sendbuf_size() @@ -463,10 +505,9 @@ impl OsIpcSender { } pub fn connect(name: String) -> Result { - let name = CString::new(name).unwrap(); unsafe { let fd = libc::socket(libc::AF_UNIX, SOCK_SEQPACKET | SOCK_FLAGS, 0); - let (sockaddr, len) = new_sockaddr_un(name.as_ptr()); + let (sockaddr, len) = build_sockaddr(&name)?; if libc::connect( fd, &sockaddr as *const _ as *const sockaddr, @@ -686,7 +727,10 @@ pub struct OsIpcOneShotServer { // Object representing the temporary directory the socket was created in. // The directory is automatically deleted (along with the socket inside it) // when this field is dropped. - _temp_dir: TempDir, + // + // `None` on Linux, where the abstract namespace needs no filesystem entry + // and thus no writable directory anywhere. + _temp_dir: Option, } impl Drop for OsIpcOneShotServer { @@ -702,12 +746,21 @@ impl OsIpcOneShotServer { pub fn new() -> Result<(OsIpcOneShotServer, String), UnixError> { unsafe { let fd = libc::socket(libc::AF_UNIX, SOCK_SEQPACKET | SOCK_FLAGS, 0); - let temp_dir = Builder::new().tempdir()?; - let socket_path = temp_dir.path().join("socket"); - let path_string = socket_path.to_str().unwrap(); - let path_c_string = CString::new(path_string).unwrap(); - let (sockaddr, len) = new_sockaddr_un(path_c_string.as_ptr()); + // Linux: bind in the abstract namespace, no filesystem entry. + #[cfg(target_os = "linux")] + let (name_string, temp_dir) = (make_abstract_name(), None); + + // Other Unix targets have no abstract namespace: bind to a + // path inside a fresh temp directory instead. + #[cfg(not(target_os = "linux"))] + let (name_string, temp_dir) = { + let temp_dir = Builder::new().tempdir()?; + let socket_path = temp_dir.path().join("socket"); + (socket_path.to_str().unwrap().to_string(), Some(temp_dir)) + }; + + let (sockaddr, len) = build_sockaddr(&name_string)?; if libc::bind( fd, &sockaddr as *const _ as *const sockaddr, @@ -726,7 +779,7 @@ impl OsIpcOneShotServer { fd, _temp_dir: temp_dir, }, - path_string.to_string(), + name_string )) } } From e75cee3bd1336fe9b14346ca2087c5f2343aef6d Mon Sep 17 00:00:00 2001 From: "Anatoly Sergeev (miruji)" Date: Mon, 28 Sep 2026 15:32:31 +0300 Subject: [PATCH 2/3] Add explanatory comments --- src/platform/unix/mod.rs | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/platform/unix/mod.rs b/src/platform/unix/mod.rs index 286cb91f..c4519771 100644 --- a/src/platform/unix/mod.rs +++ b/src/platform/unix/mod.rs @@ -94,10 +94,15 @@ unsafe fn new_sockaddr_un(path: *const c_char) -> (sockaddr_un, usize) { (sockaddr, mem::size_of::()) } -/// Builds a `sockaddr_un` for `name`. On Linux, a name prefixed with `@` -/// is bound in the abstract namespace (`sun_path[0] = 0`) instead of on -/// the filesystem — no directory or inode is ever created for it. Any -/// other name goes through the regular filesystem-backed encoding. +/// Builds a `sockaddr_un` for `name`. The `@` prefix is the user-space +/// convention (see systemd `ListenStream=@`, socat `ABSTRACT-LISTEN:`, +/// and how `ss -x` / `netstat -x` render abstract sockets) for opting +/// into the Linux abstract namespace. +/// +/// A name without `@` falls back to the regular filesystem-backed +/// encoding, which keeps this helper generic enough for future callers +/// that may want filesystem sockets on Linux too (e.g. for compatibility +/// with non-abstract clients). unsafe fn build_sockaddr(name: &str) -> Result<(sockaddr_un, usize), UnixError> { #[cfg(target_os = "linux")] if let Some(abstract_name) = name.strip_prefix('@') { @@ -109,6 +114,13 @@ unsafe fn build_sockaddr(name: &str) -> Result<(sockaddr_un, usize), UnixError> if bytes.len() > max_len { return Err(UnixError::last()); } + // Linux abstract namespace (see man 7 unix): sun_path[0] is a marker + // (NUL), not a terminator. The kernel uses addrlen to determine the + // address bytes, so the name goes in sun_path[1..]. + // + // The null-terminated pathname requirement from connect(2) applies to + // filesystem-backed pathname sockets; Linux abstract sockets use addrlen + // to determine the address bytes instead. for (i, b) in bytes.iter().enumerate() { sockaddr.sun_path[i + 1] = *b as c_char; } From 25334bf71b7ff4a6eace568ad4b6dd4500a361d1 Mon Sep 17 00:00:00 2001 From: "Anatoly Sergeev (miruji)" Date: Wed, 30 Sep 2026 13:07:45 +0300 Subject: [PATCH 3/3] Drop the @ abstract-namespace prefix convention --- src/platform/unix/mod.rs | 49 ++++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/src/platform/unix/mod.rs b/src/platform/unix/mod.rs index c4519771..135ea321 100644 --- a/src/platform/unix/mod.rs +++ b/src/platform/unix/mod.rs @@ -83,6 +83,10 @@ pub enum OsTrySelectError { Empty, } +/// Builds a filesystem-backed `sockaddr_un` for a pathname socket. Only used +/// on non-Linux Unix targets; Linux goes through the abstract-namespace +/// branch in `build_sockaddr` instead. +#[cfg(not(target_os = "linux"))] unsafe fn new_sockaddr_un(path: *const c_char) -> (sockaddr_un, usize) { let mut sockaddr: sockaddr_un = mem::zeroed(); libc::strncpy( @@ -94,22 +98,26 @@ unsafe fn new_sockaddr_un(path: *const c_char) -> (sockaddr_un, usize) { (sockaddr, mem::size_of::()) } -/// Builds a `sockaddr_un` for `name`. The `@` prefix is the user-space -/// convention (see systemd `ListenStream=@`, socat `ABSTRACT-LISTEN:`, -/// and how `ss -x` / `netstat -x` render abstract sockets) for opting -/// into the Linux abstract namespace. +/// Builds a `sockaddr_un` for `name`. +/// +/// On Linux the name is placed in the abstract socket namespace +/// (see man 7 unix): `sun_path[0]` is a NUL marker rather than a +/// terminator, the kernel uses `addrlen` to determine the address +/// bytes, and no filesystem entry is created. This lets the one-shot +/// server bind without a writable temp directory, which matters in +/// containers/sandboxes where `/tmp` is read-only. /// -/// A name without `@` falls back to the regular filesystem-backed -/// encoding, which keeps this helper generic enough for future callers -/// that may want filesystem sockets on Linux too (e.g. for compatibility -/// with non-abstract clients). +/// On other Unix targets there is no abstract namespace, so the name +/// is encoded as a regular filesystem-backed pathname (see +/// `new_sockaddr_un`). unsafe fn build_sockaddr(name: &str) -> Result<(sockaddr_un, usize), UnixError> { #[cfg(target_os = "linux")] - if let Some(abstract_name) = name.strip_prefix('@') { + { let mut sockaddr: sockaddr_un = mem::zeroed(); sockaddr.sun_family = libc::AF_UNIX as sa_family_t; - // sun_path[0] = 0 (already zeroed) marks this abstract on Linux. - let bytes = abstract_name.as_bytes(); + // sun_path[0] = 0 (already zeroed) marks this as an abstract + // socket address on Linux (see man 7 unix). + let bytes = name.as_bytes(); let max_len = sockaddr.sun_path.len() - 1; // leave room for the leading NUL if bytes.len() > max_len { return Err(UnixError::last()); @@ -125,14 +133,21 @@ unsafe fn build_sockaddr(name: &str) -> Result<(sockaddr_un, usize), UnixError> sockaddr.sun_path[i + 1] = *b as c_char; } let addr_len = size_of::() + 1 + bytes.len(); - return Ok((sockaddr, addr_len)); + Ok((sockaddr, addr_len)) + } + + // Fallback for non-Linux Unix targets: encode `name` as a regular + // filesystem-backed pathname. + #[cfg(not(target_os = "linux"))] + { + let c_name = CString::new(name).unwrap(); + Ok(new_sockaddr_un(c_name.as_ptr())) } - let c_name = CString::new(name).unwrap(); - Ok(new_sockaddr_un(c_name.as_ptr())) } -/// Generates a random name for an abstract-namespace socket, prefixed -/// with `@` so `build_sockaddr` recognises it. +/// Generates a random name for an abstract-namespace socket. The +/// returned name has no filesystem meaning on Linux; `build_sockaddr` +/// interprets it directly as an abstract-namespace address. #[cfg(target_os = "linux")] fn make_abstract_name() -> String { let mut buf = [0u8; 16]; @@ -143,7 +158,7 @@ fn make_abstract_name() -> String { for b in buf { hex.push_str(&format!("{:02x}", b)); } - format!("@ipc-osn-{hex}") + format!("ipc-osn-{hex}") } static SYSTEM_SENDBUF_SIZE: LazyLock = LazyLock::new(|| {