Files
kyleandClaude Opus 5.5 e08deb39a6 brokerd: recover a torn line in place, real dates only, bounded ttl, EMFILE
From the independent review of task 23. A torn last line followed by an empty
later file had its recovery written into the later file, which broke the
chain for good; the line is now ended in its own file. The log-name rule
takes months 01 to 12 and days 01 to 31 only. [approvals] ttl_ms is limited
to a day, the longest loopd waits after a pending frame. Running out of file
descriptors or memory pauses the listener instead of stopping brokerd (the
errors the previous fix skipped do not occur on Linux). args.rs's doc fixed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-22 21:48:24 -07:00

204 lines
6.4 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}");
}
}
/// `[approvals] ttl_ms` is between 1 ms and a day: `loopd` waits at most a day after a pending
/// frame, so a longer approval would be abandoned while `bxctl` still listed it.
#[test]
fn an_approval_ttl_outside_a_day_is_a_config_error() {
for ttl in ["0", "86400001", "18446744073709551615"] {
let dir = TempDir::new("ptr-ttl");
std::fs::create_dir_all(dir.path().join("grants")).unwrap();
let path = dir.write(
"brokerd.toml",
&format!(
"[paths]\nhome = \"{0}\"\ngrants = \"{0}/grants\"\n[approvals]\nttl_ms = {ttl}\n",
dir.path().display()
),
);
let out = brokerd([
OsStr::new("serve"),
OsStr::new("--config"),
path.as_os_str(),
]);
fails_with_pointer(&out);
assert!(stderr(&out).contains("ttl_ms"), "{ttl}: {}", stderr(&out));
}
}