#![cfg(unix)]
#![allow(dead_code)]
use std::os::unix::fs::MetadataExt;
use std::os::unix::net::UnixStream;
use std::path::Path;
use anyhow::{anyhow, bail, Context, Result};
pub fn harden_socket_dir(path: &Path) -> Result<()> {
let meta = std::fs::metadata(path)
.with_context(|| format!("socket dir stat failed: {}", path.display()))?;
if !meta.is_dir() {
bail!("socket dir is not a directory: {}", path.display());
}
let our_uid = unsafe { libc::getuid() };
let owned_by_us = meta.uid() == our_uid;
let perm = meta.mode() & 0o777;
let sticky = meta.mode() & 0o1000 != 0;
if owned_by_us {
if perm & 0o077 != 0 {
bail!(
"socket dir {} owned by us but has insecure permissions {:o} (group/other access) — refusing to bind",
path.display(),
perm
);
}
} else {
if !sticky {
bail!(
"socket dir {} is owned by uid {} (not {}) and lacks the sticky bit — refusing to bind",
path.display(),
meta.uid(),
our_uid
);
}
}
Ok(())
}
pub fn fix_socket_permissions(path: &Path) -> Result<()> {
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
.with_context(|| format!("chmod 0600 failed for {}", path.display()))?;
let meta = std::fs::metadata(path)
.with_context(|| format!("socket stat failed: {}", path.display()))?;
let perm = meta.mode() & 0o777;
if perm != 0o600 {
bail!(
"socket {} has unexpected mode {:o} after chmod 0600",
path.display(),
perm
);
}
let our_uid = unsafe { libc::getuid() };
if meta.uid() != our_uid {
bail!(
"socket {} is owned by uid {}, not {}",
path.display(),
meta.uid(),
our_uid
);
}
Ok(())
}
pub fn abstract_socket_name(session: &str) -> String {
let uid = unsafe { libc::getuid() };
format!("ezpn-{uid}-{session}")
}
#[cfg(any(target_os = "linux", target_os = "android"))]
pub fn bind_abstract(name: &str) -> Result<std::os::unix::net::UnixListener> {
use std::os::linux::net::SocketAddrExt;
use std::os::unix::net::{SocketAddr, UnixListener};
let addr = SocketAddr::from_abstract_name(name.as_bytes())
.with_context(|| format!("from_abstract_name({name}) failed"))?;
let listener =
UnixListener::bind_addr(&addr).with_context(|| format!("bind_addr({name}) failed"))?;
Ok(listener)
}
#[cfg(not(any(target_os = "linux", target_os = "android")))]
pub fn bind_abstract(_name: &str) -> Result<std::os::unix::net::UnixListener> {
bail!("abstract namespace sockets are Linux-only")
}
pub fn peer_uid(stream: &UnixStream) -> Result<u32> {
use std::os::unix::io::AsRawFd;
let fd = stream.as_raw_fd();
#[cfg(target_os = "linux")]
{
let mut cred = libc::ucred {
pid: 0,
uid: 0,
gid: 0,
};
let mut len = std::mem::size_of::<libc::ucred>() as libc::socklen_t;
let rc = unsafe {
libc::getsockopt(
fd,
libc::SOL_SOCKET,
libc::SO_PEERCRED,
&mut cred as *mut _ as *mut libc::c_void,
&mut len,
)
};
if rc != 0 {
return Err(anyhow!(std::io::Error::last_os_error()))
.context("getsockopt(SO_PEERCRED) failed");
}
Ok(cred.uid)
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
{
let mut cred: libc::xucred = unsafe { std::mem::zeroed() };
let mut len = std::mem::size_of::<libc::xucred>() as libc::socklen_t;
let rc = unsafe {
libc::getsockopt(
fd,
libc::SOL_LOCAL,
libc::LOCAL_PEERCRED,
&mut cred as *mut _ as *mut libc::c_void,
&mut len,
)
};
if rc != 0 {
return Err(anyhow!(std::io::Error::last_os_error()))
.context("getsockopt(LOCAL_PEERCRED) failed");
}
Ok(cred.cr_uid)
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "ios")))]
{
let _ = fd;
bail!("peer credential check not implemented on this platform");
}
}
pub fn verify_secrets_file(path: &Path) -> Result<()> {
let meta = std::fs::metadata(path)
.with_context(|| format!("secrets file stat failed: {}", path.display()))?;
if !meta.is_file() {
bail!("secrets path is not a regular file: {}", path.display());
}
let perm = meta.mode() & 0o777;
if perm != 0o600 {
bail!(
"secrets file {} has insecure mode {:o} (must be 0600)",
path.display(),
perm
);
}
let our_uid = unsafe { libc::getuid() };
if meta.uid() != our_uid {
bail!(
"secrets file {} is owned by uid {}, not {}",
path.display(),
meta.uid(),
our_uid
);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::os::unix::fs::PermissionsExt;
use tempfile::tempdir;
#[test]
fn harden_socket_dir_accepts_0700() {
let dir = tempdir().unwrap();
std::fs::set_permissions(dir.path(), std::fs::Permissions::from_mode(0o700)).unwrap();
harden_socket_dir(dir.path()).expect("0700 dir is acceptable");
}
#[test]
fn harden_socket_dir_refuses_0755() {
let dir = tempdir().unwrap();
std::fs::set_permissions(dir.path(), std::fs::Permissions::from_mode(0o755)).unwrap();
let err = harden_socket_dir(dir.path()).expect_err("0755 must be rejected");
let msg = format!("{err:#}");
assert!(
msg.contains("insecure permissions"),
"unexpected error: {msg}"
);
}
#[test]
fn harden_socket_dir_accepts_tmp_with_sticky_bit() {
let tmp = Path::new("/tmp");
if std::fs::metadata(tmp).is_ok() {
harden_socket_dir(tmp).expect("/tmp must be acceptable");
}
}
#[test]
fn fix_socket_permissions_chmods_and_verifies() {
let dir = tempdir().unwrap();
let p = dir.path().join("sock");
std::fs::write(&p, b"").unwrap();
std::fs::set_permissions(&p, std::fs::Permissions::from_mode(0o644)).unwrap();
fix_socket_permissions(&p).expect("chmod must succeed");
let mode = std::fs::metadata(&p).unwrap().mode() & 0o777;
assert_eq!(mode, 0o600);
}
#[test]
fn peer_uid_roundtrip_on_pair() {
let (a, b) = UnixStream::pair().unwrap();
let our_uid = unsafe { libc::getuid() };
assert_eq!(peer_uid(&a).unwrap(), our_uid);
assert_eq!(peer_uid(&b).unwrap(), our_uid);
}
#[test]
fn verify_secrets_file_accepts_0600() {
let dir = tempdir().unwrap();
let p = dir.path().join("secrets.toml");
std::fs::write(&p, b"").unwrap();
std::fs::set_permissions(&p, std::fs::Permissions::from_mode(0o600)).unwrap();
verify_secrets_file(&p).expect("0600 must be accepted");
}
#[test]
fn verify_secrets_file_refuses_0644() {
let dir = tempdir().unwrap();
let p = dir.path().join("secrets.toml");
std::fs::write(&p, b"").unwrap();
std::fs::set_permissions(&p, std::fs::Permissions::from_mode(0o644)).unwrap();
let err = verify_secrets_file(&p).expect_err("0644 must be rejected");
let msg = format!("{err:#}");
assert!(msg.contains("insecure mode"), "unexpected error: {msg}");
}
#[test]
fn abstract_socket_name_includes_uid_and_session() {
let name = abstract_socket_name("demo");
let our_uid = unsafe { libc::getuid() };
assert_eq!(name, format!("ezpn-{our_uid}-demo"));
}
#[cfg(any(target_os = "linux", target_os = "android"))]
#[test]
fn abstract_namespace_bind_roundtrips() {
use std::io::{Read, Write};
use std::os::linux::net::SocketAddrExt;
use std::os::unix::net::{SocketAddr, UnixStream};
let name = format!("ezpn-test-{}", std::process::id());
let listener = bind_abstract(&name).expect("abstract bind must succeed on Linux");
let addr = SocketAddr::from_abstract_name(name.as_bytes()).unwrap();
let mut client =
UnixStream::connect_addr(&addr).expect("abstract connect must succeed on Linux");
let (mut server, _) = listener.accept().expect("accept");
client.write_all(b"ping").unwrap();
let mut buf = [0u8; 4];
server.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"ping");
}
}