use crate::CoreError;
use std::ffi::CString;
use std::fs;
use std::io::{Read, Write};
use std::os::unix::io::{AsRawFd, FromRawFd, OwnedFd, RawFd};
use std::path::Path;
use std::time::UNIX_EPOCH;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PathFingerprint {
pub len: u64,
pub modified_ns: u128,
}
pub fn path_fingerprint(path: &Path) -> Result<PathFingerprint, CoreError> {
let metadata = std::fs::metadata(path).map_err(|err| {
CoreError::sys(err.raw_os_error().unwrap_or(libc::EIO), "path_fingerprint")
})?;
let modified_ns = metadata
.modified()
.ok()
.and_then(|modified| modified.duration_since(UNIX_EPOCH).ok())
.map(|duration| duration.as_nanos())
.unwrap_or_default();
Ok(PathFingerprint {
len: metadata.len(),
modified_ns,
})
}
pub fn path_exists(path: &str) -> bool {
match std::ffi::CString::new(path) {
Ok(c) => unsafe { libc::access(c.as_ptr(), libc::F_OK) == 0 },
Err(_) => false,
}
}
pub fn path_lstat_exists(path: &str) -> bool {
match std::ffi::CString::new(path) {
Ok(c) => unsafe {
let mut stat = std::mem::zeroed();
libc::lstat(c.as_ptr(), &mut stat) == 0
},
Err(_) => false,
}
}
pub fn read_to_string(path: &str) -> Result<String, CoreError> {
std::fs::read_to_string(path)
.map_err(|err| CoreError::sys(err.raw_os_error().unwrap_or(libc::EIO), "read_to_string"))
}
pub fn readahead(fd: impl AsRawFd, offset: u64, len: usize) -> Result<(), CoreError> {
readahead_raw(fd.as_raw_fd(), offset, len)
}
pub const FADV_NORMAL: i32 = libc::POSIX_FADV_NORMAL;
pub const FADV_RANDOM: i32 = libc::POSIX_FADV_RANDOM;
pub const FADV_SEQUENTIAL: i32 = libc::POSIX_FADV_SEQUENTIAL;
pub const FADV_WILLNEED: i32 = libc::POSIX_FADV_WILLNEED;
pub const FADV_DONTNEED: i32 = libc::POSIX_FADV_DONTNEED;
pub const FADV_NOREUSE: i32 = libc::POSIX_FADV_NOREUSE;
pub fn fadvise(fd: impl AsRawFd, offset: u64, len: usize, advice: i32) -> Result<(), CoreError> {
let ret = unsafe {
libc::posix_fadvise(
fd.as_raw_fd(),
offset as libc::off_t,
len as libc::off_t,
advice,
)
};
if ret == 0 {
Ok(())
} else {
Err(CoreError::sys(ret, "posix_fadvise"))
}
}
pub fn mmap_madvise(
fd: impl AsRawFd,
offset: u64,
len: usize,
touch: bool,
) -> Result<(), CoreError> {
mmap_madvise_raw(fd.as_raw_fd(), offset, len, touch)
}
#[cfg(any(target_os = "linux", target_os = "android"))]
fn mmap_madvise_raw(
fd: libc::c_int,
offset: u64,
len: usize,
touch: bool,
) -> Result<(), CoreError> {
if len == 0 {
return Ok(());
}
let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
if page_size <= 0 {
return Err(CoreError::sys(libc::EINVAL, "sysconf(_SC_PAGESIZE)"));
}
let page_size = page_size as u64;
if offset % page_size != 0 || offset > libc::off_t::MAX as u64 {
return Err(CoreError::sys(libc::EINVAL, "mmap"));
}
let ptr = unsafe {
libc::mmap(
std::ptr::null_mut(),
len,
libc::PROT_READ,
libc::MAP_PRIVATE,
fd,
offset as libc::off_t,
)
};
if ptr == libc::MAP_FAILED {
let code = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
return Err(CoreError::sys(code, "mmap"));
}
let result = if unsafe { libc::madvise(ptr, len, libc::MADV_WILLNEED) } == -1 {
let code = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
Err(CoreError::sys(code, "madvise"))
} else {
if touch {
let mut pos = 0usize;
let page_size = page_size as usize;
while pos < len {
unsafe {
std::ptr::read_volatile((ptr as *const u8).add(pos));
}
pos = pos.saturating_add(page_size);
}
}
Ok(())
};
if unsafe { libc::munmap(ptr, len) } == -1 {
let code = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
return Err(CoreError::sys(code, "munmap"));
}
result
}
#[cfg(not(any(target_os = "linux", target_os = "android")))]
fn mmap_madvise_raw(
_fd: libc::c_int,
_offset: u64,
_len: usize,
_touch: bool,
) -> Result<(), CoreError> {
Err(CoreError::sys(libc::ENOSYS, "mmap"))
}
#[cfg(any(target_os = "linux", target_os = "android"))]
fn readahead_raw(fd: libc::c_int, offset: u64, len: usize) -> Result<(), CoreError> {
if offset > libc::off64_t::MAX as u64 {
return Err(CoreError::sys(libc::EINVAL, "readahead"));
}
let count = len as libc::size_t;
let offset = offset as libc::off64_t;
loop {
let ret = unsafe { libc::syscall(readahead_syscall_number(), fd, offset, count) };
if ret == -1 {
let code = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
if code == libc::EINTR {
continue;
}
return Err(CoreError::sys(code, "readahead"));
}
return Ok(());
}
}
#[cfg(not(any(target_os = "linux", target_os = "android")))]
fn readahead_raw(_fd: libc::c_int, _offset: u64, _len: usize) -> Result<(), CoreError> {
Err(CoreError::sys(libc::ENOSYS, "readahead"))
}
#[cfg(target_os = "linux")]
#[inline(always)]
const fn readahead_syscall_number() -> libc::c_long {
libc::SYS_readahead
}
#[cfg(all(target_os = "android", target_arch = "aarch64"))]
#[inline(always)]
const fn readahead_syscall_number() -> libc::c_long {
213
}
#[cfg(all(target_os = "android", target_arch = "arm"))]
#[inline(always)]
const fn readahead_syscall_number() -> libc::c_long {
225
}
#[cfg(all(target_os = "android", target_arch = "x86_64"))]
#[inline(always)]
const fn readahead_syscall_number() -> libc::c_long {
187
}
#[cfg(all(target_os = "android", target_arch = "x86"))]
#[inline(always)]
const fn readahead_syscall_number() -> libc::c_long {
225
}
const TEMP_ATTEMPTS: usize = 32;
fn cstr(s: &str) -> Result<CString, CoreError> {
CString::new(s).map_err(|_| CoreError::sys(libc::EINVAL, "path:nul_byte"))
}
fn openat_raw(dirfd: RawFd, name: &str, flags: i32, mode: u32) -> Result<RawFd, CoreError> {
let c = cstr(name)?;
let fd = unsafe { libc::openat(dirfd, c.as_ptr(), flags, mode as libc::c_uint) };
if fd < 0 {
Err(std::io::Error::last_os_error().into())
} else {
Ok(fd)
}
}
fn openat_dir(dirfd: RawFd, name: &str) -> Result<OwnedFd, CoreError> {
let fd = openat_raw(
dirfd,
name,
libc::O_RDONLY | libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_CLOEXEC,
0,
)?;
Ok(unsafe { OwnedFd::from_raw_fd(fd) })
}
fn walk_dir(path: &Path, create_mode: Option<u32>) -> Result<OwnedFd, CoreError> {
let abs = if path.is_absolute() {
path.to_path_buf()
} else {
std::env::current_dir()?.join(path)
};
let mut dir = openat_dir(libc::AT_FDCWD, "/")?;
for comp in abs.components() {
use std::path::Component;
match comp {
Component::RootDir | Component::CurDir => {}
Component::Normal(name) => {
let name = name
.to_str()
.ok_or_else(|| CoreError::sys(libc::EINVAL, "path:non_utf8"))?;
let next = match openat_dir(dir.as_raw_fd(), name) {
Ok(fd) => fd,
Err(e) if e.raw_os_error() == Some(libc::ENOENT) && create_mode.is_some() => {
let c = cstr(name)?;
if unsafe {
libc::mkdirat(
dir.as_raw_fd(),
c.as_ptr(),
create_mode.unwrap() as libc::mode_t,
)
} != 0
{
if std::io::Error::last_os_error().raw_os_error() == Some(libc::EEXIST)
{
openat_dir(dir.as_raw_fd(), name)?
} else {
return Err(std::io::Error::last_os_error().into());
}
} else {
openat_dir(dir.as_raw_fd(), name)?
}
}
Err(e) => return Err(e),
};
dir = next;
}
Component::ParentDir => {
return Err(CoreError::sys(libc::EINVAL, "path:parent_component"));
}
Component::Prefix(_) => unreachable!("non-Windows path"),
}
}
Ok(dir)
}
fn open_dir_nofollow(path: &Path) -> Result<OwnedFd, CoreError> {
walk_dir(path, None)
}
fn fstat(fd: RawFd) -> Result<libc::stat, CoreError> {
let mut st: libc::stat = unsafe { std::mem::zeroed() };
if unsafe { libc::fstat(fd, &mut st) } != 0 {
Err(std::io::Error::last_os_error().into())
} else {
Ok(st)
}
}
fn unlink_name(dirfd: RawFd, name: &str) -> Result<(), CoreError> {
let c = cstr(name)?;
if unsafe { libc::unlinkat(dirfd, c.as_ptr(), 0) } != 0 {
Err(std::io::Error::last_os_error().into())
} else {
Ok(())
}
}
fn basename(target: &Path) -> Result<String, CoreError> {
target
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.ok_or_else(|| CoreError::sys(libc::EINVAL, "path:no_file_name"))
}
fn parent_dir(target: &Path) -> &Path {
target
.parent()
.filter(|p| !p.as_os_str().is_empty())
.unwrap_or_else(|| Path::new("."))
}
fn open_parent_nofollow(target: &Path) -> Result<OwnedFd, CoreError> {
let dir = open_dir_nofollow(parent_dir(target))?;
let st = fstat(dir.as_raw_fd())?;
let euid = unsafe { libc::geteuid() };
if st.st_uid != euid {
return Err(CoreError::sys(libc::EACCES, "parent_dir_owner"));
}
Ok(dir)
}
struct TmpGuard {
dirfd: RawFd,
name: String,
}
impl Drop for TmpGuard {
fn drop(&mut self) {
let _ = unlink_name(self.dirfd, &self.name);
}
}
pub fn write_atomic(path: impl AsRef<Path>, content: &[u8]) -> Result<(), CoreError> {
let target = path.as_ref();
let file_name = basename(target)?;
let dir = open_parent_nofollow(target)?;
let dirfd = dir.as_raw_fd();
for attempt in 0..TEMP_ATTEMPTS {
let tmp_name = format!(".{file_name}.{}.{attempt}", std::process::id());
match openat_raw(
dirfd,
&tmp_name,
libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL | libc::O_CLOEXEC,
0o600,
) {
Ok(raw) => {
let mut file = unsafe { fs::File::from_raw_fd(raw) };
let _guard = TmpGuard {
dirfd,
name: tmp_name.clone(),
};
file.write_all(content)?;
file.sync_all()?;
drop(file);
let c_tmp = cstr(&tmp_name)?;
let c_final = cstr(&file_name)?;
if unsafe { libc::renameat(dirfd, c_tmp.as_ptr(), dirfd, c_final.as_ptr()) } != 0 {
return Err(std::io::Error::last_os_error().into());
}
std::mem::forget(_guard);
return Ok(());
}
Err(e) if e.raw_os_error() == Some(libc::EEXIST) => continue,
Err(e) => return Err(e),
}
}
Err(CoreError::sys(libc::EEXIST, "temp:exhausted"))
}
pub fn read_nofollow(path: impl AsRef<Path>) -> Result<String, CoreError> {
let target = path.as_ref();
let file_name = basename(target)?;
let dir = open_parent_nofollow(target)?;
let fd = openat_raw(
dir.as_raw_fd(),
&file_name,
libc::O_RDONLY | libc::O_NOFOLLOW | libc::O_CLOEXEC,
0,
)?;
let mut file = unsafe { fs::File::from_raw_fd(fd) };
let mut content = String::new();
file.read_to_string(&mut content)?;
Ok(content)
}
pub fn open_append_nofollow(path: impl AsRef<Path>) -> Result<fs::File, CoreError> {
let target = path.as_ref();
let file_name = basename(target)?;
let dir = open_parent_nofollow(target)?;
let fd = openat_raw(
dir.as_raw_fd(),
&file_name,
libc::O_WRONLY | libc::O_CREAT | libc::O_APPEND | libc::O_NOFOLLOW | libc::O_CLOEXEC,
0o644,
)?;
Ok(unsafe { fs::File::from_raw_fd(fd) })
}
pub fn remove_nofollow(path: impl AsRef<Path>) -> Result<(), CoreError> {
let target = path.as_ref();
let file_name = basename(target)?;
let dir = open_parent_nofollow(target)?;
unlink_name(dir.as_raw_fd(), &file_name)
}
pub fn ensure_state_dir(dir: impl AsRef<Path>) -> Result<OwnedFd, CoreError> {
let dir = dir.as_ref();
let fd = walk_dir(dir, Some(0o700))?;
let st = fstat(fd.as_raw_fd())?;
let euid = unsafe { libc::geteuid() };
if st.st_uid != euid {
return Err(CoreError::sys(libc::EACCES, "state_dir_owner"));
}
Ok(fd)
}
#[cfg(test)]
mod tests {
#[cfg(target_os = "linux")]
#[test]
fn test_readahead_syscall_number_linux_matches_libc() {
assert_eq!(super::readahead_syscall_number(), libc::SYS_readahead);
}
#[cfg(all(target_os = "android", target_arch = "aarch64"))]
#[test]
fn test_readahead_syscall_number_android_aarch64() {
assert_eq!(super::readahead_syscall_number(), 213);
}
#[cfg(all(target_os = "android", target_arch = "arm"))]
#[test]
fn test_readahead_syscall_number_android_arm() {
assert_eq!(super::readahead_syscall_number(), 225);
}
#[cfg(all(target_os = "android", target_arch = "x86_64"))]
#[test]
fn test_readahead_syscall_number_android_x86_64() {
assert_eq!(super::readahead_syscall_number(), 187);
}
#[cfg(all(target_os = "android", target_arch = "x86"))]
#[test]
fn test_readahead_syscall_number_android_x86() {
assert_eq!(super::readahead_syscall_number(), 225);
}
}
#[cfg(test)]
mod safe_fs_tests {
use super::*;
use std::os::unix::fs::symlink;
use std::path::PathBuf;
fn tmpdir(name: &str) -> PathBuf {
let d = std::env::temp_dir().join(format!(
"coreshift_safe_fs_dir_{}_{name}",
std::process::id()
));
let _ = fs::remove_dir_all(&d);
fs::create_dir_all(&d).unwrap();
d
}
#[test]
fn test_write_atomic_creates_regular_file() {
let dir = tmpdir("w");
let p = dir.join("out.txt");
write_atomic(&p, b"hello").unwrap();
assert_eq!(fs::read_to_string(&p).unwrap(), "hello");
assert!(fs::symlink_metadata(&p).unwrap().file_type().is_file());
}
#[test]
fn test_write_atomic_replaces_existing_symlink_not_target() {
let dir = tmpdir("s1");
let target = dir.join("victim");
let link = dir.join("link");
fs::write(&target, b"precious").unwrap();
symlink(&target, &link).unwrap();
write_atomic(&link, b"new").unwrap();
assert_eq!(fs::read_to_string(&target).unwrap(), "precious");
assert_eq!(fs::read_to_string(&link).unwrap(), "new");
assert!(fs::symlink_metadata(&link).unwrap().file_type().is_file());
}
#[test]
fn test_read_nofollow_refuses_symlink() {
let dir = tmpdir("r");
let target = dir.join("victim2");
let link = dir.join("link2");
fs::write(&target, b"secret").unwrap();
symlink(&target, &link).unwrap();
assert_eq!(read_nofollow(&target).unwrap(), "secret");
assert!(read_nofollow(&link).is_err());
}
#[test]
fn test_open_append_nofollow_refuses_symlink() {
let dir = tmpdir("a");
let target = dir.join("target3");
let link = dir.join("link3");
fs::write(&target, b"x").unwrap();
symlink(&target, &link).unwrap();
assert!(open_append_nofollow(&target).is_ok());
assert!(open_append_nofollow(&link).is_err());
let _ = fs::remove_file(&target);
let _ = fs::remove_file(&link);
}
#[test]
fn test_write_atomic_refuses_symlinked_parent() {
let dir = tmpdir("parent_symlink");
let elsewhere = tmpdir("parent_dest");
let link = dir.join("coreshift");
symlink(&elsewhere, &link).unwrap();
assert!(write_atomic(link.join("payload.txt"), b"boom").is_err());
assert!(!elsewhere.join("payload.txt").exists());
assert!(!link.join("payload.txt").exists());
}
#[test]
fn test_read_nofollow_refuses_symlinked_parent() {
let dir = tmpdir("read_parent_symlink");
let elsewhere = tmpdir("read_parent_dest");
fs::write(elsewhere.join("conf"), b"injected").unwrap();
let link = dir.join("coreshift");
symlink(&elsewhere, &link).unwrap();
assert!(read_nofollow(link.join("conf")).is_err());
}
#[test]
fn test_open_append_nofollow_refuses_symlinked_parent() {
let dir = tmpdir("append_parent_symlink");
let elsewhere = tmpdir("append_parent_dest");
let link = dir.join("coreshift");
symlink(&elsewhere, &link).unwrap();
assert!(open_append_nofollow(link.join("daemon.log")).is_err());
assert!(!elsewhere.join("daemon.log").exists());
}
#[test]
fn test_ensure_state_dir_refuses_symlink() {
let dir = tmpdir("state_symlink");
let elsewhere = tmpdir("state_dest");
let link = dir.join("state");
symlink(&elsewhere, &link).unwrap();
assert!(ensure_state_dir(&link).is_err());
let real = tmpdir("state_real");
assert!(ensure_state_dir(&real).is_ok());
}
#[test]
fn test_remove_nofollow_removes_entry_not_target() {
let dir = tmpdir("unlink");
let target = dir.join("victim4");
let link = dir.join("link4");
fs::write(&target, b"keep").unwrap();
symlink(&target, &link).unwrap();
remove_nofollow(&link).unwrap();
assert!(!link.exists());
assert_eq!(fs::read_to_string(&target).unwrap(), "keep");
}
#[test]
fn test_write_atomic_requires_parent_to_exist() {
let dir = tmpdir("missing_parent");
let p = dir.join("nope").join("file.txt");
assert!(write_atomic(&p, b"x").is_err());
assert!(!p.exists());
}
#[test]
fn test_ops_refuse_foreign_owned_parent() {
if unsafe { libc::geteuid() } != 0 {
return;
}
let dir = tmpdir("foreign_owner");
let path = dir.join("f");
let owned = cstr(&dir.to_string_lossy()).unwrap();
assert_eq!(unsafe { libc::chown(owned.as_ptr(), 65534, 65534) }, 0);
assert!(write_atomic(&path, b"x").is_err());
assert!(read_nofollow(&path).is_err());
assert!(open_append_nofollow(&path).is_err());
assert!(remove_nofollow(&path).is_err());
assert!(ensure_state_dir(&dir).is_err());
assert!(!path.exists());
}
fn temp_leftovers(dir: &Path, file_name: &str) -> Vec<String> {
let prefix = format!(".{file_name}.");
fs::read_dir(dir)
.map(|rd| {
rd.filter_map(|e| e.ok())
.filter_map(|e| e.file_name().to_str().map(str::to_owned))
.filter(|n| n.starts_with(&prefix))
.collect()
})
.unwrap_or_default()
}
#[test]
fn test_write_atomic_cleans_temp_on_rename_failure() {
let dir = tmpdir("rename_fail");
let dest = dir.join("dest");
fs::create_dir_all(&dest).unwrap();
fs::write(dest.join("keep"), b"x").unwrap();
assert!(write_atomic(&dest, b"boom").is_err());
assert_eq!(fs::read_to_string(dest.join("keep")).unwrap(), "x");
assert!(
temp_leftovers(&dir, "dest").is_empty(),
"temp file must be cleaned up"
);
}
#[test]
fn test_write_atomic_leaves_no_temp_on_success() {
let dir = tmpdir("no_temp_success");
let p = dir.join("out.txt");
write_atomic(&p, b"hello").unwrap();
assert_eq!(fs::read_to_string(&p).unwrap(), "hello");
assert!(
temp_leftovers(&dir, "out.txt").is_empty(),
"no temp left behind"
);
}
#[test]
fn test_write_atomic_retries_when_temp_name_exists() {
let dir = tmpdir("temp_collision");
let p = dir.join("out.txt");
let pid = std::process::id();
let collided = dir.join(format!(".out.txt.{pid}.0"));
fs::write(&collided, b"not mine").unwrap();
write_atomic(&p, b"hello").unwrap();
assert_eq!(fs::read_to_string(&p).unwrap(), "hello");
assert_eq!(fs::read_to_string(&collided).unwrap(), "not mine");
let leftovers = temp_leftovers(&dir, "out.txt");
assert_eq!(
leftovers.len(),
1,
"only the pre-existing colliding temp remains"
);
assert_eq!(leftovers[0], format!(".out.txt.{pid}.0"));
}
#[test]
fn test_ops_refuse_foreign_owned_writable_parent() {
use std::os::unix::fs::MetadataExt;
let euid = unsafe { libc::geteuid() };
if euid == 0 {
return;
}
let tmp = std::env::temp_dir();
let meta = match fs::symlink_metadata(&tmp) {
Ok(m) => m,
Err(_) => return,
};
if meta.uid() == euid {
return; }
if meta.mode() & 0o002 == 0 && meta.mode() & 0o020 == 0 {
return; }
let p = tmp.join(format!(
"coreshift_fs_foreign_{}_{}",
std::process::id(),
"out"
));
let _ = fs::remove_file(&p);
fs::write(&p, b"probe").unwrap();
assert!(read_nofollow(&p).is_err());
assert!(open_append_nofollow(&p).is_err());
assert!(write_atomic(&p, b"boom").is_err());
assert!(remove_nofollow(&p).is_err());
assert!(ensure_state_dir(&tmp).is_err());
assert_eq!(fs::read_to_string(&p).unwrap(), "probe");
let _ = fs::remove_file(&p);
}
#[test]
fn test_ensure_state_dir_creates_fresh_dir() {
let base = tmpdir("fresh_base");
let nested = base.join("a").join("b").join("state");
let fd = ensure_state_dir(&nested).unwrap();
assert!(nested.is_dir());
drop(fd);
assert!(ensure_state_dir(&nested).is_ok());
}
#[test]
fn test_open_append_nofollow_refuses_dangling_symlink() {
let dir = tmpdir("dangling_append");
let missing = dir.join("not_there.txt");
let link = dir.join("linkd");
symlink(&missing, &link).unwrap();
assert!(open_append_nofollow(&link).is_err());
assert!(
!missing.exists(),
"must not create the target through a dangling link"
);
}
#[test]
fn test_read_nofollow_refuses_dangling_symlink() {
let dir = tmpdir("dangling_read");
let missing = dir.join("not_there2.txt");
let link = dir.join("linkd2");
symlink(&missing, &link).unwrap();
assert!(read_nofollow(&link).is_err());
assert!(!missing.exists());
}
#[test]
fn test_ops_refuse_regular_file_parent() {
let dir = tmpdir("regfile_parent");
let f = dir.join("notadir");
fs::write(&f, b"x").unwrap();
assert!(write_atomic(f.join("out"), b"y").is_err());
assert!(read_nofollow(f.join("out")).is_err());
assert!(open_append_nofollow(f.join("out")).is_err());
assert!(remove_nofollow(f.join("out")).is_err());
assert!(ensure_state_dir(&f).is_err());
assert_eq!(fs::read_to_string(&f).unwrap(), "x");
}
}