use super::io_other;
use khive_fs::directory_walk::{
walk_to_directory, AncestorLinkPolicy, AncestorWalkEndpoint, BudgetExhausted,
ANCESTOR_LINK_BUDGET,
};
use khive_fs::fd_relative::{clear_errno, current_errno, errno_location};
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;
use std::time::{Duration, SystemTime};
pub(super) const MAX_SIDECAR_ENTRY_BYTES: u64 = 64 * 1024;
const RAW_SCAN_FACTOR: usize = 8;
pub(super) fn current_uid() -> u32 {
unsafe { libc::geteuid() }
}
#[cfg(test)]
thread_local! {
static REMOVE_IF_SAME_RACE_HOOK: std::cell::RefCell<Option<Box<dyn FnOnce()>>> =
const { std::cell::RefCell::new(None) };
}
#[cfg(test)]
pub(super) fn set_remove_if_same_race_hook(hook: impl FnOnce() + 'static) {
REMOVE_IF_SAME_RACE_HOOK.with(|cell| *cell.borrow_mut() = Some(Box::new(hook)));
}
#[cfg(test)]
fn take_remove_if_same_race_hook() -> Option<Box<dyn FnOnce()>> {
REMOVE_IF_SAME_RACE_HOOK.with(|cell| cell.borrow_mut().take())
}
#[cfg(test)]
thread_local! {
static LIST_NAMES_READDIR_FAULT: std::cell::Cell<Option<libc::c_int>> =
const { std::cell::Cell::new(None) };
}
#[cfg(test)]
pub(super) fn set_list_names_readdir_fault(errno: libc::c_int) {
LIST_NAMES_READDIR_FAULT.with(|cell| cell.set(Some(errno)));
}
fn take_list_names_readdir_fault() -> Option<libc::c_int> {
#[cfg(test)]
{
LIST_NAMES_READDIR_FAULT.with(|cell| cell.take())
}
#[cfg(not(test))]
{
None
}
}
pub(super) fn readdir_null_is_error(errno: libc::c_int) -> bool {
errno != 0
}
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
}
fn sidecar_walk_error(error: io::Error) -> io::Error {
let component = error
.get_ref()
.and_then(|inner| inner.downcast_ref::<BudgetExhausted>())
.map(|exhausted| exhausted.component.clone());
match component {
Some(name) => io_other(format!(
"walpin sidecar ancestor {name:?} exceeded the symlink resolution depth budget"
)),
None => error,
}
}
pub(super) struct SidecarDirHandle(fs::File);
pub(super) struct CheckedEntry {
pub(super) body: Vec<u8>,
pub(super) mtime: SystemTime,
device: u64,
inode: u64,
}
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 mut policy = AncestorLinkPolicy::new(AncestorWalkEndpoint::TargetParent);
let walked = walk_to_directory(path, &mut policy, ANCESTOR_LINK_BUDGET);
let mut pinned = walked.map_err(sidecar_walk_error)?;
pinned
.pop()
.ok_or_else(|| io_other("walpin sidecar ancestor walk pinned no directory"))
}
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_entry(&self, name: &str) -> io::Result<Option<CheckedEntry>> {
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.uid() != current_uid() {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!("walpin sidecar entry {name:?} is not owned by the current user"),
));
}
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(CheckedEntry {
body: buf,
mtime,
device: meta.dev(),
inode: meta.ino(),
}))
}
pub(super) fn read_checked(&self, name: &str) -> io::Result<Option<(Vec<u8>, SystemTime)>> {
Ok(self
.read_checked_entry(name)?
.map(|entry| (entry.body, entry.mtime)))
}
pub(super) fn remove_if_same(&self, name: &str, expected: &CheckedEntry) -> io::Result<bool> {
let Some(current) = self.stat_entry(name)? else {
return Ok(false);
};
if is_symlink_mode(current.st_mode) || (current.st_mode & libc::S_IFMT) != libc::S_IFREG {
return Err(io_other(format!(
"refusing to remove replaced walpin sidecar entry {name:?}"
)));
}
let current_device = u64::try_from(i128::from(current.st_dev)).unwrap_or(u64::MAX);
let current_inode = current.st_ino;
if current_device != expected.device || current_inode != expected.inode {
return Ok(false);
}
#[cfg(test)]
if let Some(hook) = take_remove_if_same_race_hook() {
hook();
}
self.unlink_tolerant(name)?;
Ok(true)
}
pub(super) fn list_names(&self, max: usize) -> io::Result<(Vec<String>, 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 producer_temps = Vec::new();
let mut truncated = false;
loop {
clear_errno();
let entry = if let Some(errno) = take_list_names_readdir_fault() {
unsafe { *errno_location() = errno };
std::ptr::null_mut()
} else {
unsafe { libc::readdir(dirp) }
};
if entry.is_null() {
if readdir_null_is_error(current_errno()) {
let err = io::Error::last_os_error();
unsafe { libc::closedir(dirp) };
return Err(err);
}
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 {
let candidate = unsafe { CStr::from_ptr((*entry).d_name.as_ptr()) }
.to_string_lossy()
.into_owned();
if super::producer_temp_identity(&candidate).is_some() {
if producer_temps.len() == max {
truncated = true;
break;
}
producer_temps.push(candidate);
}
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, producer_temps, 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)
}