use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use serde::{Deserialize, Serialize};
const START_TIME_EPSILON_SECS: u64 = 2;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct WalpinHeartbeat {
pub pid: u32,
pub process_role: String,
pub started_at: i64,
pub oldest_tx_age_secs: f64,
pub oldest_tx_label: Option<String>,
#[serde(default)]
pub oldest_tx_started_at: Option<i64>,
pub updated_at: i64,
#[serde(default, alias = "interval_ms")]
pub sweep_interval_ms: u64,
#[serde(default)]
pub attribution_basis: Option<String>,
}
impl WalpinHeartbeat {
pub fn attribution_is_evidence_backed(&self) -> bool {
self.attribution_basis.as_deref() == Some("origin")
}
pub fn current_oldest_tx_age_secs(&self, now_epoch_secs: i64) -> f64 {
match self.oldest_tx_started_at {
Some(started_at) => (now_epoch_secs - started_at).max(0) as f64,
None => self.oldest_tx_age_secs,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct LiveWalpinEntry {
pub heartbeat: WalpinHeartbeat,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct WalpinBeacon {
pub pid: u32,
pub process_role: String,
pub started_at: i64,
#[serde(default, alias = "interval_ms")]
pub sweep_interval_ms: u64,
}
#[derive(Debug, Clone, PartialEq)]
pub enum WalpinPidHealth {
Reporting(WalpinHeartbeat),
RegisteredSilent { pid: u32 },
Unknown { pid: u32, reason: &'static str },
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct WalpinReport {
pub entries: Vec<WalpinPidHealth>,
}
impl WalpinReport {
pub fn reporting(&self) -> impl Iterator<Item = &WalpinHeartbeat> {
self.entries.iter().filter_map(|e| match e {
WalpinPidHealth::Reporting(hb) => Some(hb),
_ => None,
})
}
pub fn registered_silent_pids(&self) -> impl Iterator<Item = u32> + '_ {
self.entries.iter().filter_map(|e| match e {
WalpinPidHealth::RegisteredSilent { pid } => Some(*pid),
_ => None,
})
}
pub fn unknown_pids(&self) -> impl Iterator<Item = u32> + '_ {
self.entries.iter().filter_map(|e| match e {
WalpinPidHealth::Unknown { pid, .. } => Some(*pid),
_ => None,
})
}
pub fn fully_attributed(&self) -> bool {
self.unknown_pids().next().is_none()
}
}
fn io_other(msg: impl Into<String>) -> io::Error {
io::Error::other(msg.into())
}
pub fn sidecar_dir_for(db_path: &Path) -> PathBuf {
let mut file = db_path.file_name().unwrap_or_default().to_os_string();
file.push(".walpin");
match db_path.parent() {
Some(parent) => parent.join(file),
None => PathBuf::from(file),
}
}
pub fn sidecar_enabled(is_file_backed: bool) -> bool {
match std::env::var("KHIVE_WALPIN_SIDECAR") {
Ok(raw) => match raw.trim().to_ascii_lowercase().as_str() {
"1" | "true" | "yes" | "on" => true,
"0" | "false" | "no" | "off" => false,
_ => is_file_backed,
},
Err(_) => is_file_backed,
}
}
#[cfg(unix)]
mod unix_impl {
use super::io_other;
use std::ffi::{CStr, CString};
use std::fs;
use std::io::{self, Read, Write};
use std::os::unix::ffi::OsStrExt;
use std::os::unix::io::{AsRawFd, FromRawFd, RawFd};
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};
pub(super) const MAX_SIDECAR_ENTRY_BYTES: u64 = 64 * 1024;
const MAX_ANCESTOR_SYMLINK_DEPTH: u32 = 8;
const RAW_SCAN_FACTOR: usize = 8;
pub(super) fn current_uid() -> u32 {
unsafe { libc::geteuid() }
}
fn path_cstring(path: &Path) -> io::Result<CString> {
CString::new(path.as_os_str().as_bytes())
.map_err(|_| io_other(format!("path {path:?} contains an interior NUL byte")))
}
fn name_cstring(name: &str) -> io::Result<CString> {
CString::new(name)
.map_err(|_| io_other(format!("sidecar entry name {name:?} contains a NUL byte")))
}
fn name_cstring_os(name: &std::ffi::OsStr) -> io::Result<CString> {
CString::new(name.as_bytes())
.map_err(|_| io_other(format!("sidecar entry name {name:?} contains a NUL byte")))
}
fn is_symlink_mode(mode: libc::mode_t) -> bool {
(mode & libc::S_IFMT) == libc::S_IFLNK
}
pub(super) struct SidecarDirHandle(fs::File);
impl SidecarDirHandle {
fn raw(&self) -> RawFd {
self.0.as_raw_fd()
}
pub(super) fn open_or_create(dir: &Path) -> io::Result<Self> {
let (parent_fd, c_name) = Self::open_parent_and_name(dir)?;
match Self::open_validated_at(&parent_fd, &c_name, dir) {
Ok(handle) => Ok(handle),
Err(e) if e.kind() == io::ErrorKind::NotFound => {
let rc =
unsafe { libc::mkdirat(parent_fd.as_raw_fd(), c_name.as_ptr(), 0o700) };
if rc != 0 {
let err = io::Error::last_os_error();
if err.kind() != io::ErrorKind::AlreadyExists {
return Err(err);
}
}
Self::open_validated_at(&parent_fd, &c_name, dir)
}
Err(e) => Err(e),
}
}
pub(super) fn open_if_exists(dir: &Path) -> io::Result<Option<Self>> {
let (parent_fd, c_name) = Self::open_parent_and_name(dir)?;
match Self::open_validated_at(&parent_fd, &c_name, dir) {
Ok(handle) => Ok(Some(handle)),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(e),
}
}
fn open_parent_and_name(dir: &Path) -> io::Result<(fs::File, CString)> {
let parent = match dir.parent() {
Some(p) if !p.as_os_str().is_empty() => p,
_ => Path::new("."),
};
let name = dir.file_name().ok_or_else(|| {
io_other(format!(
"walpin sidecar path {dir:?} has no final path component"
))
})?;
let parent_file = Self::open_dir_component_walk(parent)?;
let c_name = name_cstring_os(name)?;
Ok((parent_file, c_name))
}
fn open_dir_component_walk(path: &Path) -> io::Result<fs::File> {
let start = Self::open_anchor(path.is_absolute())?;
let mut budget = MAX_ANCESTOR_SYMLINK_DEPTH;
Self::walk_components(start, path, &mut budget)
}
fn open_anchor(absolute: bool) -> io::Result<fs::File> {
let anchor = if absolute { "/" } else { "." };
let c_anchor = path_cstring(Path::new(anchor))?;
let fd = unsafe {
libc::open(
c_anchor.as_ptr(),
libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_CLOEXEC,
)
};
if fd < 0 {
return Err(io::Error::last_os_error());
}
Ok(unsafe { fs::File::from_raw_fd(fd) })
}
fn walk_components(start: fs::File, path: &Path, budget: &mut u32) -> io::Result<fs::File> {
use std::path::Component;
let mut current = start;
for component in path.components() {
let name = match component {
Component::RootDir | Component::CurDir | Component::Prefix(_) => continue,
Component::ParentDir => std::ffi::OsStr::new(".."),
Component::Normal(c) => c,
};
current = Self::open_component(current, name, budget)?;
}
Ok(current)
}
fn open_component(
dir: fs::File,
name: &std::ffi::OsStr,
budget: &mut u32,
) -> io::Result<fs::File> {
let c_name = name_cstring_os(name)?;
let next_fd = unsafe {
libc::openat(
dir.as_raw_fd(),
c_name.as_ptr(),
libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_CLOEXEC,
)
};
if next_fd >= 0 {
return Ok(unsafe { fs::File::from_raw_fd(next_fd) });
}
let open_err = io::Error::last_os_error();
let raw = open_err.raw_os_error();
if raw != Some(libc::ELOOP) && raw != Some(libc::ENOTDIR) {
return Err(open_err);
}
let mut st: libc::stat = unsafe { std::mem::zeroed() };
let rc = unsafe {
libc::fstatat(
dir.as_raw_fd(),
c_name.as_ptr(),
&mut st,
libc::AT_SYMLINK_NOFOLLOW,
)
};
if rc != 0 || !is_symlink_mode(st.st_mode) {
return Err(open_err);
}
if st.st_uid != 0 {
return Err(io_other(format!(
"walpin sidecar ancestor {name:?} is a non-root-owned symlink; refusing"
)));
}
if *budget == 0 {
return Err(io_other(format!(
"walpin sidecar ancestor {name:?} exceeded the symlink resolution depth budget"
)));
}
*budget -= 1;
let target = Self::read_link_component(&dir, &c_name)?;
let target_path = PathBuf::from(target);
let next_start = if target_path.is_absolute() {
Self::open_anchor(true)?
} else {
dir
};
Self::walk_components(next_start, &target_path, budget)
}
fn read_link_component(dir: &fs::File, c_name: &CString) -> io::Result<std::ffi::OsString> {
use std::os::unix::ffi::OsStringExt;
let mut buf = vec![0u8; libc::PATH_MAX as usize];
let rc = unsafe {
libc::readlinkat(
dir.as_raw_fd(),
c_name.as_ptr(),
buf.as_mut_ptr() as *mut libc::c_char,
buf.len(),
)
};
if rc < 0 {
return Err(io::Error::last_os_error());
}
buf.truncate(rc as usize);
Ok(std::ffi::OsString::from_vec(buf))
}
fn open_validated_at(
parent_fd: &fs::File,
c_name: &CString,
dir: &Path,
) -> io::Result<Self> {
let fd = unsafe {
libc::openat(
parent_fd.as_raw_fd(),
c_name.as_ptr(),
libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_CLOEXEC,
)
};
if fd < 0 {
let err = io::Error::last_os_error();
if err.kind() != io::ErrorKind::NotFound {
if let Ok(meta) = fs::symlink_metadata(dir) {
if meta.file_type().is_symlink() {
return Err(io_other(format!(
"walpin sidecar path {dir:?} is a symlink; refusing"
)));
}
}
}
return Err(err);
}
let handle = Self(unsafe { fs::File::from_raw_fd(fd) });
handle.validate(dir)?;
Ok(handle)
}
fn validate(&self, dir: &Path) -> io::Result<()> {
let st = self.fstat_self()?;
if (st.st_mode & libc::S_IFMT) != libc::S_IFDIR {
return Err(io_other(format!(
"walpin sidecar path {dir:?} is not a directory"
)));
}
let mode = st.st_mode & 0o777;
if mode != 0o700 {
return Err(io_other(format!(
"walpin sidecar dir {dir:?} has mode {mode:o}, expected 0700; \
refusing rather than chmod"
)));
}
if st.st_uid != current_uid() {
return Err(io_other(format!(
"walpin sidecar dir {dir:?} is not owned by the current user; refusing"
)));
}
Ok(())
}
fn fstat_self(&self) -> io::Result<libc::stat> {
let mut st: libc::stat = unsafe { std::mem::zeroed() };
let rc = unsafe { libc::fstat(self.raw(), &mut st) };
if rc != 0 {
return Err(io::Error::last_os_error());
}
Ok(st)
}
fn stat_entry(&self, name: &str) -> io::Result<Option<libc::stat>> {
let c_name = name_cstring(name)?;
let mut st: libc::stat = unsafe { std::mem::zeroed() };
let rc = unsafe {
libc::fstatat(
self.raw(),
c_name.as_ptr(),
&mut st,
libc::AT_SYMLINK_NOFOLLOW,
)
};
if rc != 0 {
let err = io::Error::last_os_error();
if err.kind() == io::ErrorKind::NotFound {
return Ok(None);
}
return Err(err);
}
Ok(Some(st))
}
pub(super) fn write_atomic(
&self,
target_name: &str,
tmp_name: &str,
body: &[u8],
) -> io::Result<()> {
if let Some(st) = self.stat_entry(target_name)? {
if is_symlink_mode(st.st_mode) {
return Err(io_other(format!(
"walpin sidecar entry {target_name:?} is a symlink; refusing to write \
through it"
)));
}
}
let _ = self.unlink_tolerant(tmp_name);
let c_tmp = name_cstring(tmp_name)?;
let fd = unsafe {
libc::openat(
self.raw(),
c_tmp.as_ptr(),
libc::O_WRONLY
| libc::O_CREAT
| libc::O_EXCL
| libc::O_NOFOLLOW
| libc::O_CLOEXEC,
0o600,
)
};
if fd < 0 {
return Err(io::Error::last_os_error());
}
{
let mut file = unsafe { fs::File::from_raw_fd(fd) };
file.write_all(body)?;
file.sync_all()?;
}
self.rename_over(tmp_name, target_name)
}
fn rename_over(&self, from: &str, to: &str) -> io::Result<()> {
let c_from = name_cstring(from)?;
let c_to = name_cstring(to)?;
let rc =
unsafe { libc::renameat(self.raw(), c_from.as_ptr(), self.raw(), c_to.as_ptr()) };
if rc != 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}
pub(super) fn unlink_tolerant(&self, name: &str) -> io::Result<()> {
let c_name = name_cstring(name)?;
let rc = unsafe { libc::unlinkat(self.raw(), c_name.as_ptr(), 0) };
if rc != 0 {
let err = io::Error::last_os_error();
if err.kind() != io::ErrorKind::NotFound {
return Err(err);
}
}
Ok(())
}
pub(super) fn remove_checked(&self, name: &str) -> io::Result<()> {
match self.stat_entry(name)? {
None => Ok(()),
Some(st) if is_symlink_mode(st.st_mode) => Err(io_other(format!(
"refusing to remove symlinked walpin sidecar entry {name:?}"
))),
Some(_) => self.unlink_tolerant(name),
}
}
pub(super) fn touch_mtime(&self, name: &str) -> io::Result<()> {
let st = self
.stat_entry(name)?
.ok_or_else(|| io_other(format!("walpin sidecar entry {name:?} does not exist")))?;
if is_symlink_mode(st.st_mode) {
return Err(io_other(format!(
"walpin sidecar entry {name:?} is a symlink; refusing to touch it"
)));
}
let c_name = name_cstring(name)?;
let fd = unsafe {
libc::openat(
self.raw(),
c_name.as_ptr(),
libc::O_WRONLY | libc::O_NOFOLLOW | libc::O_CLOEXEC | libc::O_NONBLOCK,
)
};
if fd < 0 {
return Err(io::Error::last_os_error());
}
let file = unsafe { fs::File::from_raw_fd(fd) };
if !file.metadata()?.file_type().is_file() {
return Err(io_other(format!(
"walpin sidecar entry {name:?} is not a regular file"
)));
}
let times = [
libc::timespec {
tv_sec: 0,
tv_nsec: libc::UTIME_OMIT,
},
libc::timespec {
tv_sec: 0,
tv_nsec: libc::UTIME_NOW,
},
];
let rc = unsafe { libc::futimens(file.as_raw_fd(), times.as_ptr()) };
if rc != 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}
pub(super) fn read_checked(
&self,
name: &str,
) -> io::Result<Option<(Vec<u8>, u32, SystemTime)>> {
use std::os::unix::fs::MetadataExt;
if self.stat_entry(name)?.is_none() {
return Ok(None);
}
let c_name = name_cstring(name)?;
let fd = unsafe {
libc::openat(
self.raw(),
c_name.as_ptr(),
libc::O_RDONLY | libc::O_NOFOLLOW | libc::O_CLOEXEC | libc::O_NONBLOCK,
)
};
if fd < 0 {
let err = io::Error::last_os_error();
if err.kind() == io::ErrorKind::NotFound {
return Ok(None);
}
return Err(err);
}
let file = unsafe { fs::File::from_raw_fd(fd) };
let meta = file.metadata()?;
if !meta.file_type().is_file() {
return Err(io_other(format!(
"walpin sidecar entry {name:?} is not a regular file"
)));
}
if meta.len() > MAX_SIDECAR_ENTRY_BYTES {
return Err(io_other(format!(
"walpin sidecar entry {name:?} exceeds {MAX_SIDECAR_ENTRY_BYTES} bytes"
)));
}
let mut buf = Vec::new();
(&file)
.take(MAX_SIDECAR_ENTRY_BYTES + 1)
.read_to_end(&mut buf)?;
if buf.len() as u64 > MAX_SIDECAR_ENTRY_BYTES {
return Err(io_other(format!(
"walpin sidecar entry {name:?} exceeds {MAX_SIDECAR_ENTRY_BYTES} bytes"
)));
}
let mtime = SystemTime::UNIX_EPOCH + Duration::new(meta.mtime().max(0) as u64, 0);
Ok(Some((buf, meta.uid(), mtime)))
}
pub(super) fn list_names(&self, max: usize) -> io::Result<(Vec<String>, bool)> {
let dup_fd = unsafe { libc::dup(self.raw()) };
if dup_fd < 0 {
return Err(io::Error::last_os_error());
}
let dirp = unsafe { libc::fdopendir(dup_fd) };
if dirp.is_null() {
let err = io::Error::last_os_error();
unsafe { libc::close(dup_fd) };
return Err(err);
}
let raw_scan_limit = max.saturating_mul(RAW_SCAN_FACTOR).max(max);
let mut raw_scanned: usize = 0;
let mut names = Vec::new();
let mut truncated = false;
loop {
let entry = unsafe { libc::readdir(dirp) };
if entry.is_null() {
break;
}
if raw_scanned == raw_scan_limit {
truncated = true;
break;
}
raw_scanned += 1;
let first = unsafe { *(*entry).d_name.as_ptr() };
if first == b'.' as libc::c_char {
continue;
}
if names.len() == max {
truncated = true;
break;
}
let name = unsafe { CStr::from_ptr((*entry).d_name.as_ptr()) }
.to_string_lossy()
.into_owned();
names.push(name);
}
unsafe { libc::closedir(dirp) };
Ok((names, truncated))
}
}
pub(super) fn is_process_alive(pid: u32) -> bool {
let Ok(pid) = i32::try_from(pid) else {
return false;
};
if pid <= 0 {
return false;
}
let rc = unsafe { libc::kill(pid, 0) };
if rc == 0 {
return true;
}
io::Error::last_os_error().raw_os_error() == Some(libc::EPERM)
}
}
#[cfg(windows)]
mod windows_impl {
use super::io_other;
use std::ffi::OsStr;
use std::fs;
use std::io::{self, Write};
use std::os::raw::c_void;
use std::os::windows::ffi::{OsStrExt, OsStringExt};
use std::os::windows::io::{AsRawHandle, FromRawHandle, RawHandle};
use std::path::{Path, PathBuf};
use std::time::SystemTime;
fn to_wide_nul(path: &Path) -> Vec<u16> {
let mut wide: Vec<u16> = path.as_os_str().encode_wide().collect();
wide.push(0);
wide
}
fn open_reparse_aware(
path: &Path,
access: u32,
disposition: u32,
extra_flags: u32,
) -> io::Result<fs::File> {
let wide = to_wide_nul(path);
let handle = unsafe {
CreateFileW(
wide.as_ptr(),
access,
FILE_SHARE_READ | FILE_SHARE_WRITE,
std::ptr::null_mut(),
disposition,
FILE_FLAG_OPEN_REPARSE_POINT | extra_flags,
std::ptr::null_mut(),
)
};
if handle == invalid_handle_value() {
return Err(io::Error::last_os_error());
}
Ok(unsafe { fs::File::from_raw_handle(handle as RawHandle) })
}
fn file_info(file: &fs::File) -> io::Result<ByHandleFileInformation> {
let mut info: ByHandleFileInformation = unsafe { std::mem::zeroed() };
if unsafe { GetFileInformationByHandle(file.as_raw_handle() as Handle, &mut info) } == 0 {
return Err(io::Error::last_os_error());
}
Ok(info)
}
fn resolved_path(file: &fs::File) -> io::Result<PathBuf> {
let mut buf: Vec<u16> = vec![0u16; 260];
loop {
let len = unsafe {
GetFinalPathNameByHandleW(
file.as_raw_handle() as Handle,
buf.as_mut_ptr(),
buf.len() as u32,
0,
)
};
if len == 0 {
return Err(io::Error::last_os_error());
}
if (len as usize) < buf.len() {
buf.truncate(len as usize);
return Ok(PathBuf::from(std::ffi::OsString::from_wide(&buf)));
}
buf.resize(len as usize + 1, 0);
}
}
fn verify_under(resolved: &Path, dir_resolved: &Path, target: &Path) -> io::Result<()> {
if resolved.parent() != Some(dir_resolved) {
return Err(io_other(format!(
"walpin sidecar path {target:?} resolved outside its validated directory"
)));
}
Ok(())
}
fn resolved_paths_match(a: &Path, b: &Path) -> bool {
a.as_os_str().to_string_lossy().to_ascii_lowercase()
== b.as_os_str().to_string_lossy().to_ascii_lowercase()
}
fn open_dir_handle(dir: &Path) -> io::Result<fs::File> {
let parent = dir.parent().unwrap_or_else(|| Path::new("."));
let name = dir.file_name().ok_or_else(|| {
io_other(format!(
"walpin sidecar path {dir:?} has no final path component"
))
})?;
let parent_handle =
open_reparse_aware(parent, 0, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS)?;
let parent_info = file_info(&parent_handle)?;
if parent_info.dw_file_attributes & FILE_ATTRIBUTE_DIRECTORY == 0 {
return Err(io_other(format!(
"walpin sidecar parent {parent:?} is not a directory"
)));
}
let expected = resolved_path(&parent_handle)?.join(name);
let file = open_reparse_aware(dir, 0, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS)?;
let info = file_info(&file)?;
if info.dw_file_attributes & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
return Err(io_other(format!(
"walpin sidecar path {dir:?} is a symlink; refusing"
)));
}
if info.dw_file_attributes & FILE_ATTRIBUTE_DIRECTORY == 0 {
return Err(io_other(format!(
"walpin sidecar path {dir:?} exists and is not a directory"
)));
}
let resolved = resolved_path(&file)?;
if !resolved_paths_match(&resolved, &expected) {
return Err(io_other(format!(
"walpin sidecar path {dir:?} resolved to {resolved:?}, outside its \
validated database-directory anchor {expected:?}; refusing"
)));
}
Ok(file)
}
pub(super) fn ensure_sidecar_dir(dir: &Path) -> io::Result<()> {
match open_dir_handle(dir) {
Ok(_) => Ok(()),
Err(e) if e.kind() == io::ErrorKind::NotFound => {
fs::create_dir(dir)?;
open_dir_handle(dir).map(|_| ())
}
Err(e) => Err(e),
}
}
fn delete_via_handle(file: &fs::File) -> io::Result<()> {
let info = FileDispositionInfo { delete_pending: 1 };
let ok = unsafe {
SetFileInformationByHandle(
file.as_raw_handle() as Handle,
FILE_DISPOSITION_INFO_CLASS,
&info as *const FileDispositionInfo as *mut c_void,
std::mem::size_of::<FileDispositionInfo>() as u32,
)
};
if ok == 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}
fn rename_via_handle(
file: &fs::File,
dir_handle: &fs::File,
target_name: &str,
) -> io::Result<()> {
let wide: Vec<u16> = OsStr::new(target_name).encode_wide().collect();
let name_bytes = wide.len() * 2;
let header_size = std::mem::size_of::<FileRenameInfo>() - 2;
let total_size = header_size + name_bytes;
let words = total_size.div_ceil(8).max(1);
let mut buf: Vec<u64> = vec![0u64; words];
unsafe {
let header = buf.as_mut_ptr() as *mut FileRenameInfo;
(*header).replace_if_exists = 1;
(*header).root_directory = dir_handle.as_raw_handle() as Handle;
(*header).file_name_length = name_bytes as u32;
let name_ptr = (*header).file_name.as_mut_ptr();
std::ptr::copy_nonoverlapping(wide.as_ptr(), name_ptr, wide.len());
}
let byte_ptr = buf.as_mut_ptr() as *mut c_void;
let ok = unsafe {
SetFileInformationByHandle(
file.as_raw_handle() as Handle,
FILE_RENAME_INFO_CLASS,
byte_ptr,
total_size as u32,
)
};
if ok == 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}
pub(super) fn write_atomic(
dir: &Path,
target_name: &str,
tmp_name: &str,
body: &[u8],
) -> io::Result<()> {
let dir_handle = open_dir_handle(dir)?;
let dir_resolved = resolved_path(&dir_handle)?;
let target = dir.join(target_name);
match open_reparse_aware(&target, 0, OPEN_EXISTING, 0) {
Ok(existing) => {
let info = file_info(&existing)?;
if info.dw_file_attributes & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
return Err(io_other(format!(
"walpin sidecar path {target:?} is a symlink; refusing to write through it"
)));
}
}
Err(e) if e.kind() == io::ErrorKind::NotFound => {}
Err(e) => return Err(e),
}
let tmp = dir.join(tmp_name);
if let Ok(stale) = open_reparse_aware(&tmp, DELETE, OPEN_EXISTING, 0) {
let _ = delete_via_handle(&stale);
}
let mut tmp_file = open_reparse_aware(&tmp, GENERIC_WRITE | DELETE, CREATE_NEW, 0)?;
verify_under(&resolved_path(&tmp_file)?, &dir_resolved, &tmp)?;
tmp_file.write_all(body)?;
tmp_file.sync_all()?;
rename_via_handle(&tmp_file, &dir_handle, target_name)
}
pub(super) fn remove_checked(dir: &Path, name: &str) -> io::Result<()> {
let dir_handle = open_dir_handle(dir)?;
let dir_resolved = resolved_path(&dir_handle)?;
let target = dir.join(name);
let file = match open_reparse_aware(&target, DELETE, OPEN_EXISTING, 0) {
Ok(f) => f,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
Err(e) => return Err(e),
};
let info = file_info(&file)?;
if info.dw_file_attributes & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
return Err(io_other(format!(
"refusing to remove symlinked walpin sidecar entry {target:?}"
)));
}
verify_under(&resolved_path(&file)?, &dir_resolved, &target)?;
delete_via_handle(&file)
}
pub(super) fn touch_mtime(dir: &Path, name: &str) -> io::Result<()> {
let dir_handle = open_dir_handle(dir)?;
let dir_resolved = resolved_path(&dir_handle)?;
let target = dir.join(name);
let file = open_reparse_aware(&target, GENERIC_WRITE, OPEN_EXISTING, 0).map_err(|_| {
io_other(format!(
"walpin sidecar entry {target:?} does not exist or could not be opened"
))
})?;
let info = file_info(&file)?;
if info.dw_file_attributes & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
return Err(io_other(format!(
"walpin sidecar entry {target:?} is a reparse point; refusing to touch it"
)));
}
verify_under(&resolved_path(&file)?, &dir_resolved, &target)?;
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map_err(|e| io_other(e.to_string()))?;
const EPOCH_DIFF_100NS: u64 = 116_444_736_000_000_000;
let ticks =
now.as_secs() * 10_000_000 + u64::from(now.subsec_nanos()) / 100 + EPOCH_DIFF_100NS;
let last_write = FileTime {
dw_low_date_time: (ticks & 0xFFFF_FFFF) as u32,
dw_high_date_time: (ticks >> 32) as u32,
};
if unsafe {
SetFileTime(
file.as_raw_handle() as Handle,
std::ptr::null(),
std::ptr::null(),
&last_write,
)
} == 0
{
return Err(io::Error::last_os_error());
}
Ok(())
}
type Handle = *mut c_void;
#[repr(C)]
struct FileTime {
dw_low_date_time: u32,
dw_high_date_time: u32,
}
#[repr(C)]
struct ByHandleFileInformation {
dw_file_attributes: u32,
ft_creation_time: FileTime,
ft_last_access_time: FileTime,
ft_last_write_time: FileTime,
dw_volume_serial_number: u32,
n_file_size_high: u32,
n_file_size_low: u32,
n_number_of_links: u32,
n_file_index_high: u32,
n_file_index_low: u32,
}
#[repr(C)]
struct FileRenameInfo {
replace_if_exists: u8,
root_directory: Handle,
file_name_length: u32,
file_name: [u16; 1],
}
#[repr(C)]
struct FileDispositionInfo {
delete_pending: u8,
}
const PROCESS_QUERY_LIMITED_INFORMATION: u32 = 0x1000;
const STILL_ACTIVE: u32 = 259;
const GENERIC_WRITE: u32 = 0x4000_0000;
const DELETE: u32 = 0x0001_0000;
const FILE_SHARE_READ: u32 = 0x0000_0001;
const FILE_SHARE_WRITE: u32 = 0x0000_0002;
const OPEN_EXISTING: u32 = 3;
const CREATE_NEW: u32 = 1;
const FILE_FLAG_BACKUP_SEMANTICS: u32 = 0x0200_0000;
const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000;
const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x0000_0400;
const FILE_ATTRIBUTE_DIRECTORY: u32 = 0x0000_0010;
const FILE_RENAME_INFO_CLASS: i32 = 3;
const FILE_DISPOSITION_INFO_CLASS: i32 = 4;
fn invalid_handle_value() -> Handle {
usize::MAX as Handle
}
extern "system" {
fn OpenProcess(dw_desired_access: u32, b_inherit_handle: i32, dw_process_id: u32)
-> Handle;
fn CloseHandle(h_object: Handle) -> i32;
fn GetExitCodeProcess(h_process: Handle, lp_exit_code: *mut u32) -> i32;
fn GetProcessTimes(
h_process: Handle,
lp_creation_time: *mut FileTime,
lp_exit_time: *mut FileTime,
lp_kernel_time: *mut FileTime,
lp_user_time: *mut FileTime,
) -> i32;
fn CreateFileW(
lp_file_name: *const u16,
dw_desired_access: u32,
dw_share_mode: u32,
lp_security_attributes: *mut c_void,
dw_creation_disposition: u32,
dw_flags_and_attributes: u32,
h_template_file: Handle,
) -> Handle;
fn GetFileInformationByHandle(
h_file: Handle,
lp_file_information: *mut ByHandleFileInformation,
) -> i32;
fn SetFileTime(
h_file: Handle,
lp_creation_time: *const FileTime,
lp_last_access_time: *const FileTime,
lp_last_write_time: *const FileTime,
) -> i32;
fn GetFinalPathNameByHandleW(
h_file: Handle,
lp_sz_file_path: *mut u16,
cch_file_path: u32,
dw_flags: u32,
) -> u32;
fn SetFileInformationByHandle(
h_file: Handle,
file_information_class: i32,
lp_file_information: *mut c_void,
dw_buffer_size: u32,
) -> i32;
}
pub(super) fn is_process_alive(pid: u32) -> bool {
let handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid) };
if handle.is_null() {
return false;
}
let mut exit_code: u32 = 0;
let ok = unsafe { GetExitCodeProcess(handle, &mut exit_code) };
unsafe { CloseHandle(handle) };
ok != 0 && exit_code == STILL_ACTIVE
}
pub(super) fn process_start_time_secs(pid: u32) -> Option<i64> {
let handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid) };
if handle.is_null() {
return None;
}
let mut creation = FileTime {
dw_low_date_time: 0,
dw_high_date_time: 0,
};
let mut exit = FileTime {
dw_low_date_time: 0,
dw_high_date_time: 0,
};
let mut kernel = FileTime {
dw_low_date_time: 0,
dw_high_date_time: 0,
};
let mut user = FileTime {
dw_low_date_time: 0,
dw_high_date_time: 0,
};
let ok =
unsafe { GetProcessTimes(handle, &mut creation, &mut exit, &mut kernel, &mut user) };
unsafe { CloseHandle(handle) };
if ok == 0 {
return None;
}
let ticks = ((creation.dw_high_date_time as u64) << 32) | creation.dw_low_date_time as u64;
const EPOCH_DIFF_100NS: u64 = 116_444_736_000_000_000;
let unix_100ns = ticks.checked_sub(EPOCH_DIFF_100NS)?;
Some((unix_100ns / 10_000_000) as i64)
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct CensusResult {
pub holders: std::collections::HashSet<u32>,
pub uninspectable_pids: Vec<u32>,
pub truncated: bool,
}
impl CensusResult {
pub fn is_complete(&self) -> bool {
self.uninspectable_pids.is_empty() && !self.truncated
}
fn apply_self_canary(&mut self) {
if !self.holders.contains(&std::process::id()) {
self.truncated = true;
}
}
}
#[cfg(target_os = "macos")]
fn macos_pid_genuinely_gone(errno: Option<i32>) -> bool {
errno == Some(libc::ESRCH)
}
#[cfg(target_os = "macos")]
fn proc_pidfdinfo_returned_expected_size(returned_bytes: i32, expected_size: usize) -> bool {
returned_bytes > 0 && returned_bytes as usize == expected_size
}
#[cfg(target_os = "macos")]
const CENSUS_BUFFER_NEGOTIATION_ATTEMPTS: usize = 4;
#[cfg(target_os = "macos")]
fn negotiate_buffer<T: Default + Clone>(
size_call: impl Fn() -> std::os::raw::c_int,
data_call: impl Fn(*mut std::os::raw::c_void, std::os::raw::c_int) -> std::os::raw::c_int,
) -> io::Result<(Vec<T>, bool)> {
let item_size = std::mem::size_of::<T>();
for attempt in 0..CENSUS_BUFFER_NEGOTIATION_ATTEMPTS {
let needed = size_call();
if needed <= 0 {
return Err(io::Error::last_os_error());
}
let needed_items = needed as usize / item_size + 1;
let item_count = needed_items + needed_items / 4 + 8;
let mut buf: Vec<T> = vec![T::default(); item_count];
let cap_bytes = (buf.len() * item_size) as std::os::raw::c_int;
let bytes = data_call(buf.as_mut_ptr() as *mut std::os::raw::c_void, cap_bytes);
if bytes <= 0 {
return Err(io::Error::last_os_error());
}
let filled_capacity = bytes as usize >= cap_bytes as usize;
let is_last_attempt = attempt + 1 == CENSUS_BUFFER_NEGOTIATION_ATTEMPTS;
if filled_capacity && !is_last_attempt {
continue;
}
let count = (bytes as usize / item_size).min(buf.len());
buf.truncate(count);
return Ok((buf, filled_capacity));
}
unreachable!("loop always returns or errors within CENSUS_BUFFER_NEGOTIATION_ATTEMPTS")
}
#[cfg(target_os = "macos")]
pub fn census_holders(db_path: &Path) -> io::Result<CensusResult> {
use std::os::raw::{c_int, c_void};
use std::os::unix::fs::MetadataExt;
const PROC_ALL_PIDS: u32 = 1;
const PROC_PIDLISTFDS: c_int = 1;
const PROC_PIDFDVNODEPATHINFO: c_int = 2;
const PROX_FDTYPE_VNODE: u32 = 1;
const MAXPATHLEN: usize = 1024;
#[repr(C)]
#[derive(Clone, Default)]
struct ProcFdInfo {
proc_fd: i32,
proc_fdtype: u32,
}
#[repr(C)]
struct ProcFileInfo {
fi_openflags: u32,
fi_status: u32,
fi_offset: i64,
fi_type: i32,
fi_guardflags: u32,
}
#[repr(C)]
struct FsId {
val: [i32; 2],
}
#[repr(C)]
struct VinfoStat {
vst_dev: u32,
vst_mode: u16,
vst_nlink: u16,
vst_ino: u64,
vst_uid: u32,
vst_gid: u32,
vst_atime: i64,
vst_atimensec: i64,
vst_mtime: i64,
vst_mtimensec: i64,
vst_ctime: i64,
vst_ctimensec: i64,
vst_birthtime: i64,
vst_birthtimensec: i64,
vst_size: i64,
vst_blocks: i64,
vst_blksize: i32,
vst_flags: u32,
vst_gen: u32,
vst_rdev: u32,
vst_qspare: [i64; 2],
}
#[repr(C)]
struct VnodeInfo {
vi_stat: VinfoStat,
vi_type: i32,
vi_pad: i32,
vi_fsid: FsId,
}
#[repr(C)]
struct VnodeInfoPath {
vip_vi: VnodeInfo,
vip_path: [u8; MAXPATHLEN],
}
#[repr(C)]
struct VnodeFdInfoWithPath {
pfi: ProcFileInfo,
pvip: VnodeInfoPath,
}
#[link(name = "proc")]
extern "C" {
fn proc_listpids(kind: u32, typeinfo: u32, buffer: *mut c_void, buffersize: c_int)
-> c_int;
fn proc_pidinfo(
pid: c_int,
flavor: c_int,
arg: u64,
buffer: *mut c_void,
buffersize: c_int,
) -> c_int;
fn proc_pidfdinfo(
pid: c_int,
fd: c_int,
flavor: c_int,
buffer: *mut c_void,
buffersize: c_int,
) -> c_int;
}
let target_meta = fs::metadata(db_path)?;
let target_ident = (target_meta.dev() as u32, target_meta.ino());
let (pid_buf, pid_list_truncated): (Vec<i32>, bool) = negotiate_buffer(
|| unsafe { proc_listpids(PROC_ALL_PIDS, 0, std::ptr::null_mut(), 0) },
|buf_ptr, buf_bytes| unsafe { proc_listpids(PROC_ALL_PIDS, 0, buf_ptr, buf_bytes) },
)?;
let mut holders = std::collections::HashSet::new();
let mut uninspectable: Vec<u32> = Vec::new();
for &pid in &pid_buf {
if pid <= 0 {
continue;
}
let (fd_buf, fd_list_truncated): (Vec<ProcFdInfo>, bool) = match negotiate_buffer(
|| unsafe { proc_pidinfo(pid, PROC_PIDLISTFDS, 0, std::ptr::null_mut(), 0) },
|buf_ptr, buf_bytes| unsafe {
proc_pidinfo(pid, PROC_PIDLISTFDS, 0, buf_ptr, buf_bytes)
},
) {
Ok(v) => v,
Err(e) => {
if !macos_pid_genuinely_gone(e.raw_os_error()) {
uninspectable.push(pid as u32);
}
continue;
}
};
if fd_list_truncated {
uninspectable.push(pid as u32);
}
for fdinfo in &fd_buf {
if fdinfo.proc_fdtype != PROX_FDTYPE_VNODE {
continue;
}
let mut vinfo: VnodeFdInfoWithPath = unsafe { std::mem::zeroed() };
let vsize = unsafe {
proc_pidfdinfo(
pid,
fdinfo.proc_fd,
PROC_PIDFDVNODEPATHINFO,
&mut vinfo as *mut _ as *mut c_void,
std::mem::size_of::<VnodeFdInfoWithPath>() as c_int,
)
};
if !proc_pidfdinfo_returned_expected_size(
vsize,
std::mem::size_of::<VnodeFdInfoWithPath>(),
) {
if vsize <= 0 {
let errno = io::Error::last_os_error().raw_os_error();
if !macos_pid_genuinely_gone(errno) {
uninspectable.push(pid as u32);
}
} else {
uninspectable.push(pid as u32);
}
continue;
}
let vstat = &vinfo.pvip.vip_vi.vi_stat;
if (vstat.vst_dev, vstat.vst_ino) == target_ident {
holders.insert(pid as u32);
break;
}
}
}
uninspectable.sort_unstable();
uninspectable.dedup();
let mut census = CensusResult {
holders,
uninspectable_pids: uninspectable,
truncated: pid_list_truncated,
};
census.apply_self_canary();
Ok(census)
}
#[cfg(target_os = "linux")]
fn linux_proc_gone(err: &io::Error) -> bool {
err.kind() == io::ErrorKind::NotFound
}
#[cfg(target_os = "linux")]
const PROC_PID_INIT_INO: u64 = 0xEFFFFFFC;
#[cfg(target_os = "linux")]
fn pid_ns_is_init(ino: u64) -> bool {
ino == PROC_PID_INIT_INO
}
#[cfg(target_os = "linux")]
fn proc_mount_restricts_visibility(options: &str) -> bool {
options.split(',').map(str::trim).any(|opt| {
if let Some(value) = opt.strip_prefix("hidepid=") {
!matches!(value, "0" | "off")
} else {
opt == "hidepid" || opt == "subset" || opt.starts_with("subset=")
}
})
}
#[cfg(target_os = "linux")]
fn proc_mount_is_visibility_restricted() -> Option<bool> {
let mountinfo = fs::read_to_string("/proc/self/mountinfo").ok()?;
proc_mounts_restricted_in(&mountinfo)
}
#[cfg(target_os = "linux")]
fn proc_mounts_restricted_in(mountinfo: &str) -> Option<bool> {
let mut found_any = false;
for line in mountinfo.lines() {
let Some((fields_part, super_part)) = line.split_once(" - ") else {
continue;
};
let fields: Vec<&str> = fields_part.split(' ').collect();
if fields.len() < 6 || fields[4] != "/proc" {
continue;
}
let mount_options = fields[5];
let super_fields: Vec<&str> = super_part.split(' ').collect();
if super_fields.first().copied() != Some("proc") {
continue;
}
let super_options = super_fields.get(2).copied().unwrap_or("");
found_any = true;
if proc_mount_restricts_visibility(mount_options)
|| proc_mount_restricts_visibility(super_options)
{
return Some(true);
}
}
if found_any {
Some(false)
} else {
None
}
}
#[cfg(target_os = "linux")]
pub fn census_holders(db_path: &Path) -> io::Result<CensusResult> {
use std::os::unix::fs::MetadataExt;
let target_meta = fs::metadata(db_path)?;
let target_ident = (target_meta.dev(), target_meta.ino());
let mut holders = std::collections::HashSet::new();
let mut uninspectable: Vec<u32> = Vec::new();
let mut truncated = false;
match fs::metadata("/proc/self/ns/pid") {
Ok(meta) if pid_ns_is_init(meta.ino()) => {}
_ => truncated = true,
}
match proc_mount_is_visibility_restricted() {
Some(false) => {}
Some(true) | None => truncated = true,
}
let proc_dir = fs::read_dir("/proc")?;
for entry_result in proc_dir {
let proc_entry = match entry_result {
Ok(e) => e,
Err(_) => {
truncated = true;
continue;
}
};
let Some(pid) = proc_entry
.file_name()
.to_str()
.and_then(|s| s.parse::<u32>().ok())
else {
continue;
};
let fd_dir = proc_entry.path().join("fd");
let fds = match fs::read_dir(&fd_dir) {
Ok(fds) => fds,
Err(e) if linux_proc_gone(&e) => continue,
Err(_) => {
uninspectable.push(pid);
continue;
}
};
for fd_result in fds {
let fd_entry = match fd_result {
Ok(e) => e,
Err(_) => {
uninspectable.push(pid);
continue;
}
};
match fs::metadata(fd_entry.path()) {
Ok(meta) => {
if (meta.dev(), meta.ino()) == target_ident {
holders.insert(pid);
break;
}
}
Err(e) if e.kind() == io::ErrorKind::NotFound => {}
Err(_) => uninspectable.push(pid),
}
}
}
uninspectable.sort_unstable();
uninspectable.dedup();
let mut census = CensusResult {
holders,
uninspectable_pids: uninspectable,
truncated,
};
census.apply_self_canary();
Ok(census)
}
#[cfg(all(unix, not(any(target_os = "macos", target_os = "linux"))))]
pub fn census_holders(_db_path: &Path) -> io::Result<CensusResult> {
Err(io_other(
"OS-derived holder census has no implementation on this Unix target",
))
}
pub fn ensure_sidecar_dir(dir: &Path) -> io::Result<()> {
#[cfg(unix)]
{
unix_impl::SidecarDirHandle::open_or_create(dir)?;
Ok(())
}
#[cfg(windows)]
{
windows_impl::ensure_sidecar_dir(dir)
}
}
pub fn write_heartbeat(dir: &Path, heartbeat: &WalpinHeartbeat) -> io::Result<()> {
let body =
serde_json::to_vec(heartbeat).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
let target = format!("{}.json", heartbeat.pid);
let tmp = format!(".{}.json.tmp", heartbeat.pid);
#[cfg(unix)]
{
let handle = unix_impl::SidecarDirHandle::open_or_create(dir)?;
handle.write_atomic(&target, &tmp, &body)
}
#[cfg(windows)]
{
windows_impl::ensure_sidecar_dir(dir)?;
windows_impl::write_atomic(dir, &target, &tmp, &body)
}
}
pub fn touch_heartbeat(dir: &Path, pid: u32) -> io::Result<()> {
let name = format!("{pid}.json");
#[cfg(unix)]
{
let handle = unix_impl::SidecarDirHandle::open_or_create(dir)?;
handle.touch_mtime(&name)
}
#[cfg(windows)]
{
windows_impl::touch_mtime(dir, &name)
}
}
pub fn remove_beacon(dir: &Path, pid: u32) -> io::Result<()> {
let target = format!("{pid}.beacon");
#[cfg(unix)]
{
match unix_impl::SidecarDirHandle::open_if_exists(dir)? {
Some(handle) => handle.remove_checked(&target),
None => Ok(()),
}
}
#[cfg(windows)]
{
windows_impl::remove_checked(dir, &target)
}
}
pub fn remove_heartbeat(dir: &Path, pid: u32) -> io::Result<()> {
let target = format!("{pid}.json");
#[cfg(unix)]
{
match unix_impl::SidecarDirHandle::open_if_exists(dir)? {
Some(handle) => handle.remove_checked(&target),
None => Ok(()),
}
}
#[cfg(windows)]
{
windows_impl::remove_checked(dir, &target)
}
}
pub fn write_beacon(dir: &Path, beacon: &WalpinBeacon) -> io::Result<()> {
let body =
serde_json::to_vec(beacon).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
let target = format!("{}.beacon", beacon.pid);
let tmp = format!(".{}.beacon.tmp", beacon.pid);
#[cfg(unix)]
{
let handle = unix_impl::SidecarDirHandle::open_or_create(dir)?;
handle.write_atomic(&target, &tmp, &body)
}
#[cfg(windows)]
{
windows_impl::ensure_sidecar_dir(dir)?;
windows_impl::write_atomic(dir, &target, &tmp, &body)
}
}
pub fn touch_beacon(dir: &Path, pid: u32) -> io::Result<()> {
let name = format!("{pid}.beacon");
#[cfg(unix)]
{
let handle = unix_impl::SidecarDirHandle::open_or_create(dir)?;
handle.touch_mtime(&name)
}
#[cfg(windows)]
{
windows_impl::touch_mtime(dir, &name)
}
}
pub fn beacon_path(dir: &Path, pid: u32) -> PathBuf {
dir.join(format!("{pid}.beacon"))
}
pub fn is_process_alive(pid: u32) -> bool {
#[cfg(unix)]
{
unix_impl::is_process_alive(pid)
}
#[cfg(windows)]
{
windows_impl::is_process_alive(pid)
}
}
#[cfg(target_os = "macos")]
pub fn process_start_time_secs(pid: u32) -> Option<i64> {
use std::os::raw::{c_int, c_void};
const PROC_PIDTBSDINFO: c_int = 3;
const MAXCOMLEN: usize = 16;
#[repr(C)]
struct ProcBsdInfo {
pbi_flags: u32,
pbi_status: u32,
pbi_xstatus: u32,
pbi_pid: u32,
pbi_ppid: u32,
pbi_uid: u32,
pbi_gid: u32,
pbi_ruid: u32,
pbi_rgid: u32,
pbi_svuid: u32,
pbi_svgid: u32,
rfu_1: u32,
pbi_comm: [u8; MAXCOMLEN],
pbi_name: [u8; 2 * MAXCOMLEN],
pbi_nfiles: u32,
pbi_pgid: u32,
pbi_pjobc: u32,
e_tdev: u32,
e_tpgid: u32,
pbi_nice: i32,
pbi_start_tvsec: u64,
pbi_start_tvusec: u64,
}
#[link(name = "proc")]
extern "C" {
fn proc_pidinfo(
pid: c_int,
flavor: c_int,
arg: u64,
buffer: *mut c_void,
buffersize: c_int,
) -> c_int;
}
let pid_i32 = i32::try_from(pid).ok()?;
let mut info: ProcBsdInfo = unsafe { std::mem::zeroed() };
let size = std::mem::size_of::<ProcBsdInfo>() as c_int;
let ret = unsafe {
proc_pidinfo(
pid_i32,
PROC_PIDTBSDINFO,
0,
&mut info as *mut _ as *mut c_void,
size,
)
};
if ret != size {
return None;
}
i64::try_from(info.pbi_start_tvsec).ok()
}
#[cfg(target_os = "linux")]
pub fn process_start_time_secs(pid: u32) -> Option<i64> {
let stat = fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
let rparen = stat.rfind(')')?;
let rest = stat.get(rparen + 1..)?;
let fields: Vec<&str> = rest.split_whitespace().collect();
let starttime_ticks: u64 = fields.get(19)?.parse().ok()?;
let clk_tck = unsafe { libc::sysconf(libc::_SC_CLK_TCK) };
if clk_tck <= 0 {
return None;
}
let secs_since_boot = starttime_ticks / clk_tck as u64;
let stat_all = fs::read_to_string("/proc/stat").ok()?;
let btime = stat_all.lines().find_map(|line| {
line.strip_prefix("btime ")
.and_then(|v| v.trim().parse::<i64>().ok())
})?;
Some(btime + secs_since_boot as i64)
}
#[cfg(windows)]
pub fn process_start_time_secs(pid: u32) -> Option<i64> {
windows_impl::process_start_time_secs(pid)
}
#[cfg(not(any(target_os = "macos", target_os = "linux", windows)))]
pub fn process_start_time_secs(_pid: u32) -> Option<i64> {
None
}
fn now_epoch_secs() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0)
}
#[cfg(unix)]
fn stale_window_from(interval: Duration) -> i64 {
interval
.max(Duration::from_secs(1))
.saturating_mul(3)
.as_secs() as i64
}
#[cfg(unix)]
fn stale_window_secs(producer_interval_ms: u64, fallback_secs: i64) -> i64 {
if producer_interval_ms == 0 {
fallback_secs
} else {
stale_window_from(Duration::from_millis(producer_interval_ms))
}
}
#[cfg(unix)]
fn epoch_abs_diff(a: i64, b: i64) -> u64 {
a.checked_sub(b)
.map(|d| d.unsigned_abs())
.unwrap_or(u64::MAX)
}
#[cfg(unix)]
pub fn enumerate_live(dir: &Path, sweep_interval: Duration) -> io::Result<WalpinReport> {
enumerate_live_bounded(dir, sweep_interval, MAX_SIDECAR_ENTRIES)
}
#[cfg(unix)]
const MAX_SIDECAR_ENTRIES: usize = 512;
#[cfg(unix)]
const CAP_SENTINEL_PID: u32 = 0;
#[cfg(unix)]
fn enumerate_live_bounded(
dir: &Path,
sweep_interval: Duration,
max_entries: usize,
) -> io::Result<WalpinReport> {
let handle = match unix_impl::SidecarDirHandle::open_if_exists(dir) {
Ok(Some(h)) => h,
Ok(None) => return Ok(WalpinReport::default()),
Err(e) => return Err(e),
};
let now = now_epoch_secs();
let fallback_window_secs = stale_window_from(sweep_interval);
let mut heartbeats: std::collections::HashMap<u32, WalpinHeartbeat> = Default::default();
let mut beacon_pids: std::collections::HashSet<u32> = Default::default();
let mut unknown: Vec<(u32, &'static str)> = Vec::new();
let mut wedged: std::collections::HashSet<u32> = Default::default();
let (names, truncated) = handle.list_names(max_entries)?;
if truncated {
unknown.push((
CAP_SENTINEL_PID,
"refused: sidecar entry count exceeds enumeration cap",
));
}
for name in names {
let is_heartbeat = name.ends_with(".json");
let is_beacon = name.ends_with(".beacon");
if !is_heartbeat && !is_beacon {
continue;
}
let Some(pid) = name
.rsplit_once('.')
.and_then(|(stem, _)| stem.parse::<u32>().ok())
else {
continue;
};
let (body, owner_uid, mtime) = match handle.read_checked(&name) {
Ok(Some(v)) => v,
Ok(None) => continue, Err(_) => {
unknown.push((
pid,
"refused: untrusted sidecar entry (symlink, non-regular, or oversized)",
));
continue;
}
};
if owner_uid != unix_impl::current_uid() {
unknown.push((pid, "refused: sidecar entry not owned by current user"));
continue;
}
let mtime_secs = mtime
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
if is_heartbeat {
let heartbeat: WalpinHeartbeat = match serde_json::from_slice(&body) {
Ok(hb) => hb,
Err(_) => {
let _ = handle.unlink_tolerant(&name);
unknown.push((pid, "malformed walpin heartbeat entry"));
continue;
}
};
let alive = is_process_alive(heartbeat.pid);
let identity_ok = alive
&& process_start_time_secs(heartbeat.pid)
.map(|actual| {
epoch_abs_diff(actual, heartbeat.started_at) <= START_TIME_EPSILON_SECS
})
.unwrap_or(false);
if !identity_ok {
let _ = handle.unlink_tolerant(&name);
continue;
}
let window = stale_window_secs(heartbeat.sweep_interval_ms, fallback_window_secs);
let hb_fresh = if heartbeat.oldest_tx_started_at.is_some() {
epoch_abs_diff(now, mtime_secs) <= window as u64
} else {
epoch_abs_diff(now, heartbeat.updated_at) <= window as u64
};
if !hb_fresh {
let _ = handle.unlink_tolerant(&name);
wedged.insert(pid);
unknown.push((pid, "stale walpin heartbeat"));
continue;
}
heartbeats.insert(heartbeat.pid, heartbeat);
} else {
let beacon: WalpinBeacon = match serde_json::from_slice(&body) {
Ok(b) => b,
Err(_) => {
let _ = handle.unlink_tolerant(&name);
unknown.push((pid, "malformed walpin beacon entry"));
continue;
}
};
let alive = is_process_alive(beacon.pid);
let identity_ok = alive
&& process_start_time_secs(beacon.pid)
.map(|actual| {
epoch_abs_diff(actual, beacon.started_at) <= START_TIME_EPSILON_SECS
})
.unwrap_or(false);
if !identity_ok {
let _ = handle.unlink_tolerant(&name);
continue;
}
let window = stale_window_secs(beacon.sweep_interval_ms, fallback_window_secs);
let fresh = epoch_abs_diff(now, mtime_secs) <= window as u64;
if !fresh {
let _ = handle.unlink_tolerant(&name);
wedged.insert(pid);
unknown.push((pid, "stale walpin beacon"));
continue;
}
beacon_pids.insert(beacon.pid);
}
}
let mut entries: Vec<WalpinPidHealth> = Vec::new();
for (pid, hb) in heartbeats {
entries.push(WalpinPidHealth::Reporting(hb));
beacon_pids.remove(&pid);
}
for pid in beacon_pids {
if wedged.contains(&pid) {
continue; }
entries.push(WalpinPidHealth::RegisteredSilent { pid });
}
for (pid, reason) in unknown {
entries.push(WalpinPidHealth::Unknown { pid, reason });
}
Ok(WalpinReport { entries })
}
#[cfg(test)]
pub(crate) struct EnvVarGuard {
key: &'static str,
saved: Option<String>,
}
#[cfg(test)]
impl EnvVarGuard {
pub(crate) fn capture(key: &'static str) -> Self {
Self {
key,
saved: std::env::var(key).ok(),
}
}
}
#[cfg(test)]
impl Drop for EnvVarGuard {
fn drop(&mut self) {
match &self.saved {
Some(v) => std::env::set_var(self.key, v),
None => std::env::remove_var(self.key),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(unix)]
use std::os::unix::fs::{MetadataExt, PermissionsExt};
#[cfg(unix)]
fn current_uid() -> u32 {
unix_impl::current_uid()
}
fn heartbeat(pid: u32) -> WalpinHeartbeat {
let now = now_epoch_secs();
WalpinHeartbeat {
pid,
process_role: "session".to_string(),
started_at: process_start_time_secs(std::process::id()).unwrap_or(0),
oldest_tx_age_secs: 45.0,
oldest_tx_label: Some("test_span".to_string()),
oldest_tx_started_at: Some(now - 45),
updated_at: now,
sweep_interval_ms: 5_000,
attribution_basis: Some("origin".to_string()),
}
}
#[test]
fn sidecar_dir_is_db_scoped_sibling() {
let dir = tempfile::tempdir().unwrap();
let db = dir.path().join("khive.db");
assert_eq!(sidecar_dir_for(&db), dir.path().join("khive.db.walpin"));
}
#[test]
#[serial_test::serial(khive_walpin_sidecar_env)]
fn sidecar_enabled_defaults_to_file_backed() {
let _guard = EnvVarGuard::capture("KHIVE_WALPIN_SIDECAR");
std::env::remove_var("KHIVE_WALPIN_SIDECAR");
assert!(sidecar_enabled(true), "file-backed must default on");
assert!(!sidecar_enabled(false), "in-memory must default off");
}
#[test]
#[serial_test::serial(khive_walpin_sidecar_env)]
fn sidecar_enabled_env_override_wins_either_way() {
let _guard = EnvVarGuard::capture("KHIVE_WALPIN_SIDECAR");
std::env::set_var("KHIVE_WALPIN_SIDECAR", "off");
assert!(
!sidecar_enabled(true),
"explicit off must override file-backed default"
);
std::env::set_var("KHIVE_WALPIN_SIDECAR", "on");
assert!(
sidecar_enabled(false),
"explicit on must override in-memory default"
);
}
#[test]
fn ensure_sidecar_dir_creates_0700_owned_dir() {
let root = tempfile::tempdir().unwrap();
let dir = root.path().join("khive.db.walpin");
ensure_sidecar_dir(&dir).expect("should create");
let meta = fs::symlink_metadata(&dir).unwrap();
assert!(meta.is_dir());
assert_eq!(meta.permissions().mode() & 0o777, 0o700);
assert_eq!(meta.uid(), current_uid());
}
#[test]
fn ensure_sidecar_dir_refuses_wrong_mode() {
let root = tempfile::tempdir().unwrap();
let dir = root.path().join("khive.db.walpin");
fs::create_dir(&dir).unwrap();
fs::set_permissions(&dir, fs::Permissions::from_mode(0o755)).unwrap();
let err = ensure_sidecar_dir(&dir).expect_err("wrong mode must be refused");
assert!(err.to_string().contains("expected 0700"));
}
#[test]
fn ensure_sidecar_dir_refuses_symlink() {
let root = tempfile::tempdir().unwrap();
let real = root.path().join("real_dir");
fs::create_dir(&real).unwrap();
let link = root.path().join("khive.db.walpin");
std::os::unix::fs::symlink(&real, &link).unwrap();
let err = ensure_sidecar_dir(&link).expect_err("symlink must be refused");
assert!(err.to_string().contains("symlink"));
}
#[test]
#[cfg(unix)]
fn ensure_sidecar_dir_refuses_non_root_owned_ancestor_symlink() {
let root = tempfile::tempdir().unwrap();
let real = root.path().join("real_ancestor");
fs::create_dir(&real).unwrap();
let link = root.path().join("linked_ancestor");
std::os::unix::fs::symlink(&real, &link).unwrap();
let dir = link.join("khive.db.walpin");
let err =
ensure_sidecar_dir(&dir).expect_err("non-root-owned ancestor symlink must be refused");
assert!(
err.to_string().contains("symlink"),
"unexpected error: {err}"
);
}
#[test]
fn write_then_read_heartbeat_roundtrips() {
let root = tempfile::tempdir().unwrap();
let dir = root.path().join("khive.db.walpin");
let hb = heartbeat(std::process::id());
write_heartbeat(&dir, &hb).expect("write should succeed");
let content = fs::read_to_string(dir.join(format!("{}.json", hb.pid))).unwrap();
let read_back: WalpinHeartbeat = serde_json::from_str(&content).unwrap();
assert_eq!(read_back, hb);
}
#[test]
fn heartbeat_deserializes_pre_rename_interval_ms_field() {
let json = r#"{
"pid": 4242,
"process_role": "session",
"started_at": 1000,
"oldest_tx_age_secs": 45.0,
"oldest_tx_label": "test_span",
"updated_at": 1045,
"interval_ms": 60000
}"#;
let hb: WalpinHeartbeat = serde_json::from_str(json).unwrap();
assert_eq!(hb.sweep_interval_ms, 60_000);
}
#[test]
fn beacon_deserializes_pre_rename_interval_ms_field() {
let json = r#"{
"pid": 4242,
"process_role": "session",
"started_at": 1000,
"interval_ms": 60000
}"#;
let b: WalpinBeacon = serde_json::from_str(json).unwrap();
assert_eq!(b.sweep_interval_ms, 60_000);
}
#[test]
fn write_heartbeat_refuses_symlinked_target() {
let root = tempfile::tempdir().unwrap();
let dir = root.path().join("khive.db.walpin");
ensure_sidecar_dir(&dir).unwrap();
let real = root.path().join("elsewhere.txt");
fs::write(&real, b"nope").unwrap();
let hb = heartbeat(999_999);
let target = dir.join(format!("{}.json", hb.pid));
std::os::unix::fs::symlink(&real, &target).unwrap();
let err = write_heartbeat(&dir, &hb).expect_err("symlinked target must be refused");
assert!(err.to_string().contains("symlink"));
assert_eq!(fs::read_to_string(&real).unwrap(), "nope");
}
#[cfg(unix)]
#[test]
fn sidecar_dir_distinguishes_non_utf8_db_names_on_disk() {
use std::os::unix::ffi::OsStrExt;
let root = tempfile::tempdir().unwrap();
let name_a = std::ffi::OsStr::from_bytes(b"khive-\xffdb.sqlite");
let name_b = std::ffi::OsStr::from_bytes(b"khive-\xfedb.sqlite");
let dir_a = sidecar_dir_for(&root.path().join(name_a));
let dir_b = sidecar_dir_for(&root.path().join(name_b));
assert_ne!(
dir_a, dir_b,
"distinct db names must produce distinct sidecar paths"
);
let hb = heartbeat(std::process::id());
if let Err(e) = write_heartbeat(&dir_a, &hb) {
eprintln!(
"skipping sidecar_dir_distinguishes_non_utf8_db_names_on_disk: filesystem \
rejected a non-UTF-8 sidecar directory name ({e}); this platform's filesystem \
does not support the case under test"
);
return;
}
write_heartbeat(&dir_b, &hb).expect("write to second non-UTF-8 sidecar");
let mut entries: Vec<_> = fs::read_dir(root.path())
.unwrap()
.filter_map(|e| e.ok())
.map(|e| e.file_name())
.collect();
entries.sort();
assert_eq!(
entries.len(),
2,
"distinct non-UTF-8 database names must produce two distinct sidecar directories \
on disk, not collide onto one: got {entries:?}"
);
}
#[test]
fn remove_heartbeat_is_idempotent_when_absent() {
let root = tempfile::tempdir().unwrap();
let dir = root.path().join("khive.db.walpin");
ensure_sidecar_dir(&dir).unwrap();
remove_heartbeat(&dir, 123_456).expect("removing an absent entry is a no-op");
}
#[test]
fn is_process_alive_true_for_self_false_for_reserved_pid() {
assert!(is_process_alive(std::process::id()));
assert!(!is_process_alive(0));
}
#[test]
fn process_start_time_resolves_for_self() {
let start = process_start_time_secs(std::process::id());
assert!(
start.is_some(),
"must resolve this process's own start time"
);
let now = now_epoch_secs();
assert!(
start.unwrap() <= now,
"start time must not be in the future"
);
}
fn beacon(pid: u32) -> WalpinBeacon {
WalpinBeacon {
pid,
process_role: "session".to_string(),
started_at: process_start_time_secs(std::process::id()).unwrap_or(0),
sweep_interval_ms: 5_000,
}
}
#[test]
fn enumerate_live_reports_and_retains_a_genuinely_live_entry() {
let root = tempfile::tempdir().unwrap();
let dir = root.path().join("khive.db.walpin");
let hb = heartbeat(std::process::id());
write_heartbeat(&dir, &hb).unwrap();
let report = enumerate_live(&dir, Duration::from_secs(5)).unwrap();
let reporting: Vec<_> = report.reporting().collect();
assert_eq!(reporting.len(), 1);
assert_eq!(reporting[0].pid, hb.pid);
assert!(report.fully_attributed());
assert!(dir.join(format!("{}.json", hb.pid)).exists());
}
#[test]
fn epoch_abs_diff_saturates_instead_of_wrapping() {
assert_eq!(epoch_abs_diff(5, 3), 2);
assert_eq!(epoch_abs_diff(3, 5), 2);
assert_eq!(epoch_abs_diff(0, 0), 0);
assert_eq!(epoch_abs_diff(1, i64::MIN), u64::MAX);
assert_eq!(epoch_abs_diff(i64::MIN, i64::MAX), u64::MAX);
assert_eq!(epoch_abs_diff(-1, i64::MAX), 1u64 << 63);
}
#[test]
fn enumerate_live_extreme_timestamp_classifies_unknown_not_fresh() {
let root = tempfile::tempdir().unwrap();
let dir = root.path().join("khive.db.walpin");
let mut hb = heartbeat(std::process::id());
hb.oldest_tx_started_at = None;
hb.updated_at = i64::MIN;
write_heartbeat(&dir, &hb).unwrap();
let report = enumerate_live(&dir, Duration::from_secs(5)).unwrap();
assert!(
report.reporting().next().is_none(),
"an extreme updated_at must never classify as fresh"
);
assert!(
report
.entries
.iter()
.any(|e| matches!(e, WalpinPidHealth::Unknown { pid, .. } if *pid == hb.pid)),
"the extreme-timestamp entry must stay Unknown, not vanish"
);
}
#[test]
fn enumerate_live_bounded_caps_listing_with_sentinel_marker() {
let root = tempfile::tempdir().unwrap();
let dir = root.path().join("khive.db.walpin");
let live = heartbeat(std::process::id());
write_heartbeat(&dir, &live).unwrap();
for pid in [2_000_000_001u32, 2_000_000_002] {
let mut hb = heartbeat(std::process::id());
hb.pid = pid;
write_heartbeat(&dir, &hb).unwrap();
}
let report = enumerate_live_bounded(&dir, Duration::from_secs(5), 1).unwrap();
let markers = report
.entries
.iter()
.filter(|e| {
matches!(
e,
WalpinPidHealth::Unknown { pid: 0, reason }
if reason.contains("enumeration cap")
)
})
.count();
assert_eq!(
markers, 1,
"a truncated listing must surface exactly one sentinel Unknown marker"
);
assert!(
!report.fully_attributed(),
"a capped enumeration can never claim full attribution"
);
assert!(report.entries.len() <= 2, "got {:?}", report.entries);
}
#[test]
fn enumerate_live_bounded_caps_hidden_entry_scan_with_sentinel_marker() {
let root = tempfile::tempdir().unwrap();
let dir = root.path().join("khive.db.walpin");
let live = heartbeat(std::process::id());
write_heartbeat(&dir, &live).unwrap();
for i in 0..64 {
std::fs::write(dir.join(format!(".junk{i}")), b"x").unwrap();
}
let report = enumerate_live_bounded(&dir, Duration::from_secs(5), 4).unwrap();
let markers = report
.entries
.iter()
.filter(|e| {
matches!(
e,
WalpinPidHealth::Unknown { pid: 0, reason }
if reason.contains("enumeration cap")
)
})
.count();
assert_eq!(
markers, 1,
"a hidden-entry flood must surface exactly one sentinel Unknown marker"
);
assert!(
!report.fully_attributed(),
"an enumeration cut short by hidden entries can never claim full attribution"
);
}
#[test]
fn enumerate_live_uncapped_population_has_no_sentinel() {
let root = tempfile::tempdir().unwrap();
let dir = root.path().join("khive.db.walpin");
let live = heartbeat(std::process::id());
write_heartbeat(&dir, &live).unwrap();
let report = enumerate_live(&dir, Duration::from_secs(5)).unwrap();
assert!(
report
.entries
.iter()
.all(|e| !matches!(e, WalpinPidHealth::Unknown { pid: 0, .. })),
"an in-budget population must not carry the cap sentinel"
);
}
#[test]
fn enumerate_live_deletes_dead_pid_entry() {
let root = tempfile::tempdir().unwrap();
let dir = root.path().join("khive.db.walpin");
let mut hb = heartbeat(std::process::id());
hb.pid = 2_000_000_000;
hb.started_at = 12345;
write_heartbeat(&dir, &hb).unwrap();
let report = enumerate_live(&dir, Duration::from_secs(5)).unwrap();
assert!(report.entries.is_empty());
assert!(!dir.join(format!("{}.json", hb.pid)).exists());
}
#[test]
fn enumerate_live_deletes_mismatched_start_time_entry() {
let root = tempfile::tempdir().unwrap();
let dir = root.path().join("khive.db.walpin");
let mut hb = heartbeat(std::process::id());
hb.started_at = 1;
write_heartbeat(&dir, &hb).unwrap();
let report = enumerate_live(&dir, Duration::from_secs(5)).unwrap();
assert!(
report.entries.is_empty(),
"mismatched identity must fail the gate"
);
assert!(!dir.join(format!("{}.json", hb.pid)).exists());
}
#[test]
fn enumerate_live_deletes_stale_updated_at_entry() {
let root = tempfile::tempdir().unwrap();
let dir = root.path().join("khive.db.walpin");
let mut hb = heartbeat(std::process::id());
hb.oldest_tx_started_at = None;
hb.updated_at = now_epoch_secs() - 3600; write_heartbeat(&dir, &hb).unwrap();
let report = enumerate_live(&dir, Duration::from_secs(5)).unwrap();
assert_eq!(report.reporting().count(), 0);
assert_eq!(report.unknown_pids().collect::<Vec<_>>(), vec![hb.pid]);
assert!(!report.fully_attributed());
assert!(!dir.join(format!("{}.json", hb.pid)).exists());
}
#[test]
fn enumerate_live_subsecond_sweep_interval_does_not_collapse_freshness_window() {
let root = tempfile::tempdir().unwrap();
let dir = root.path().join("khive.db.walpin");
let hb = heartbeat(std::process::id());
write_heartbeat(&dir, &hb).unwrap();
let report = enumerate_live(&dir, Duration::from_millis(200)).unwrap();
assert_eq!(
report.reporting().count(),
1,
"must not be spuriously stale"
);
}
#[test]
fn enumerate_live_refuses_symlinked_entry_as_unknown_without_touching_target() {
let root = tempfile::tempdir().unwrap();
let dir = root.path().join("khive.db.walpin");
ensure_sidecar_dir(&dir).unwrap();
let real = root.path().join("elsewhere.txt");
fs::write(&real, b"precious").unwrap();
let link = dir.join("42.json");
std::os::unix::fs::symlink(&real, &link).unwrap();
let report = enumerate_live(&dir, Duration::from_secs(5)).unwrap();
assert!(report.reporting().count() == 0);
assert_eq!(report.unknown_pids().collect::<Vec<_>>(), vec![42]);
assert!(!report.fully_attributed());
assert_eq!(fs::read_to_string(&real).unwrap(), "precious");
assert!(
link.exists(),
"the symlink itself must not be deleted either"
);
}
#[test]
fn enumerate_live_refuses_non_owned_entry_before_reading_contents() {
let root = tempfile::tempdir().unwrap();
let dir = root.path().join("khive.db.walpin");
let hb = heartbeat(std::process::id());
write_heartbeat(&dir, &hb).unwrap();
let meta = fs::symlink_metadata(dir.join(format!("{}.json", hb.pid))).unwrap();
assert_eq!(
meta.uid(),
current_uid(),
"self-written entries are owned by the current user, exercising the accept path"
);
}
#[test]
fn enumerate_live_refuses_non_compliant_directory_wholesale() {
let root = tempfile::tempdir().unwrap();
let dir = root.path().join("khive.db.walpin");
fs::create_dir(&dir).unwrap();
fs::set_permissions(&dir, fs::Permissions::from_mode(0o755)).unwrap();
let err = enumerate_live(&dir, Duration::from_secs(5))
.expect_err("non-compliant directory must be refused, not silently enumerated");
assert!(err.to_string().contains("expected 0700"));
}
#[test]
fn enumerate_live_missing_directory_is_ok_empty_not_a_failure() {
let root = tempfile::tempdir().unwrap();
let dir = root.path().join("khive.db.walpin");
let report = enumerate_live(&dir, Duration::from_secs(5)).unwrap();
assert!(report.entries.is_empty());
}
#[test]
fn enumerate_live_classifies_registered_silent_beacon_with_no_heartbeat() {
let root = tempfile::tempdir().unwrap();
let dir = root.path().join("khive.db.walpin");
let b = beacon(std::process::id());
write_beacon(&dir, &b).unwrap();
let report = enumerate_live(&dir, Duration::from_secs(5)).unwrap();
assert_eq!(report.reporting().count(), 0);
assert_eq!(
report.registered_silent_pids().collect::<Vec<_>>(),
vec![std::process::id()]
);
assert!(report.fully_attributed());
}
#[test]
fn enumerate_live_reporting_wins_over_registered_silent_for_same_pid() {
let root = tempfile::tempdir().unwrap();
let dir = root.path().join("khive.db.walpin");
let pid = std::process::id();
write_beacon(&dir, &beacon(pid)).unwrap();
write_heartbeat(&dir, &heartbeat(pid)).unwrap();
let report = enumerate_live(&dir, Duration::from_secs(5)).unwrap();
assert_eq!(report.reporting().count(), 1);
assert_eq!(report.registered_silent_pids().count(), 0);
}
#[test]
fn enumerate_live_deletes_dead_beacon() {
let root = tempfile::tempdir().unwrap();
let dir = root.path().join("khive.db.walpin");
let mut b = beacon(std::process::id());
b.pid = 2_000_000_001;
b.started_at = 12345;
write_beacon(&dir, &b).unwrap();
let report = enumerate_live(&dir, Duration::from_secs(5)).unwrap();
assert!(report.entries.is_empty());
assert!(!beacon_path(&dir, b.pid).exists());
}
#[test]
fn write_beacon_refuses_symlinked_target() {
let root = tempfile::tempdir().unwrap();
let dir = root.path().join("khive.db.walpin");
ensure_sidecar_dir(&dir).unwrap();
let real = root.path().join("elsewhere.txt");
fs::write(&real, b"nope").unwrap();
let b = beacon(999_998);
let target = beacon_path(&dir, b.pid);
std::os::unix::fs::symlink(&real, &target).unwrap();
let err = write_beacon(&dir, &b).expect_err("symlinked target must be refused");
assert!(err.to_string().contains("symlink"));
assert_eq!(fs::read_to_string(&real).unwrap(), "nope");
}
#[test]
fn enumerate_live_classifies_stale_beacon_as_unknown() {
let root = tempfile::tempdir().unwrap();
let dir = root.path().join("khive.db.walpin");
let pid = std::process::id();
write_beacon(&dir, &beacon(pid)).unwrap();
let beacon_file = fs::OpenOptions::new()
.write(true)
.open(dir.join(format!("{pid}.beacon")))
.unwrap();
beacon_file
.set_modified(SystemTime::now() - Duration::from_secs(3600))
.unwrap();
let report = enumerate_live(&dir, Duration::from_secs(5)).unwrap();
assert_eq!(report.registered_silent_pids().count(), 0);
assert_eq!(report.unknown_pids().collect::<Vec<_>>(), vec![pid]);
assert!(!report.fully_attributed());
assert!(
!beacon_path(&dir, pid).exists(),
"a stale beacon must be deleted, not left to re-classify next sweep"
);
}
#[test]
fn enumerate_live_stale_heartbeat_with_fresh_beacon_stays_unknown_not_registered_silent() {
let root = tempfile::tempdir().unwrap();
let dir = root.path().join("khive.db.walpin");
let pid = std::process::id();
write_beacon(&dir, &beacon(pid)).unwrap();
let mut hb = heartbeat(pid);
hb.oldest_tx_started_at = None;
hb.updated_at = now_epoch_secs() - 3600;
write_heartbeat(&dir, &hb).unwrap();
let report = enumerate_live(&dir, Duration::from_secs(5)).unwrap();
assert_eq!(report.reporting().count(), 0);
assert_eq!(
report.registered_silent_pids().collect::<Vec<_>>(),
Vec::<u32>::new(),
"a co-existing fresh beacon must not rescue a PID with a stale heartbeat"
);
assert_eq!(report.unknown_pids().collect::<Vec<_>>(), vec![pid]);
}
#[test]
#[cfg(target_os = "macos")]
fn census_holders_macos_discovers_self_as_a_holder_of_an_open_db_file() {
let root = tempfile::tempdir().unwrap();
let db_path = root.path().join("test.db");
let file = fs::File::create(&db_path).unwrap();
let census = census_holders(&db_path).expect("census must succeed for a live target");
assert!(
census.holders.contains(&std::process::id()),
"this process holds {db_path:?} open and must appear in its own OS-derived census"
);
assert!(
!census.truncated,
"self was found; the self-canary must not report truncation on its own"
);
drop(file);
}
#[test]
#[cfg(unix)]
fn heartbeat_freshness_uses_producer_cadence_not_enumerator_interval() {
let root = tempfile::tempdir().unwrap();
let dir = root.path().join("khive.db.walpin");
let pid = std::process::id();
let mut slow = heartbeat(pid);
slow.oldest_tx_started_at = None;
slow.sweep_interval_ms = 60_000;
slow.updated_at = now_epoch_secs() - 30;
write_heartbeat(&dir, &slow).unwrap();
let report = enumerate_live(&dir, Duration::from_millis(500)).unwrap();
assert_eq!(
report.reporting().count(),
1,
"a heartbeat 30s old under a recorded 60s cadence is fresh: {report:?}"
);
let mut fast = heartbeat(pid);
fast.oldest_tx_started_at = None;
fast.sweep_interval_ms = 1_000;
fast.updated_at = now_epoch_secs() - 30;
write_heartbeat(&dir, &fast).unwrap();
let report = enumerate_live(&dir, Duration::from_secs(60)).unwrap();
assert_eq!(report.reporting().count(), 0);
assert_eq!(report.unknown_pids().collect::<Vec<_>>(), vec![pid]);
}
#[test]
#[cfg(unix)]
fn enumerate_live_new_style_heartbeat_uses_mtime_not_stale_updated_at() {
let root = tempfile::tempdir().unwrap();
let dir = root.path().join("khive.db.walpin");
let mut hb = heartbeat(std::process::id());
hb.updated_at = now_epoch_secs() - 3600;
write_heartbeat(&dir, &hb).unwrap();
let report = enumerate_live(&dir, Duration::from_secs(5)).unwrap();
assert_eq!(
report.reporting().count(),
1,
"a new-style record with a fresh mtime must classify live regardless \
of a stale `updated_at` body field: {report:?}"
);
}
#[test]
#[cfg(unix)]
fn enumerate_live_new_style_heartbeat_stale_via_mtime_despite_fresh_updated_at() {
let root = tempfile::tempdir().unwrap();
let dir = root.path().join("khive.db.walpin");
let pid = std::process::id();
let hb = heartbeat(pid); write_heartbeat(&dir, &hb).unwrap();
let heartbeat_path = dir.join(format!("{pid}.json"));
let file = fs::OpenOptions::new()
.write(true)
.open(&heartbeat_path)
.unwrap();
file.set_modified(SystemTime::now() - Duration::from_secs(3600))
.unwrap();
let report = enumerate_live(&dir, Duration::from_secs(5)).unwrap();
assert_eq!(report.reporting().count(), 0);
assert_eq!(report.unknown_pids().collect::<Vec<_>>(), vec![pid]);
assert!(
!heartbeat_path.exists(),
"a new-style entry stale by mtime must be deleted, not merely unreported"
);
}
#[test]
#[cfg(unix)]
fn stale_window_from_boundary_inclusive_and_floors_subsecond_cadence_at_three_seconds() {
assert_eq!(stale_window_from(Duration::from_secs(2)), 6);
assert_eq!(stale_window_from(Duration::from_millis(100)), 3);
assert_eq!(stale_window_from(Duration::from_millis(999)), 3);
assert_eq!(stale_window_from(Duration::from_secs(1)), 3);
}
#[test]
#[cfg(unix)]
fn enumerate_live_new_style_heartbeat_boundary_is_inclusive() {
let root = tempfile::tempdir().unwrap();
let dir = root.path().join("khive.db.walpin");
let pid = std::process::id();
let mut hb = heartbeat(pid);
hb.sweep_interval_ms = 2_000;
write_heartbeat(&dir, &hb).unwrap();
let heartbeat_path = dir.join(format!("{pid}.json"));
let touch = |age_secs: u64| {
let file = fs::OpenOptions::new()
.write(true)
.open(&heartbeat_path)
.unwrap();
file.set_modified(SystemTime::now() - Duration::from_secs(age_secs))
.unwrap();
};
touch(6);
let report = enumerate_live(&dir, Duration::from_secs(5)).unwrap();
assert_eq!(
report.reporting().count(),
1,
"exactly 3x the declared interval old must still classify live: {report:?}"
);
touch(7);
let report = enumerate_live(&dir, Duration::from_secs(5)).unwrap();
assert_eq!(
report.reporting().count(),
0,
"one second past 3x the declared interval must classify stale: {report:?}"
);
}
#[test]
#[cfg(unix)]
fn enumerate_live_new_style_heartbeat_subsecond_cadence_floors_at_three_seconds() {
let root = tempfile::tempdir().unwrap();
let dir = root.path().join("khive.db.walpin");
let pid = std::process::id();
let mut hb = heartbeat(pid);
hb.sweep_interval_ms = 100;
write_heartbeat(&dir, &hb).unwrap();
let heartbeat_path = dir.join(format!("{pid}.json"));
let file = fs::OpenOptions::new()
.write(true)
.open(&heartbeat_path)
.unwrap();
file.set_modified(SystemTime::now() - Duration::from_secs(2))
.unwrap();
let report = enumerate_live(&dir, Duration::from_secs(5)).unwrap();
assert_eq!(
report.reporting().count(),
1,
"a 100ms declared cadence must floor its window at 3s, not 1s: 2s old must \
still classify live: {report:?}"
);
}
#[test]
fn attribution_is_evidence_backed_fails_closed_on_missing_or_unrecognized_value() {
let mut hb = heartbeat(std::process::id());
hb.attribution_basis = Some("origin".to_string());
assert!(hb.attribution_is_evidence_backed());
hb.attribution_basis = Some("fallback".to_string());
assert!(!hb.attribution_is_evidence_backed());
hb.attribution_basis = None;
assert!(
!hb.attribution_is_evidence_backed(),
"a missing attribution_basis must never be read as evidence-backed"
);
hb.attribution_basis = Some("some-future-value".to_string());
assert!(
!hb.attribution_is_evidence_backed(),
"an unrecognized value must degrade to fallback-confidence, never guess origin"
);
}
#[test]
#[cfg(unix)]
fn fifo_sidecar_entry_is_refused_without_blocking() {
let root = tempfile::tempdir().unwrap();
let dir = root.path().join("khive.db.walpin");
let pid = std::process::id();
write_beacon(&dir, &beacon(pid)).unwrap();
use std::os::unix::ffi::OsStrExt;
let fifo = dir.join("999999941.json");
let c_path = std::ffi::CString::new(fifo.as_os_str().as_bytes()).unwrap();
let rc = unsafe { libc::mkfifo(c_path.as_ptr(), 0o600) };
assert_eq!(rc, 0, "mkfifo failed: {}", io::Error::last_os_error());
let report = enumerate_live(&dir, Duration::from_secs(5)).unwrap();
assert!(
report.unknown_pids().any(|p| p == 999_999_941),
"a FIFO entry must classify its PID as unknown: {report:?}"
);
assert_eq!(
report.registered_silent_pids().collect::<Vec<_>>(),
vec![pid]
);
}
#[test]
#[cfg(unix)]
fn oversized_sidecar_entry_is_refused() {
let root = tempfile::tempdir().unwrap();
let dir = root.path().join("khive.db.walpin");
let pid = std::process::id();
write_beacon(&dir, &beacon(pid)).unwrap();
fs::write(dir.join("999999942.json"), vec![b'x'; 128 * 1024]).unwrap();
let report = enumerate_live(&dir, Duration::from_secs(5)).unwrap();
assert!(
report.unknown_pids().any(|p| p == 999_999_942),
"an oversized entry must classify its PID as unknown: {report:?}"
);
}
#[test]
#[cfg(any(target_os = "macos", target_os = "linux"))]
fn census_holders_discovers_holder_through_hard_link_path() {
let root = tempfile::tempdir().unwrap();
let db_path = root.path().join("test.db");
fs::File::create(&db_path).unwrap();
let link_path = root.path().join("test-link.db");
fs::hard_link(&db_path, &link_path).unwrap();
let file = fs::File::open(&link_path).unwrap();
let census = census_holders(&db_path).expect("census must succeed for a live target");
assert!(
census.holders.contains(&std::process::id()),
"this process holds the db open via hard link {link_path:?} and must appear in the census for {db_path:?}"
);
drop(file);
}
#[test]
#[cfg(target_os = "macos")]
fn negotiate_buffer_converges_when_the_set_stops_growing() {
let probe = std::cell::Cell::new(0usize);
let sizes = [4usize, 8, 8]; let (items, truncated) = negotiate_buffer::<i32>(
|| {
let i = probe.get().min(sizes.len() - 1);
sizes[i] as std::os::raw::c_int
},
|_buf_ptr, buf_bytes| {
let i = probe.get();
probe.set(i + 1);
if i < 2 {
buf_bytes
} else {
(buf_bytes as usize - 4) as std::os::raw::c_int
}
},
)
.expect("negotiation must succeed once the set stabilizes");
assert!(
!truncated,
"a snapshot that ends up strictly under capacity must not be marked truncated"
);
assert!(!items.is_empty());
}
#[test]
#[cfg(target_os = "macos")]
fn negotiate_buffer_reports_truncated_after_exhausting_retries() {
let (items, truncated) =
negotiate_buffer::<i32>(|| 4 as std::os::raw::c_int, |_buf_ptr, buf_bytes| buf_bytes)
.expect("negotiation must still return a (possibly truncated) result, not error");
assert!(
truncated,
"a buffer that stays exactly full across every retry must be reported truncated"
);
assert!(!items.is_empty());
}
#[test]
#[cfg(target_os = "macos")]
fn negotiate_buffer_propagates_a_failed_size_call() {
let result = negotiate_buffer::<i32>(
|| -1 as std::os::raw::c_int,
|_buf_ptr, buf_bytes| buf_bytes,
);
assert!(result.is_err(), "a non-positive size probe must error out");
}
#[test]
#[cfg(target_os = "macos")]
fn macos_pid_genuinely_gone_only_true_for_esrch() {
assert!(macos_pid_genuinely_gone(Some(libc::ESRCH)));
assert!(!macos_pid_genuinely_gone(Some(libc::EPERM)));
assert!(!macos_pid_genuinely_gone(Some(libc::EACCES)));
assert!(!macos_pid_genuinely_gone(None));
}
#[test]
#[cfg(target_os = "macos")]
fn proc_pidfdinfo_returned_expected_size_boundary() {
let expected = std::mem::size_of::<u64>(); assert!(
proc_pidfdinfo_returned_expected_size(expected as i32, expected),
"an exact match on the expected struct size must be ok"
);
assert!(
!proc_pidfdinfo_returned_expected_size(expected as i32 - 1, expected),
"a positive but short byte count must be an inspection failure"
);
assert!(
!proc_pidfdinfo_returned_expected_size(0, expected),
"a zero return must be an inspection failure"
);
assert!(
!proc_pidfdinfo_returned_expected_size(-1, expected),
"a negative return must be an inspection failure"
);
}
#[test]
#[cfg(target_os = "linux")]
fn census_holders_linux_discovers_self_as_a_holder_of_an_open_db_file() {
let root = tempfile::tempdir().unwrap();
let db_path = root.path().join("test.db");
let file = fs::File::create(&db_path).unwrap();
let census = census_holders(&db_path).expect("census must succeed for a live target");
assert!(
census.holders.contains(&std::process::id()),
"this process holds {db_path:?} open and must appear in its own OS-derived census"
);
assert!(
!census.truncated,
"self was found; the self-canary (and the namespace check, when this test runs \
in the host's own PID namespace) must not report truncation on its own"
);
drop(file);
}
#[test]
#[cfg(target_os = "linux")]
fn linux_proc_gone_only_true_for_not_found() {
assert!(linux_proc_gone(&io::Error::from(io::ErrorKind::NotFound)));
assert!(!linux_proc_gone(&io::Error::from(
io::ErrorKind::PermissionDenied
)));
}
#[test]
#[cfg(target_os = "linux")]
fn pid_ns_is_init_only_true_for_the_fixed_kernel_inode() {
assert!(pid_ns_is_init(PROC_PID_INIT_INO));
assert!(!pid_ns_is_init(PROC_PID_INIT_INO + 1));
assert!(!pid_ns_is_init(0));
assert!(!pid_ns_is_init(12345));
}
#[test]
#[cfg(target_os = "linux")]
fn proc_mount_restricts_visibility_classifies_hidepid_and_subset() {
assert!(!proc_mount_restricts_visibility(
"rw,nosuid,nodev,noexec,relatime"
));
assert!(proc_mount_restricts_visibility("rw,hidepid=2"));
assert!(proc_mount_restricts_visibility("rw,hidepid=invisible"));
assert!(proc_mount_restricts_visibility("rw,hidepid=ptraceable"));
assert!(proc_mount_restricts_visibility("rw,subset=pid"));
assert!(!proc_mount_restricts_visibility("hidepid=0"));
assert!(!proc_mount_restricts_visibility("rw,hidepid=off"));
assert!(proc_mount_restricts_visibility("rw,hidepid"));
}
#[test]
#[cfg(target_os = "linux")]
fn proc_mounts_restricted_in_is_any_restrictive_across_stacked_mounts() {
let clean = "36 25 0:16 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw";
let restricted = "99 25 0:34 / /proc rw,relatime - proc proc rw,hidepid=2";
let clean_then_restricted = format!("{clean}\n{restricted}");
let restricted_then_clean = format!("{restricted}\n{clean}");
assert_eq!(proc_mounts_restricted_in(clean), Some(false));
assert_eq!(proc_mounts_restricted_in(restricted), Some(true));
assert_eq!(
proc_mounts_restricted_in(&clean_then_restricted),
Some(true)
);
assert_eq!(
proc_mounts_restricted_in(&restricted_then_clean),
Some(true)
);
assert_eq!(
proc_mounts_restricted_in("36 25 0:16 / /sys rw - sysfs sysfs rw"),
None
);
}
#[test]
#[cfg(target_os = "linux")]
fn proc_mount_is_visibility_restricted_reads_this_hosts_own_proc_mount() {
assert!(
proc_mount_is_visibility_restricted().is_some(),
"expected to find and parse this process's own /proc mount entry in \
/proc/self/mountinfo"
);
}
#[test]
fn census_result_is_complete_reflects_uninspectable_pids() {
let complete = CensusResult {
holders: std::collections::HashSet::from([1, 2]),
uninspectable_pids: Vec::new(),
truncated: false,
};
assert!(complete.is_complete());
let incomplete = CensusResult {
holders: std::collections::HashSet::from([1]),
uninspectable_pids: vec![7],
truncated: false,
};
assert!(!incomplete.is_complete());
}
#[test]
fn census_result_is_complete_reflects_truncated() {
let truncated = CensusResult {
holders: std::collections::HashSet::from([1]),
uninspectable_pids: Vec::new(),
truncated: true,
};
assert!(!truncated.is_complete());
}
#[test]
fn self_canary_marks_truncated_when_own_pid_missing() {
let mut census = CensusResult {
holders: std::collections::HashSet::from([std::process::id().wrapping_add(1)]),
uninspectable_pids: Vec::new(),
truncated: false,
};
census.apply_self_canary();
assert!(
census.truncated,
"a census that discovered other holders but not the calling process itself is \
positive proof of a missed enumeration and must be marked incomplete"
);
}
#[test]
fn self_canary_leaves_a_correct_census_untouched() {
let mut census = CensusResult {
holders: std::collections::HashSet::from([std::process::id()]),
uninspectable_pids: Vec::new(),
truncated: false,
};
census.apply_self_canary();
assert!(
!census.truncated,
"self was found; the canary must not fire"
);
}
#[test]
fn self_canary_does_not_clear_an_existing_truncated_flag() {
let mut census = CensusResult {
holders: std::collections::HashSet::from([std::process::id()]),
uninspectable_pids: Vec::new(),
truncated: true,
};
census.apply_self_canary();
assert!(
census.truncated,
"the self-canary only ever sets `truncated`; it must never clear a flag another \
step already raised"
);
}
}