Runtime Paths Checks as opt-in #935

Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
Alexey
2026-09-29 19:24:29 +03:00
parent e3f62db347
commit 53f131e34d
16 changed files with 635 additions and 94 deletions
+165 -8
View File
@@ -15,6 +15,9 @@ pub fn parse_daemon_args(args: &[String]) -> DaemonOptions {
"--foreground" | "-f" => {
opts.foreground = true;
}
"--strict-runtime-paths" => {
opts.strict_runtime_paths = true;
}
"--pid-file" => {
i += 1;
if i < args.len() {
@@ -60,19 +63,21 @@ pub fn parse_daemon_args(args: &[String]) -> DaemonOptions {
}
/// Sends SIGTERM and waits briefly for graceful PID-file cleanup.
pub(super) fn stop(pid_file: &Path) -> i32 {
pub(super) fn stop(pid_file: &Path, strict_runtime_paths: bool) -> i32 {
use nix::sys::signal::Signal;
println!("Stopping telemt daemon...");
match daemon::signal_pid_file(pid_file, Signal::SIGTERM) {
match daemon::signal_pid_file(pid_file, Signal::SIGTERM, strict_runtime_paths) {
Ok(()) => {
println!("Stop signal sent successfully");
// Wait for process to exit for up to ten seconds.
for _ in 0..20 {
std::thread::sleep(std::time::Duration::from_millis(500));
if let daemon::DaemonStatus::NotRunning = daemon::check_status(pid_file) {
if let daemon::DaemonStatus::NotRunning =
daemon::check_status(pid_file, strict_runtime_paths)
{
println!("Daemon stopped");
return 0;
}
@@ -88,12 +93,12 @@ pub(super) fn stop(pid_file: &Path) -> i32 {
}
/// Sends SIGHUP to trigger configuration reload.
pub(super) fn reload(pid_file: &Path) -> i32 {
pub(super) fn reload(pid_file: &Path, strict_runtime_paths: bool) -> i32 {
use nix::sys::signal::Signal;
println!("Reloading telemt configuration...");
match daemon::signal_pid_file(pid_file, Signal::SIGHUP) {
match daemon::signal_pid_file(pid_file, Signal::SIGHUP, strict_runtime_paths) {
Ok(()) => {
println!("Reload signal sent successfully");
0
@@ -106,8 +111,8 @@ pub(super) fn reload(pid_file: &Path) -> i32 {
}
/// Reports daemon status without mutating PID lifecycle state.
pub(super) fn status(pid_file: &Path) -> i32 {
match daemon::check_status(pid_file) {
pub(super) fn status(pid_file: &Path, strict_runtime_paths: bool) -> i32 {
match daemon::check_status(pid_file, strict_runtime_paths) {
daemon::DaemonStatus::Running(pid) => {
println!("telemt is running (pid {})", pid);
0
@@ -126,16 +131,168 @@ pub(super) fn status(pid_file: &Path) -> i32 {
#[cfg(test)]
mod tests {
use std::fs;
use std::os::unix::fs::{PermissionsExt, symlink};
use std::process::{Child, Command, Stdio};
use std::time::{Duration, Instant};
use super::*;
const CONTROL_PID: &str = "TELEMT_RUNTIME_PATH_TEST_PID";
const CONTROL_READY: &str = "TELEMT_RUNTIME_PATH_TEST_READY";
const CONTROL_RELOADED: &str = "TELEMT_RUNTIME_PATH_TEST_RELOADED";
struct ControlChild(Child);
impl Drop for ControlChild {
fn drop(&mut self) {
let _ = self.0.kill();
let _ = self.0.wait();
}
}
fn wait_for_file(path: &Path) -> bool {
let deadline = Instant::now() + Duration::from_secs(5);
while Instant::now() < deadline {
if path.exists() {
return true;
}
std::thread::sleep(Duration::from_millis(10));
}
false
}
#[test]
fn runtime_path_policy_defaults_to_compatibility_for_all_commands() {
for command in ["run", "start", "stop", "reload", "status"] {
let args = vec![command.to_string(), "config.toml".to_string()];
assert!(
!crate::cli::parse_command(&args)
.daemon_opts
.strict_runtime_paths
);
assert!(!parse_daemon_args(&args).strict_runtime_paths);
for position in [1, args.len()] {
let mut strict_args = args.clone();
strict_args.insert(position, "--strict-runtime-paths".to_string());
let parsed = crate::cli::parse_command(&strict_args);
assert!(parsed.daemon_opts.strict_runtime_paths);
assert!(parse_daemon_args(&strict_args).strict_runtime_paths);
assert_eq!(parsed.config_path, "config.toml");
}
}
let args = vec![
"--strict-runtime-paths".to_string(),
"config.toml".to_string(),
];
let parsed = crate::cli::parse_command(&args);
assert_eq!(parsed.subcommand, crate::cli::Subcommand::Run);
assert!(parsed.daemon_opts.strict_runtime_paths);
assert_eq!(parsed.config_path, "config.toml");
}
#[tokio::test(flavor = "current_thread")]
async fn daemon_control_subprocess() {
let Some(pid_path) = std::env::var_os(CONTROL_PID) else {
return;
};
let ready = PathBuf::from(std::env::var_os(CONTROL_READY).unwrap());
let reloaded = PathBuf::from(std::env::var_os(CONTROL_RELOADED).unwrap());
let mut terminate =
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()).unwrap();
let mut reload =
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::hangup()).unwrap();
let mut owner = daemon::PidFile::new(PathBuf::from(pid_path), false);
owner.acquire().unwrap();
fs::write(&ready, b"ready").unwrap();
let deadline = tokio::time::sleep(Duration::from_secs(15));
tokio::pin!(deadline);
loop {
tokio::select! {
_ = terminate.recv() => break,
_ = reload.recv() => fs::write(&reloaded, b"reloaded").unwrap(),
_ = &mut deadline => panic!("daemon control subprocess timed out"),
}
}
owner.release().unwrap();
}
#[test]
fn control_commands_follow_runtime_path_policy() {
for (mode, linked_parent, trusted_parent) in [
(0o777, false, false),
(0o777, true, false),
(0o755, false, true),
] {
let root = tempfile::tempdir().unwrap();
let real = root.path().join("run");
let linked = root.path().join("linked");
fs::create_dir(&real).unwrap();
fs::set_permissions(&real, fs::Permissions::from_mode(mode)).unwrap();
symlink(&real, &linked).unwrap();
let pid_path = if linked_parent { &linked } else { &real }.join("telemt.pid");
let ready = root.path().join("ready");
let reloaded = root.path().join("reloaded");
let mut child = ControlChild(
Command::new(std::env::current_exe().unwrap())
.args([
"--exact",
"cli::daemon_commands::tests::daemon_control_subprocess",
"--nocapture",
])
.env(CONTROL_PID, &pid_path)
.env(CONTROL_READY, &ready)
.env(CONTROL_RELOADED, &reloaded)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.unwrap(),
);
assert!(
wait_for_file(&ready),
"daemon control subprocess did not become ready"
);
let command = |name: &str, strict: bool| {
let mut args = vec![
name.to_string(),
"--pid-file".to_string(),
pid_path.to_str().unwrap().to_string(),
];
if strict {
args.push("--strict-runtime-paths".to_string());
}
crate::cli::execute_subcommand(&crate::cli::parse_command(&args))
};
assert_eq!(command("status", false), Some(0));
if trusted_parent {
assert_eq!(command("status", true), Some(0));
assert_eq!(command("reload", true), Some(0));
assert!(wait_for_file(&reloaded));
assert_eq!(command("stop", true), Some(0));
} else {
assert_eq!(command("status", true), Some(1));
assert_eq!(command("reload", true), Some(1));
assert!(!reloaded.exists());
assert_eq!(command("stop", true), Some(1));
assert_eq!(command("status", false), Some(0));
assert_eq!(command("reload", false), Some(0));
assert!(wait_for_file(&reloaded));
assert_eq!(command("stop", false), Some(0));
}
assert!(child.0.wait().unwrap().success());
assert!(!pid_path.exists());
assert_eq!(command("status", false), Some(1));
}
}
#[test]
fn status_does_not_remove_stale_pid_file() {
let directory = tempfile::tempdir().unwrap();
let pid_file = directory.path().join("telemt.pid");
fs::write(&pid_file, b"2000000000\n").unwrap();
assert_eq!(status(&pid_file), 1);
assert_eq!(status(&pid_file, false), 1);
assert!(pid_file.exists());
}
}