mirror of
https://github.com/telemt/telemt.git
synced 2026-10-11 03:45:58 +03:00
Descriptor-anchored Secured Filesystem Operations
This commit is contained in:
+95
-194
@@ -1,6 +1,8 @@
|
||||
use std::fs::{self, File, OpenOptions};
|
||||
use std::io::{ErrorKind, Read, Write};
|
||||
use std::os::unix::fs::{MetadataExt, OpenOptionsExt};
|
||||
#[cfg(target_os = "linux")]
|
||||
use std::os::fd::{FromRawFd, OwnedFd};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use nix::fcntl::{Flock, FlockArg};
|
||||
@@ -281,7 +283,19 @@ pub fn signal_pid_file<P: AsRef<Path>>(
|
||||
path: P,
|
||||
signal: nix::sys::signal::Signal,
|
||||
) -> Result<(), DaemonError> {
|
||||
let pid = read_pid_file(&path)?;
|
||||
let path = path.as_ref();
|
||||
let pid = read_pid_file(path)?;
|
||||
#[cfg(target_os = "linux")]
|
||||
let pidfd = open_pidfd(pid)?;
|
||||
if !daemon_lock_is_held(path)? {
|
||||
return Err(DaemonError::PidFile(format!(
|
||||
"refusing to signal unlocked or stale PID file {}",
|
||||
path.display()
|
||||
)));
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
return signal_pidfd(&pidfd, pid, signal);
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
nix::sys::signal::kill(Pid::from_raw(pid), signal)
|
||||
.map_err(|error| DaemonError::PidFile(format!("cannot signal process {}: {}", pid, error)))
|
||||
}
|
||||
@@ -303,206 +317,93 @@ pub enum DaemonStatus {
|
||||
pub fn check_status<P: AsRef<Path>>(path: P) -> DaemonStatus {
|
||||
let path = path.as_ref();
|
||||
match read_pid_file_if_exists(path) {
|
||||
Ok(Some(pid)) if is_process_running(pid) => DaemonStatus::Running(pid),
|
||||
Ok(Some(pid))
|
||||
if daemon_lock_is_held(path).unwrap_or(false) && is_process_running(pid) =>
|
||||
{
|
||||
DaemonStatus::Running(pid)
|
||||
}
|
||||
Ok(Some(pid)) => DaemonStatus::Stale(pid),
|
||||
Ok(None) | Err(_) => DaemonStatus::NotRunning,
|
||||
}
|
||||
}
|
||||
|
||||
fn daemon_lock_is_held(path: &Path) -> Result<bool, DaemonError> {
|
||||
let lock_path = sibling_lock_path(path);
|
||||
let file = match OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.custom_flags(libc::O_CLOEXEC | libc::O_NOFOLLOW)
|
||||
.open(&lock_path)
|
||||
{
|
||||
Ok(file) => file,
|
||||
Err(error) if error.kind() == ErrorKind::NotFound => return Ok(false),
|
||||
Err(error) => {
|
||||
return Err(DaemonError::PidFile(format!(
|
||||
"cannot inspect lock {}: {}",
|
||||
lock_path.display(),
|
||||
error
|
||||
)));
|
||||
}
|
||||
};
|
||||
validate_regular_single_link(&file, &lock_path)?;
|
||||
match Flock::lock(file, FlockArg::LockExclusiveNonblock) {
|
||||
Ok(_available) => Ok(false),
|
||||
Err((_file, nix::errno::Errno::EWOULDBLOCK)) => Ok(true),
|
||||
Err((_file, error)) => Err(DaemonError::PidFile(format!(
|
||||
"cannot inspect lock ownership for {}: {}",
|
||||
lock_path.display(),
|
||||
error
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn open_pidfd(pid: i32) -> Result<OwnedFd, DaemonError> {
|
||||
// SAFETY: `pidfd_open` receives a validated positive PID and no pointer arguments.
|
||||
let descriptor = unsafe { libc::syscall(libc::SYS_pidfd_open, pid, 0) };
|
||||
if descriptor < 0 {
|
||||
return Err(DaemonError::PidFile(format!(
|
||||
"cannot open stable process handle for {}: {}",
|
||||
pid,
|
||||
std::io::Error::last_os_error()
|
||||
)));
|
||||
}
|
||||
// SAFETY: a successful `pidfd_open` returns one newly owned descriptor.
|
||||
Ok(unsafe { OwnedFd::from_raw_fd(descriptor as i32) })
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn signal_pidfd(
|
||||
pidfd: &OwnedFd,
|
||||
pid: i32,
|
||||
signal: nix::sys::signal::Signal,
|
||||
) -> Result<(), DaemonError> {
|
||||
use std::os::fd::AsRawFd;
|
||||
|
||||
// SAFETY: the pidfd is owned and valid, and both optional pointer arguments are null.
|
||||
let result = unsafe {
|
||||
libc::syscall(
|
||||
libc::SYS_pidfd_send_signal,
|
||||
pidfd.as_raw_fd(),
|
||||
signal as libc::c_int,
|
||||
std::ptr::null::<libc::siginfo_t>(),
|
||||
0,
|
||||
)
|
||||
};
|
||||
if result == 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(DaemonError::PidFile(format!(
|
||||
"cannot signal process {} through stable handle: {}",
|
||||
pid,
|
||||
std::io::Error::last_os_error()
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
fn is_process_running(pid: i32) -> bool {
|
||||
nix::sys::signal::kill(Pid::from_raw(pid), None).is_ok()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
use std::os::unix::fs::symlink;
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use super::*;
|
||||
|
||||
const HELPER_PID_PATH: &str = "TELEMT_PID_LOCK_HELPER_PATH";
|
||||
const HELPER_READY_PATH: &str = "TELEMT_PID_LOCK_HELPER_READY";
|
||||
const HELPER_STOP_PATH: &str = "TELEMT_PID_LOCK_HELPER_STOP";
|
||||
|
||||
fn wait_for_path(path: &Path, timeout: Duration) -> bool {
|
||||
let deadline = Instant::now() + timeout;
|
||||
while Instant::now() < deadline {
|
||||
if path.exists() {
|
||||
return true;
|
||||
}
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn wait_for_child(child: &mut Child, timeout: Duration) -> Option<std::process::ExitStatus> {
|
||||
let deadline = Instant::now() + timeout;
|
||||
while Instant::now() < deadline {
|
||||
if let Some(status) = child.try_wait().unwrap() {
|
||||
return Some(status);
|
||||
}
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pid_file_remains_send_and_sync() {
|
||||
fn assert_send_sync<T: Send + Sync>() {}
|
||||
|
||||
assert_send_sync::<PidFile>();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lock_holder_subprocess() {
|
||||
let Some(pid_path) = std::env::var_os(HELPER_PID_PATH) else {
|
||||
return;
|
||||
};
|
||||
let ready_path = PathBuf::from(std::env::var_os(HELPER_READY_PATH).unwrap());
|
||||
let stop_path = PathBuf::from(std::env::var_os(HELPER_STOP_PATH).unwrap());
|
||||
let mut pid_file = PidFile::new(PathBuf::from(pid_path));
|
||||
pid_file.acquire().unwrap();
|
||||
fs::write(&ready_path, b"ready").unwrap();
|
||||
assert!(wait_for_path(&stop_path, Duration::from_secs(10)));
|
||||
pid_file.release().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn persistent_sibling_lock_serializes_processes_after_pid_unlink() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let pid_path = directory.path().join("telemt.pid");
|
||||
let lock_path = sibling_lock_path(&pid_path);
|
||||
let ready_path = directory.path().join("ready");
|
||||
let stop_path = directory.path().join("stop");
|
||||
let mut child = Command::new(std::env::current_exe().unwrap())
|
||||
.args([
|
||||
"--exact",
|
||||
"daemon::pid_file::tests::lock_holder_subprocess",
|
||||
"--nocapture",
|
||||
])
|
||||
.env(HELPER_PID_PATH, &pid_path)
|
||||
.env(HELPER_READY_PATH, &ready_path)
|
||||
.env(HELPER_STOP_PATH, &stop_path)
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
.unwrap();
|
||||
|
||||
if !wait_for_path(&ready_path, Duration::from_secs(5)) {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
panic!("PID lock holder did not become ready");
|
||||
}
|
||||
let lock_inode = fs::metadata(&lock_path).unwrap().ino();
|
||||
fs::remove_file(&pid_path).unwrap();
|
||||
|
||||
let mut contender = PidFile::new(&pid_path);
|
||||
assert!(contender.acquire().is_err());
|
||||
|
||||
fs::write(&stop_path, b"stop").unwrap();
|
||||
let status = wait_for_child(&mut child, Duration::from_secs(5)).unwrap_or_else(|| {
|
||||
let _ = child.kill();
|
||||
child.wait().unwrap()
|
||||
});
|
||||
assert!(status.success());
|
||||
assert_eq!(fs::metadata(&lock_path).unwrap().ino(), lock_inode);
|
||||
|
||||
contender.acquire().unwrap();
|
||||
assert_eq!(fs::metadata(&lock_path).unwrap().ino(), lock_inode);
|
||||
contender.release().unwrap();
|
||||
assert!(!pid_path.exists());
|
||||
assert!(lock_path.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stale_pid_checks_are_read_only() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let pid_path = directory.path().join("telemt.pid");
|
||||
fs::write(&pid_path, b"2000000000\n").unwrap();
|
||||
let pid_file = PidFile::new(&pid_path);
|
||||
|
||||
assert_eq!(pid_file.check_running().unwrap(), None);
|
||||
assert_eq!(check_status(&pid_path), DaemonStatus::Stale(2_000_000_000));
|
||||
assert!(pid_path.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unowned_release_does_not_remove_pid_file() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let pid_path = directory.path().join("telemt.pid");
|
||||
fs::write(&pid_path, b"2000000000\n").unwrap();
|
||||
let mut pid_file = PidFile::new(&pid_path);
|
||||
|
||||
pid_file.release().unwrap();
|
||||
|
||||
assert!(pid_path.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn acquire_rejects_pid_symlink_without_truncating_target() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let pid_path = directory.path().join("telemt.pid");
|
||||
let target_path = directory.path().join("target");
|
||||
fs::write(&target_path, b"preserve\n").unwrap();
|
||||
symlink(&target_path, &pid_path).unwrap();
|
||||
let mut pid_file = PidFile::new(&pid_path);
|
||||
|
||||
assert!(pid_file.acquire().is_err());
|
||||
assert_eq!(fs::read(&target_path).unwrap(), b"preserve\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_does_not_remove_replacement_path() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let pid_path = directory.path().join("telemt.pid");
|
||||
let owned_path = directory.path().join("owned.pid");
|
||||
let mut pid_file = PidFile::new(&pid_path);
|
||||
pid_file.acquire().unwrap();
|
||||
fs::rename(&pid_path, &owned_path).unwrap();
|
||||
fs::write(&pid_path, b"replacement\n").unwrap();
|
||||
|
||||
let error = pid_file.release().unwrap_err();
|
||||
|
||||
assert!(error.to_string().contains("refusing to remove replaced PID file"));
|
||||
assert_eq!(fs::read(&pid_path).unwrap(), b"replacement\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pid_parser_rejects_process_group_values() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let pid_path = directory.path().join("telemt.pid");
|
||||
|
||||
for value in ["-1\n", "0\n", "1\n"] {
|
||||
fs::write(&pid_path, value).unwrap();
|
||||
assert!(read_pid_file(&pid_path).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pid_file_release_keeps_lock_inode() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let pid_path = directory.path().join("telemt.pid");
|
||||
let lock_path = sibling_lock_path(&pid_path);
|
||||
let mut pid_file = PidFile::new(&pid_path);
|
||||
|
||||
pid_file.acquire().unwrap();
|
||||
assert!(
|
||||
pid_file
|
||||
.ownership_file_handles()
|
||||
.into_iter()
|
||||
.all(|file| file.is_some())
|
||||
);
|
||||
assert_eq!(read_pid_file(&pid_path).unwrap(), std::process::id() as i32);
|
||||
let lock_inode = fs::metadata(&lock_path).unwrap().ino();
|
||||
pid_file.release().unwrap();
|
||||
|
||||
assert!(!pid_path.exists());
|
||||
assert!(lock_path.exists());
|
||||
pid_file.acquire().unwrap();
|
||||
assert_eq!(fs::metadata(&lock_path).unwrap().ino(), lock_inode);
|
||||
pid_file.release().unwrap();
|
||||
}
|
||||
}
|
||||
mod tests;
|
||||
|
||||
Reference in New Issue
Block a user