fix(store): a root too deep for a socket's address is served on macOS too
A Unix socket's address holds about 104 bytes. Linux reached a deep
root's sockets through /proc/self/fd; every other Unix refused the
root ("too long for its sockets"), which the portable macOS job met
with a test home under the runner's temp folder, and a Mac with a deep
home would meet too. On those systems the socket path now goes through
a short link of this account's, /tmp/wanderbooks-<uid>/<hash of the
dir>, in a folder made 0700 and refused unless it is this account's
own; a stale or foreign link in its place is replaced or refused. The
server and its clients compute the same link, so nothing else changes.
Tests (tests/socket_path.rs, every Unix): a deep root gets a path that
fits and reaches the dir; the link is private, stable, and replaces a
stale one of ours.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
This commit is contained in:
1 parent
1da01c3123
commit
74f1691f77
2 files changed
+147
-3
No files matched your search
@@ -0,0 +1,68 @@
|
||||
//! Socket paths for roots too deep for a Unix socket's address (about 104
|
||||
//! bytes): Linux reaches them through `/proc/self/fd`, the other Unixes
|
||||
//! through a short private link under `/tmp`, so a books root in a deep
|
||||
//! home is served like any other (the portable macOS job found the
|
||||
//! refusal).
|
||||
#![cfg(unix)]
|
||||
|
||||
use ledger_store::root::{SocketPath, short_link_for};
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn deep_dir(tag: &str) -> PathBuf {
|
||||
let deep = std::env::temp_dir()
|
||||
.join(format!("wb-socket-{tag}-{}", std::process::id()))
|
||||
.join("a".repeat(60))
|
||||
.join("b".repeat(60))
|
||||
.join("run");
|
||||
std::fs::create_dir_all(&deep).unwrap();
|
||||
deep
|
||||
}
|
||||
|
||||
/// A root deeper than the address allows still gets a socket path that
|
||||
/// fits, and a second asker (a client) reaches the same place.
|
||||
#[test]
|
||||
fn a_deep_root_gets_a_socket_path_that_fits() {
|
||||
let deep = deep_dir("fits");
|
||||
assert!(deep.as_os_str().len() > 100);
|
||||
let a = SocketPath::new(&deep, "mcp.sock").unwrap();
|
||||
assert!(a.path().as_os_str().len() <= 100, "{:?}", a.path());
|
||||
// The place it reaches is the dir itself.
|
||||
let reached = std::fs::canonicalize(a.path().parent().unwrap()).unwrap();
|
||||
assert_eq!(reached, std::fs::canonicalize(&deep).unwrap());
|
||||
}
|
||||
|
||||
/// The short link is this account's, names the dir exactly, is the same
|
||||
/// on a second ask, and replaces a stale link of ours in its place.
|
||||
#[test]
|
||||
fn the_short_link_is_private_stable_and_replaces_a_stale_one() {
|
||||
use std::os::unix::fs::{MetadataExt, PermissionsExt};
|
||||
let deep = deep_dir("link");
|
||||
let link = short_link_for(&deep).unwrap();
|
||||
assert!(
|
||||
link.as_os_str().len() + "/lease.sock".len() <= 100,
|
||||
"{link:?}"
|
||||
);
|
||||
// SAFETY: getuid has no preconditions and never fails.
|
||||
let uid = unsafe { libc::getuid() };
|
||||
assert_eq!(
|
||||
link.parent().unwrap(),
|
||||
PathBuf::from(format!("/tmp/wanderbooks-{uid}"))
|
||||
);
|
||||
let folder = std::fs::symlink_metadata(link.parent().unwrap()).unwrap();
|
||||
assert_eq!(folder.uid(), uid);
|
||||
assert_eq!(folder.permissions().mode() & 0o777, 0o700);
|
||||
assert_eq!(
|
||||
std::fs::read_link(&link).unwrap(),
|
||||
std::fs::canonicalize(&deep).unwrap()
|
||||
);
|
||||
assert_eq!(short_link_for(&deep).unwrap(), link);
|
||||
// A stale link of ours (to somewhere else) in that place is replaced.
|
||||
let elsewhere = deep_dir("elsewhere");
|
||||
std::fs::remove_file(&link).unwrap();
|
||||
std::os::unix::fs::symlink(&elsewhere, &link).unwrap();
|
||||
assert_eq!(short_link_for(&deep).unwrap(), link);
|
||||
assert_eq!(
|
||||
std::fs::read_link(&link).unwrap(),
|
||||
std::fs::canonicalize(&deep).unwrap()
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user