Description
I tried this code:
#![feature(offset_of)]
use std::{
ffi::OsStr,
os::{
fd::{AsRawFd, FromRawFd, OwnedFd},
unix::{ffi::OsStrExt, net::UnixDatagram}
}
};
fn main() {
let path = std::path::Path::new("/tmp/getsockname_example");
let path_s = path.to_str().unwrap();
let _ = std::fs::remove_file(&path); // Just in case it already exists
let mut libc_sun: libc::sockaddr_un = unsafe { std::mem::zeroed() };
let l = path_s.len(); // Does not include NUL !!!
libc_sun.sun_family = libc::AF_UNIX as _;
#[cfg(any(
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
target_os = "dragonfly",
target_os = "macos"
))]
{
libc_sun.sun_len = l as u8;
}
unsafe {
std::ptr::copy(path_s.as_ptr() as *const i8, libc_sun.sun_path.as_mut_ptr(), l);
}
let sock = unsafe {
libc::socket(libc::AF_UNIX, libc::SOCK_DGRAM, 0)
};
assert!(sock >= 0);
let sock = unsafe{ OwnedFd::from_raw_fd(sock) };
// Bind the socket to a path. The path _is_ NUL-terminated, but the NUL is
// not included in the sun_len field or the addrlen argument. That is
// legal.
let r = unsafe {
libc::bind(
sock.as_raw_fd(),
&libc_sun as *const _ as *const libc::sockaddr,
(l + std::mem::offset_of!(libc::sockaddr_un, sun_path)) as u32
)
};
assert_eq!(0, r);
// Read the socket name back via gethostname, not assuming the presence of
// a trailing NUL.
let mut libc_bound: libc::sockaddr_un = unsafe { std::mem::zeroed() };
libc_bound.sun_path.fill('X' as i8);
let mut namelen = std::mem::size_of_val(&libc_bound) as u32;
let r = unsafe {
libc::getsockname(
sock.as_raw_fd(),
&mut libc_bound as *mut _ as *mut libc::sockaddr,
&mut namelen
)
};
assert_eq!(0, r);
#[cfg(any(
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
target_os = "dragonfly",
target_os = "macos"
))]
{
assert_eq!(namelen, libc_bound.sun_len as u32);
}
let namesl = unsafe {
std::slice::from_raw_parts(
libc_bound.sun_path.as_ptr().cast(),
namelen as usize - std::mem::offset_of!(libc::sockaddr_un, sun_path)
)
};
let libc_bound_s = OsStr::from_bytes(namesl);
// Read the socket name back via std, which does assume the presence of a
// trailing NUL and truncates the address if one is not found.
let std_bound = UnixDatagram::from(sock).local_addr().unwrap();
// Finally, compare the two.
assert_eq!(libc_bound_s, std_bound.as_pathname().unwrap().as_os_str());
}
I expected to see this happen: The program should've completed successfully, with no output.
Instead, this happened:
On Linux:
thread 'main' panicked at 'assertion failed: `(left == right)`
left: `"/tmp/getsockname_example\0"`,
right: `"/tmp/getsockname_example"`', examples/getsockname.rs:88:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
On FreeBSD and NetBSD:
thread 'main' panicked at examples/getsockname.rs:88:5:
assertion `left == right` failed
left: "/tmp/getsockname_example"
right: "/tmp/getsockname_exampl"
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Meta
rustc --version --verbose
:
rustc 1.76.0-nightly (f967532a4 2023-12-08)
binary: rustc
commit-hash: f967532a47eb728ada44473a5c4c2eca1a45fe30
commit-date: 2023-12-08
host: x86_64-unknown-freebsd
release: 1.76.0-nightly
LLVM version: 17.0.5
Impact
This bug is not visible using the standard library alone. That is, the standard library correctly round-trips unix domain socket addresses. However, if a program binds a socket using some other library (e.g., nix, capsicum-net, libc, a linked C library, or even a separate C process which sends the file descriptor through a cmsg), then Rust's standard library will read its address incorrectly. On a BSD, where Rust will truncate the address, the problem is likely to be noticeable. On Linux, the reverse problem may occur: a socket bound with Rust may be read incorrectly by some other library. That's less likely, however, because most libraries will strip extra trailing NUL bytes.
Analysis
On FreeBSD and NetBSD, the man pages specify "The sun_path field must be terminated by a NUL character to be used with SUN_LEN(), but the terminating NUL is not part of the address.". In practice, however, the kernel is tolerant of and preserves extra NUL characters. That is, two sockets cannot both be bound to foo
and foo\0
at the same time; those refer to the same file. However, If a user binds a socket to foo\0
, then a subsequent call to getsockname
will still return the trailing NUL.
On Linux, the man page recommends, but does not require, that the addrlen
argument of bind
include the trailing NUL. The kernel will append one if it is not provided. That's why the program above will print a trailing "\0" on Linux. The man page also provides this helpful advice, which I recommend Rust to follow:
Applications that retrieve socket addresses can (portably) code to han‐
dle the possibility that there is no null terminator in sun_path by re‐
specting the fact that the number of valid bytes in the pathname is:
strnlen(addr.sun_path, addrlen - offsetof(sockaddr_un, sun_path))