brokerd makes a socket's directory 0700. With a socket directly in / it would chmod /, and through a symbolic link it would change the link's target. Both are now refused at start with #brokerd-start-failed. Without the fix the link case started and served, with the shared directory made private. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
180 lines
5.5 KiB
Rust
180 lines
5.5 KiB
Rust
//! `brokerd serve` fails closed at startup with a runbook pointer for every reason it cannot start,
|
|
//! and never panics on its own command line. Found in the M3a review (findings 3 and 7).
|
|
|
|
#[path = "support/tmp.rs"]
|
|
mod tmp;
|
|
|
|
use std::ffi::OsStr;
|
|
use std::os::unix::ffi::OsStrExt;
|
|
use std::path::Path;
|
|
use std::process::{Command, Output};
|
|
|
|
use tmp::TempDir;
|
|
|
|
const START_FAILED: &str = "see docs/runbook.md#brokerd-start-failed";
|
|
|
|
fn brokerd<I: IntoIterator<Item = S>, S: AsRef<OsStr>>(args: I) -> Output {
|
|
Command::new(env!("CARGO_BIN_EXE_brokerd"))
|
|
.args(args)
|
|
.output()
|
|
.unwrap()
|
|
}
|
|
|
|
fn stderr(out: &Output) -> String {
|
|
String::from_utf8_lossy(&out.stderr).into_owned()
|
|
}
|
|
|
|
/// A config whose home is `dir`, with `sockets` as the `[sockets]` table's body.
|
|
fn config(dir: &TempDir, sockets: &str) -> std::path::PathBuf {
|
|
let home = dir.path().join("home");
|
|
let grants = dir.path().join("grants");
|
|
std::fs::create_dir_all(&grants).unwrap();
|
|
std::fs::create_dir_all(&home).unwrap();
|
|
dir.write(
|
|
"brokerd.toml",
|
|
&format!(
|
|
"[paths]\nhome = \"{}\"\ngrants = \"{}\"\n[sockets]\n{sockets}\n",
|
|
home.display(),
|
|
grants.display()
|
|
),
|
|
)
|
|
}
|
|
|
|
fn fails_with_pointer(out: &Output) {
|
|
assert_eq!(out.status.code(), Some(1), "{}", stderr(out));
|
|
assert!(
|
|
stderr(out).trim_end().ends_with(START_FAILED),
|
|
"{:?} must end with {START_FAILED:?}",
|
|
stderr(out)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_missing_config_names_the_entry() {
|
|
let dir = TempDir::new("ptr-missing");
|
|
let out = brokerd([
|
|
OsStr::new("serve"),
|
|
OsStr::new("--config"),
|
|
dir.path().join("nope.toml").as_os_str(),
|
|
]);
|
|
fails_with_pointer(&out);
|
|
}
|
|
|
|
#[test]
|
|
fn an_invalid_config_names_the_entry() {
|
|
let dir = TempDir::new("ptr-invalid");
|
|
let path = dir.write("brokerd.toml", "[paths]\nhome = 3\n");
|
|
let out = brokerd([
|
|
OsStr::new("serve"),
|
|
OsStr::new("--config"),
|
|
path.as_os_str(),
|
|
]);
|
|
fails_with_pointer(&out);
|
|
}
|
|
|
|
#[test]
|
|
fn a_socket_that_cannot_be_bound_names_the_entry() {
|
|
let dir = TempDir::new("ptr-bind");
|
|
let long = dir.path().join("s".repeat(120)).join("broker.sock");
|
|
let path = config(&dir, &format!("broker = \"{}\"", long.display()));
|
|
fails_with_pointer(&brokerd([
|
|
OsStr::new("serve"),
|
|
OsStr::new("--config"),
|
|
path.as_os_str(),
|
|
]));
|
|
}
|
|
|
|
#[test]
|
|
fn a_socket_directory_that_cannot_be_made_names_the_entry() {
|
|
let dir = TempDir::new("ptr-dir");
|
|
let file = dir.write("a-file", "");
|
|
let path = config(
|
|
&dir,
|
|
&format!("broker = \"{}\"", file.join("run/broker.sock").display()),
|
|
);
|
|
fails_with_pointer(&brokerd([
|
|
OsStr::new("serve"),
|
|
OsStr::new("--config"),
|
|
path.as_os_str(),
|
|
]));
|
|
}
|
|
|
|
/// The socket's directory is made private (0700). Through a symbolic link that would change the
|
|
/// directory it points to, so a link is refused, and the target keeps its mode.
|
|
#[test]
|
|
fn a_socket_directory_that_is_a_symbolic_link_is_refused() {
|
|
use std::os::unix::fs::PermissionsExt;
|
|
let dir = TempDir::new("ptr-link");
|
|
let shared = dir.path().join("shared");
|
|
std::fs::create_dir(&shared).unwrap();
|
|
std::fs::set_permissions(&shared, std::fs::Permissions::from_mode(0o755)).unwrap();
|
|
let link = dir.path().join("link");
|
|
std::os::unix::fs::symlink(&shared, &link).unwrap();
|
|
let path = config(
|
|
&dir,
|
|
&format!("broker = \"{}\"", link.join("broker.sock").display()),
|
|
);
|
|
|
|
let out = brokerd([
|
|
OsStr::new("serve"),
|
|
OsStr::new("--config"),
|
|
path.as_os_str(),
|
|
]);
|
|
fails_with_pointer(&out);
|
|
assert!(stderr(&out).contains("symbolic link"), "{}", stderr(&out));
|
|
let mode = std::fs::metadata(&shared).unwrap().permissions().mode() & 0o777;
|
|
assert_eq!(mode, 0o755, "the link's target keeps its mode");
|
|
}
|
|
|
|
/// A socket directly in `/` would make `/` private: refused before anything is changed.
|
|
#[test]
|
|
fn a_socket_in_the_root_directory_is_refused() {
|
|
let dir = TempDir::new("ptr-root");
|
|
let path = config(&dir, "broker = \"/broker.sock\"");
|
|
let out = brokerd([
|
|
OsStr::new("serve"),
|
|
OsStr::new("--config"),
|
|
path.as_os_str(),
|
|
]);
|
|
fails_with_pointer(&out);
|
|
assert!(
|
|
stderr(&out).contains("directory of its own"),
|
|
"{}",
|
|
stderr(&out)
|
|
);
|
|
}
|
|
|
|
/// A config path that is not UTF-8 is still a path: it is read (here: not found), not a panic.
|
|
#[test]
|
|
fn a_config_path_that_is_not_utf8_is_read_as_a_path() {
|
|
let dir = TempDir::new("ptr-os");
|
|
let name = dir.path().join(OsStr::from_bytes(b"conf-\xff.toml"));
|
|
let out = brokerd([
|
|
OsStr::new("serve"),
|
|
OsStr::new("--config"),
|
|
name.as_os_str(),
|
|
]);
|
|
fails_with_pointer(&out);
|
|
}
|
|
|
|
/// A flag that is not UTF-8 is not a flag brokerd knows: usage, exit 2, no panic.
|
|
#[test]
|
|
fn a_flag_that_is_not_utf8_is_a_usage_error() {
|
|
let out = brokerd([OsStr::new("serve"), OsStr::from_bytes(b"--\xff")]);
|
|
assert_eq!(out.status.code(), Some(2), "{}", stderr(&out));
|
|
assert!(stderr(&out).starts_with("usage: brokerd serve"));
|
|
let out = brokerd([OsStr::from_bytes(b"\xff")]);
|
|
assert_eq!(out.status.code(), Some(2), "{}", stderr(&out));
|
|
}
|
|
|
|
#[test]
|
|
fn the_entries_exist() {
|
|
let runbook = std::fs::read_to_string(
|
|
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../docs/runbook.md"),
|
|
)
|
|
.unwrap();
|
|
for entry in ["## brokerd-start-failed", "## brokerd-listener-lost"] {
|
|
assert!(runbook.lines().any(|l| l == entry), "{entry}");
|
|
}
|
|
}
|