use super::dir::{DirVerdict, classify_existing_dir};
use super::peer::peer_uid_verdict;
use super::*;
use std::os::unix::fs::PermissionsExt;
use std::time::Duration;
fn mode_of(path: &Path) -> u32 {
std::fs::symlink_metadata(path)
.expect("stat path under test")
.permissions()
.mode()
& 0o777
}
#[test]
fn scratch_socket_dir_from_uses_tmpdir_when_set() {
let dir = scratch_socket_dir_from(Some("/var/folders/xy/T"), 501);
assert_eq!(dir, Path::new("/var/folders/xy/T/trusty-501"));
}
#[test]
fn scratch_socket_dir_from_falls_back_to_tmp() {
for absent in [None, Some(""), Some(" ")] {
let dir = scratch_socket_dir_from(absent, 1000);
assert_eq!(
dir,
Path::new("/tmp/trusty-1000"),
"TMPDIR={absent:?} must still get a uid-keyed subdirectory"
);
}
}
#[test]
fn scratch_socket_dir_is_uid_keyed() {
let dir = scratch_socket_dir();
let leaf = dir
.file_name()
.and_then(|n| n.to_str())
.expect("scratch dir has a leaf name");
assert_eq!(leaf, format!("trusty-{}", self_uid()));
}
#[test]
fn sun_path_capacity_is_platform_plausible() {
let cap = sun_path_capacity();
assert!(
(104..=108).contains(&cap),
"sun_path capacity {cap} is outside the known 104..=108 range"
);
}
#[test]
fn check_sun_path_budget_accepts_a_path_that_fits() {
let path = PathBuf::from("/tmp/trusty-501/short.sock");
check_sun_path_budget(&path).expect("a short path must fit");
}
#[test]
fn check_sun_path_budget_rejects_an_over_long_path() {
let long = format!("/tmp/{}.sock", "x".repeat(sun_path_capacity()));
let err = check_sun_path_budget(Path::new(&long)).expect_err("must reject");
match err {
UdsSecurityError::PathTooLong { len, capacity, .. } => {
assert!(len >= capacity, "len {len} must exceed capacity {capacity}");
let rendered = err.to_string();
assert!(
rendered.contains(&capacity.to_string()) && rendered.contains(&len.to_string()),
"diagnostic must name both the budget and the actual length: {rendered}"
);
}
other => panic!("expected PathTooLong, got {other:?}"),
}
}
#[tokio::test]
async fn bind_hardened_rejects_an_over_long_path() {
let tmp = tempfile::tempdir().expect("tempdir");
let long = tmp.path().join(format!("{}.sock", "x".repeat(120)));
let err = bind_hardened(&long).expect_err("must reject before binding");
assert!(
matches!(err, UdsSecurityError::PathTooLong { .. }),
"expected PathTooLong, got {err:?}"
);
}
#[test]
fn classify_existing_dir_rejects_a_symlink() {
let err = classify_existing_dir(Path::new("/tmp/x"), true, false, "symlink", 501, 0o700, 501)
.expect_err("a symlink must be refused even when owner and mode look right");
assert!(
matches!(err, UdsSecurityError::SymlinkDir { .. }),
"expected SymlinkDir, got {err:?}"
);
}
#[test]
fn classify_existing_dir_rejects_a_regular_file() {
let err = classify_existing_dir(
Path::new("/tmp/x"),
false,
false,
"regular file",
501,
0o644,
501,
)
.expect_err("a non-directory must be refused");
match err {
UdsSecurityError::NotADirectory { ref found, .. } => {
assert_eq!(found, "regular file", "diagnostic must name what was found");
}
other => panic!("expected NotADirectory, got {other:?}"),
}
}
#[test]
fn classify_existing_dir_checks_file_type_before_owner() {
let err = classify_existing_dir(Path::new("/tmp/x"), false, false, "fifo", 999, 0o777, 501)
.expect_err("no");
assert!(
matches!(err, UdsSecurityError::NotADirectory { .. }),
"file type must be checked before ownership, got {err:?}"
);
}
#[test]
fn classify_existing_dir_rejects_a_foreign_owner() {
let err = classify_existing_dir(Path::new("/tmp/x"), false, true, "directory", 0, 0o700, 501)
.expect_err("a root-owned directory must be refused");
match err {
UdsSecurityError::ForeignDirOwner {
owner, expected, ..
} => {
assert_eq!((owner, expected), (0, 501));
}
other => panic!("expected ForeignDirOwner, got {other:?}"),
}
}
#[test]
fn classify_existing_dir_narrows_a_wide_dir() {
let v = classify_existing_dir(
Path::new("/tmp/x"),
false,
true,
"directory",
501,
0o755,
501,
)
.expect("ours");
assert_eq!(v, DirVerdict::Narrow);
}
#[test]
fn classify_existing_dir_accepts_an_already_correct_dir() {
let v = classify_existing_dir(
Path::new("/tmp/x"),
false,
true,
"directory",
501,
0o700,
501,
)
.expect("ours");
assert_eq!(v, DirVerdict::Accept);
}
#[test]
fn classify_existing_dir_checks_symlink_before_owner() {
let err = classify_existing_dir(Path::new("/tmp/x"), true, false, "symlink", 999, 0o777, 501)
.expect_err("no");
assert!(
matches!(err, UdsSecurityError::SymlinkDir { .. }),
"symlink must be rejected before ownership is considered, got {err:?}"
);
}
#[test]
fn prepare_socket_dir_creates_at_0700() {
let tmp = tempfile::tempdir().expect("tempdir");
let dir = tmp.path().join("sockets");
prepare_socket_dir(&dir).expect("prepare fresh socket dir");
assert_eq!(
mode_of(&dir),
SOCKET_DIR_MODE,
"fresh socket dir must be 0700"
);
}
#[test]
fn prepare_socket_dir_creates_missing_ancestors() {
let tmp = tempfile::tempdir().expect("tempdir");
let dir = tmp.path().join("a").join("b").join("sockets");
prepare_socket_dir(&dir).expect("prepare nested socket dir");
assert_eq!(mode_of(&dir), SOCKET_DIR_MODE);
}
#[test]
fn prepare_socket_dir_narrows_a_wide_existing_dir() {
let tmp = tempfile::tempdir().expect("tempdir");
let dir = tmp.path().join("sockets");
std::fs::create_dir(&dir).expect("create wide dir");
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o755)).expect("widen");
assert_eq!(mode_of(&dir), 0o755, "precondition: dir starts wide");
prepare_socket_dir(&dir).expect("prepare pre-existing socket dir");
assert_eq!(
mode_of(&dir),
SOCKET_DIR_MODE,
"existing dir must be narrowed"
);
}
#[test]
fn prepare_socket_dir_rejects_a_symlink() {
let tmp = tempfile::tempdir().expect("tempdir");
let target = tmp.path().join("attacker_controlled");
std::fs::create_dir(&target).expect("create target");
std::fs::set_permissions(&target, std::fs::Permissions::from_mode(0o777)).expect("widen");
let link = tmp.path().join("sockets");
std::os::unix::fs::symlink(&target, &link).expect("create symlink");
let err = prepare_socket_dir(&link).expect_err("a symlinked socket dir must be refused");
assert!(
matches!(err, UdsSecurityError::SymlinkDir { .. }),
"expected SymlinkDir, got {err:?}"
);
assert_eq!(
mode_of(&target),
0o777,
"the symlink target must NOT have been chmod'd"
);
}
#[test]
fn prepare_socket_dir_rejects_a_regular_file_without_chmodding_it() {
let tmp = tempfile::tempdir().expect("tempdir");
let planted = tmp.path().join("sockets");
std::fs::write(&planted, b"not a directory").expect("plant file");
std::fs::set_permissions(&planted, std::fs::Permissions::from_mode(0o644)).expect("chmod");
let err = prepare_socket_dir(&planted).expect_err("a non-directory must be refused");
match err {
UdsSecurityError::NotADirectory { ref found, .. } => {
assert_eq!(found, "regular file");
}
other => panic!("expected NotADirectory, got {other:?}"),
}
assert_eq!(
mode_of(&planted),
0o644,
"the planted file must NOT have been chmod'd"
);
}
#[test]
fn prepare_socket_dir_is_idempotent() {
let tmp = tempfile::tempdir().expect("tempdir");
let dir = tmp.path().join("sockets");
prepare_socket_dir(&dir).expect("first prepare");
prepare_socket_dir(&dir).expect("second prepare must succeed");
assert_eq!(mode_of(&dir), SOCKET_DIR_MODE);
}
#[tokio::test]
async fn bind_hardened_sets_socket_0600_and_dir_0700() {
let tmp = tempfile::tempdir().expect("tempdir");
let dir = tmp.path().join("sockets");
let sock = dir.join("t.sock");
let _listener = bind_hardened(&sock).expect("bind hardened listener");
assert_eq!(mode_of(&sock), SOCKET_MODE, "socket must be 0600");
assert_eq!(mode_of(&dir), SOCKET_DIR_MODE, "socket dir must be 0700");
}
#[tokio::test]
async fn bind_hardened_socket_is_connectable_after_hardening() {
let tmp = tempfile::tempdir().expect("tempdir");
let sock = tmp.path().join("sockets").join("t.sock");
let listener = bind_hardened(&sock).expect("bind hardened listener");
let client = UnixStream::connect(&sock).await.expect("same-uid connect");
let (accepted, _) = listener.accept().await.expect("accept");
ensure_peer_is_self(&accepted).expect("same-uid peer must be accepted");
drop(client);
}
#[tokio::test]
async fn bind_hardened_rejects_a_path_with_no_parent() {
let err = bind_hardened(Path::new("bare.sock")).expect_err("must refuse");
assert!(
matches!(err, UdsSecurityError::NoParent { .. }),
"expected NoParent, got {err:?}"
);
}
#[tokio::test]
async fn bind_hardened_propagates_an_address_in_use_failure() {
let tmp = tempfile::tempdir().expect("tempdir");
let sock = tmp.path().join("sockets").join("t.sock");
let _first = bind_hardened(&sock).expect("first bind");
let err = bind_hardened(&sock).expect_err("second bind must fail");
assert!(
matches!(err, UdsSecurityError::Bind { .. }),
"expected Bind, got {err:?}"
);
}
#[tokio::test]
async fn connect_hardened_accepts_a_properly_hardened_socket() {
let tmp = tempfile::tempdir().expect("tempdir");
let sock = tmp.path().join("sockets").join("t.sock");
let listener = bind_hardened(&sock).expect("bind");
let client = connect_hardened(&sock).await.expect("dial our own socket");
let (accepted, _) = listener.accept().await.expect("accept");
ensure_peer_is_self(&accepted).expect("peer is us");
drop(client);
}
#[tokio::test]
async fn connect_hardened_refuses_a_world_readable_socket() {
let tmp = tempfile::tempdir().expect("tempdir");
let sock = tmp.path().join("sockets").join("t.sock");
let _listener = bind_hardened(&sock).expect("bind");
std::fs::set_permissions(&sock, std::fs::Permissions::from_mode(0o777)).expect("widen socket");
let err = connect_hardened(&sock).await.expect_err("must refuse");
match err {
UdsSecurityError::UntrustedSocket { ref reason, .. } => {
assert!(
reason.contains("0777"),
"reason must name the mode: {reason}"
);
}
other => panic!("expected UntrustedSocket, got {other:?}"),
}
}
#[tokio::test]
async fn connect_hardened_refuses_a_socket_in_a_wide_directory() {
let tmp = tempfile::tempdir().expect("tempdir");
let dir = tmp.path().join("sockets");
let sock = dir.join("t.sock");
let _listener = bind_hardened(&sock).expect("bind");
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o755)).expect("widen dir");
let err = connect_hardened(&sock).await.expect_err("must refuse");
match err {
UdsSecurityError::UntrustedSocket { ref reason, .. } => {
assert!(
reason.contains("directory") && reason.contains("0755"),
"reason must name the directory and its mode: {reason}"
);
}
other => panic!("expected UntrustedSocket, got {other:?}"),
}
}
#[tokio::test]
async fn connect_hardened_refuses_a_regular_file() {
let tmp = tempfile::tempdir().expect("tempdir");
let dir = tmp.path().join("sockets");
prepare_socket_dir(&dir).expect("prepare");
let planted = dir.join("t.sock");
std::fs::write(&planted, b"not a socket").expect("plant file");
std::fs::set_permissions(&planted, std::fs::Permissions::from_mode(0o600)).expect("chmod");
let err = connect_hardened(&planted).await.expect_err("must refuse");
match err {
UdsSecurityError::UntrustedSocket { ref reason, .. } => {
assert!(reason.contains("not a socket"), "got: {reason}");
}
other => panic!("expected UntrustedSocket, got {other:?}"),
}
}
#[test]
fn verify_socket_for_connect_reports_a_stat_failure_as_stat_not_create() {
let tmp = tempfile::tempdir().expect("tempdir");
let missing = tmp.path().join("absent").join("t.sock");
let err = verify_socket_for_connect(&missing).expect_err("must fail");
assert!(
matches!(err, UdsSecurityError::StatForConnect { .. }),
"expected StatForConnect, got {err:?}"
);
assert!(
!err.to_string().contains("create socket directory"),
"a dialer must not claim it was creating anything: {err}"
);
}
#[test]
fn verify_socket_for_connect_refuses_a_regular_file_as_the_directory() {
let tmp = tempfile::tempdir().expect("tempdir");
let planted = tmp.path().join("sockets");
std::fs::write(&planted, b"not a directory").expect("plant file");
let err = verify_socket_for_connect(&planted.join("t.sock")).expect_err("must refuse");
assert!(
matches!(err, UdsSecurityError::NotADirectory { .. }),
"expected NotADirectory, got {err:?}"
);
}
#[test]
fn verify_socket_for_connect_refuses_a_symlinked_directory() {
let tmp = tempfile::tempdir().expect("tempdir");
let target = tmp.path().join("elsewhere");
std::fs::create_dir(&target).expect("create target");
let link = tmp.path().join("sockets");
std::os::unix::fs::symlink(&target, &link).expect("symlink");
let err = verify_socket_for_connect(&link.join("t.sock")).expect_err("must refuse");
assert!(
matches!(err, UdsSecurityError::SymlinkDir { .. }),
"expected SymlinkDir, got {err:?}"
);
}
#[test]
fn peer_uid_verdict_accepts_the_same_uid() {
peer_uid_verdict(501, 501).expect("a same-uid peer must be admitted");
}
#[test]
fn peer_uid_verdict_refuses_a_foreign_uid() {
let err = peer_uid_verdict(999, 501).expect_err("a foreign uid must be refused");
match err {
UdsSecurityError::ForeignPeer { peer, expected } => {
assert_eq!((peer, expected), (999, 501));
}
other => panic!("expected ForeignPeer, got {other:?}"),
}
}
#[test]
fn peer_uid_verdict_refuses_root_when_we_are_not_root() {
let err = peer_uid_verdict(0, 501).expect_err("root must be refused like any foreign uid");
assert!(matches!(err, UdsSecurityError::ForeignPeer { peer: 0, .. }));
}
#[tokio::test]
async fn peer_uid_of_self_connection_is_self() {
let tmp = tempfile::tempdir().expect("tempdir");
let sock = tmp.path().join("sockets").join("t.sock");
let listener = bind_hardened(&sock).expect("bind");
let _client = UnixStream::connect(&sock).await.expect("connect");
let (accepted, _) = listener.accept().await.expect("accept");
assert_eq!(
peer_uid(&accepted).expect("read peer uid"),
self_uid(),
"a connection from this process must report this process's uid"
);
}
#[tokio::test]
async fn peer_pid_of_self_connection_is_this_process() {
let tmp = tempfile::tempdir().expect("tempdir");
let sock = tmp.path().join("sockets").join("pid.sock");
let listener = bind_hardened(&sock).expect("bind");
let _client = UnixStream::connect(&sock).await.expect("connect");
let (accepted, _) = listener.accept().await.expect("accept");
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "ios"))]
assert_eq!(
super::peer::peer_pid(&accepted),
Some(std::process::id()),
"a connection from this process must report this process's pid"
);
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "ios")))]
assert_eq!(super::peer::peer_pid(&accepted), None);
}
#[tokio::test]
async fn bind_singleton_binds_a_fresh_path() {
let tmp = tempfile::tempdir().expect("tempdir");
let sock = tmp.path().join("sockets").join("fresh.sock");
let listener = bind_singleton_hardened(&sock).await.expect("bind fresh");
assert_eq!(mode_of(&sock), SOCKET_MODE, "a takeover must still harden");
drop(listener);
}
#[tokio::test]
async fn bind_singleton_takes_over_a_stale_socket_file() {
let tmp = tempfile::tempdir().expect("tempdir");
let sock = tmp.path().join("sockets").join("stale.sock");
let dead = bind_hardened(&sock).expect("bind first");
drop(dead); assert!(sock.exists(), "the corpse must still be on disk");
let deadline = std::time::Instant::now() + Duration::from_secs(5);
while probe_socket_verdict(&sock, Duration::from_millis(50)).await != SocketVerdict::NotServing
{
assert!(
std::time::Instant::now() < deadline,
"the dropped listener never stopped answering connects"
);
tokio::time::sleep(Duration::from_millis(10)).await;
}
let listener = bind_singleton_hardened(&sock)
.await
.expect("a socket nobody serves must be taken over");
assert_eq!(mode_of(&sock), SOCKET_MODE);
drop(listener);
}
#[tokio::test]
async fn bind_singleton_refuses_a_socket_someone_is_serving() {
let tmp = tempfile::tempdir().expect("tempdir");
let sock = tmp.path().join("sockets").join("live.sock");
let _live = bind_hardened(&sock).expect("bind the live owner");
let err = bind_singleton_hardened(&sock)
.await
.expect_err("a served socket must not be taken over");
assert!(
matches!(err, UdsSecurityError::AlreadyServing { .. }),
"expected AlreadyServing, got {err:?}"
);
assert!(sock.exists(), "the live owner's socket must survive");
}