use super::dir::{DirVerdict, classify_existing_dir};
use super::peer::peer_uid_verdict;
use super::singleton::{TakeoverVerdict, classify_takeover};
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");
wait_until_corpse(&sock).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");
}
#[test]
fn takeover_verdict_refuses_a_non_socket_even_when_the_probe_says_dead() {
assert_eq!(
classify_takeover(false, Some(SocketVerdict::NotServing)),
TakeoverVerdict::NotASocket
);
}
#[test]
fn takeover_verdict_refuses_a_non_socket_on_every_probe_answer() {
for verdict in [
SocketVerdict::Serving,
SocketVerdict::NotServing,
SocketVerdict::Inconclusive,
] {
assert_eq!(
classify_takeover(false, Some(verdict)),
TakeoverVerdict::NotASocket,
"the file type outranks the probe, got {verdict:?}"
);
}
}
#[test]
fn takeover_verdict_refuses_a_non_socket_that_was_never_probed() {
assert_eq!(
classify_takeover(false, None),
TakeoverVerdict::NotASocket,
"an unprobed non-socket is still refused"
);
}
#[test]
fn takeover_verdict_refuses_an_unprobed_socket() {
assert_eq!(classify_takeover(true, None), TakeoverVerdict::Occupied);
}
#[test]
fn takeover_verdict_takes_over_a_dead_socket() {
assert_eq!(
classify_takeover(true, Some(SocketVerdict::NotServing)),
TakeoverVerdict::TakeOver
);
}
#[test]
fn takeover_verdict_refuses_a_served_socket() {
assert_eq!(
classify_takeover(true, Some(SocketVerdict::Serving)),
TakeoverVerdict::Occupied
);
}
#[test]
fn takeover_verdict_refuses_an_inconclusive_probe() {
assert_eq!(
classify_takeover(true, Some(SocketVerdict::Inconclusive)),
TakeoverVerdict::Occupied
);
}
#[tokio::test]
async fn bind_singleton_refuses_a_regular_file_and_leaves_it_on_disk() {
let tmp = tempfile::tempdir().expect("tempdir");
let dir = tmp.path().join("sockets");
std::fs::create_dir_all(&dir).expect("socket dir");
let sock = dir.join("occupied.sock");
std::fs::write(&sock, b"not a socket").expect("occupy the socket path");
let err = bind_singleton_hardened(&sock)
.await
.expect_err("a regular file on the socket path must stop the bind");
let rendered = err.to_string();
assert!(
rendered.contains("is a regular file, not a socket"),
"the refusal must name what is actually there, got: {rendered}"
);
assert!(sock.exists(), "the file must not have been unlinked");
assert_eq!(
std::fs::read(&sock).expect("read the file back"),
b"not a socket",
"the file's contents must be untouched"
);
}
async fn wait_until_corpse(sock: &Path) {
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;
}
}
#[tokio::test]
async fn bind_singleton_hardened_refuses_a_symlink_to_a_dead_socket() {
let tmp = tempfile::tempdir().expect("tempdir");
let dir = tmp.path().join("sockets");
let target = dir.join("dead.sock");
let dead = bind_hardened(&target).expect("bind the soon-to-be corpse");
drop(dead); wait_until_corpse(&target).await;
let link = dir.join("link.sock");
std::os::unix::fs::symlink(&target, &link).expect("symlink onto the corpse");
let err = bind_singleton_hardened(&link)
.await
.expect_err("a symlinked socket path must stop the bind");
assert!(
matches!(err, UdsSecurityError::NotASocketFile { ref found, .. } if found == "symlink"),
"expected NotASocketFile naming a symlink, got {err:?}"
);
assert!(
std::fs::symlink_metadata(&link).is_ok(),
"the symlink itself must survive"
);
assert!(
std::fs::symlink_metadata(&target).is_ok(),
"the symlink's target must survive"
);
}
#[tokio::test]
async fn bind_singleton_hardened_refuses_a_symlink_to_a_live_socket() {
let tmp = tempfile::tempdir().expect("tempdir");
let dir = tmp.path().join("sockets");
let target = dir.join("live.sock");
let _live = bind_hardened(&target).expect("bind the live owner");
let link = dir.join("link.sock");
std::os::unix::fs::symlink(&target, &link).expect("symlink onto the live socket");
let err = bind_singleton_hardened(&link)
.await
.expect_err("a symlinked socket path must stop the bind");
assert!(
matches!(err, UdsSecurityError::NotASocketFile { ref found, .. } if found == "symlink"),
"expected NotASocketFile naming a symlink, got {err:?}"
);
assert!(
std::fs::symlink_metadata(&link).is_ok(),
"the symlink itself must survive"
);
assert!(
socket_is_serving(&target, Duration::from_millis(500)).await,
"the live owner must still be answering on its own path"
);
}
#[tokio::test]
async fn bind_singleton_refuses_a_directory_on_the_socket_path() {
let tmp = tempfile::tempdir().expect("tempdir");
let dir = tmp.path().join("sockets");
let sock = dir.join("occupied.sock");
std::fs::create_dir_all(&sock).expect("occupy the socket path with a directory");
let err = bind_singleton_hardened(&sock)
.await
.expect_err("a directory on the socket path must stop the bind");
assert!(
matches!(err, UdsSecurityError::NotASocketFile { ref found, .. } if found == "directory"),
"expected NotASocketFile naming a directory, got {err:?}"
);
assert!(sock.is_dir(), "the directory must survive");
}
const FILL_CHUNK: usize = 8 * 1024;
fn bytes_in_flight(stream: &tokio::net::UnixStream) -> usize {
let chunk = vec![0u8; FILL_CHUNK];
let mut written = 0usize;
loop {
match stream.try_write(&chunk) {
Ok(0) => return written,
Ok(n) => written += n,
Err(_) => return written,
}
}
}
fn round_trips(in_flight: usize) -> usize {
let frame = MAX_FRAME_BYTES as usize;
if in_flight == 0 {
return frame;
}
frame.div_ceil(in_flight)
}
fn expect_sized(outcome: SocketBufferOutcome) -> SocketBufferSizes {
match outcome {
SocketBufferOutcome::Sized(sizes) => sizes,
other => panic!("expected a sized socket, got {other:?}"),
}
}
#[tokio::test]
async fn tune_connected_buffers_raises_both_buffers_on_a_socketpair() {
let (a, _b) = tokio::net::UnixStream::pair().expect("socketpair");
let sized = expect_sized(tune_connected_buffers(&a).expect("size the buffers"));
assert!(
sized.send >= FILL_CHUNK * 2 && sized.recv >= FILL_CHUNK * 2,
"expected both buffers well above the 8 KiB macOS default, got {sized:?}"
);
}
#[tokio::test]
async fn tune_connected_buffers_reports_a_peer_that_already_hung_up() {
let (a, b) = tokio::net::UnixStream::pair().expect("socketpair");
drop(b);
let outcome = tune_connected_buffers(&a).expect("a dead peer is not a sizing failure");
assert!(
matches!(
outcome,
SocketBufferOutcome::PeerHungUp | SocketBufferOutcome::Sized(_)
),
"unexpected outcome {outcome:?}"
);
#[cfg(target_os = "macos")]
assert_eq!(outcome, SocketBufferOutcome::PeerHungUp);
}
#[tokio::test]
async fn a_listener_can_never_classify_a_failure_as_a_hung_up_peer() {
let tmp = tempfile::tempdir().expect("tempdir");
let sock = tmp.path().join("sockets").join("classify.sock");
let listener = bind_hardened(&sock).expect("bind hardened");
let fd = std::os::fd::AsRawFd::as_raw_fd(&listener);
let einval = UdsSecurityError::SocketBuffer {
option: "SO_SNDBUF",
requested: SOCKET_BUFFER_BYTES,
source: std::io::Error::from_raw_os_error(libc::EINVAL),
};
assert!(
sockbuf::hangup_is_benign(true, &einval, fd),
"a listener has no peer, so the connected form would wave this through — \
which is exactly why the bind path must not use it"
);
assert!(
!sockbuf::hangup_is_benign(false, &einval, fd),
"the listener form must report the failure, whatever the errno says"
);
}
#[tokio::test]
async fn tune_listener_buffers_refuses_where_the_connected_form_tolerates() {
let (strict_side, peer) = tokio::net::UnixStream::pair().expect("socketpair");
drop(peer);
let (lenient_side, peer) = tokio::net::UnixStream::pair().expect("socketpair");
drop(peer);
let strict = tune_listener_buffers(&strict_side);
let lenient = tune_connected_buffers(&lenient_side).expect("the forgiving form tolerates it");
#[cfg(target_os = "macos")]
{
assert_eq!(lenient, SocketBufferOutcome::PeerHungUp);
let err = strict.expect_err("the listener form must not absorb a sizing failure");
assert!(
matches!(err, UdsSecurityError::SocketBuffer { .. }),
"expected SocketBuffer, got {err:?}"
);
}
#[cfg(not(target_os = "macos"))]
{
assert!(strict.is_ok(), "linux sizes a hung-up socket: {strict:?}");
assert!(matches!(lenient, SocketBufferOutcome::Sized(_)));
}
}
#[test]
fn a_read_back_failure_names_its_own_operation() {
let bad = std::os::unix::net::UnixStream::pair()
.expect("socketpair")
.0;
let fd = std::os::fd::AsRawFd::as_raw_fd(&bad);
drop(bad);
let err = socket_buffer_sizes(&BorrowedFdForTest(fd))
.expect_err("a closed fd cannot answer getsockopt");
assert!(
matches!(err, UdsSecurityError::SocketBufferRead { .. }),
"expected SocketBufferRead, got {err:?}"
);
let text = err.to_string();
assert!(
text.starts_with("read SO_SNDBUF back"),
"a read failure must not be reported as a set: {text}"
);
}
struct BorrowedFdForTest(i32);
impl std::os::fd::AsRawFd for BorrowedFdForTest {
fn as_raw_fd(&self) -> i32 {
self.0
}
}
#[tokio::test]
async fn accept_sized_raises_the_accepted_socket_to_the_listeners_sizing() {
let tmp = tempfile::tempdir().expect("tempdir");
let sock = tmp.path().join("sockets").join("inherit.sock");
let listener = bind_hardened(&sock).expect("bind hardened");
let _client = connect_hardened(&sock).await.expect("connect hardened");
let (server, _) = accept_sized(&listener).await.expect("accept sized");
let on_listener = socket_buffer_sizes(&listener).expect("read the listener");
let accepted = socket_buffer_sizes(&server).expect("read the accepted socket");
assert_eq!(
accepted, on_listener,
"an accepted socket must carry the same granted sizing as its listener"
);
assert!(
accepted.send >= FILL_CHUNK * 2 && accepted.recv >= FILL_CHUNK * 2,
"the accepted socket must carry the raised sizing, not the platform default: {accepted:?}"
);
}
#[test]
fn socket_buffer_request_stays_within_the_frame_budget() {
assert_eq!(SOCKET_BUFFER_BYTES, 1024 * 1024);
assert!((SOCKET_BUFFER_BYTES as u64) < MAX_FRAME_BYTES);
}
#[tokio::test]
async fn hardened_sockets_hold_far_more_in_flight_than_the_platform_default() {
let tmp = tempfile::tempdir().expect("tempdir");
let plain_path = tmp.path().join("plain.sock");
let plain_listener = tokio::net::UnixListener::bind(&plain_path).expect("bind plain");
let plain_client = tokio::net::UnixStream::connect(&plain_path)
.await
.expect("connect plain");
let (_plain_server, _) = plain_listener.accept().await.expect("accept plain");
let plain = bytes_in_flight(&plain_client);
let sized_path = tmp.path().join("sockets").join("sized.sock");
let sized_listener = bind_hardened(&sized_path).expect("bind hardened");
let sized_client = connect_hardened(&sized_path)
.await
.expect("connect hardened");
let (_sized_server, _) = sized_listener.accept().await.expect("accept hardened");
let sized = bytes_in_flight(&sized_client);
eprintln!(
"#6896 in-flight bytes: default={plain} ({} round trips for an 8 MiB frame), \
sized={sized} ({} round trips)",
round_trips(plain),
round_trips(sized)
);
assert!(
sized >= plain,
"sizing must never reduce what a socket holds in flight: \
default={plain}, sized={sized}"
);
#[cfg(target_os = "macos")]
assert!(
sized >= plain * 8 && round_trips(sized) * 8 <= round_trips(plain),
"expected at least an 8x drop in round trips on macOS: \
default={plain} ({} round trips), sized={sized} ({} round trips)",
round_trips(plain),
round_trips(sized)
);
}