1use crate::CoreError;
12use std::ffi::CString;
13use std::fs;
14use std::io::{Read, Write};
15use std::os::unix::io::{AsRawFd, FromRawFd, OwnedFd, RawFd};
16use std::path::Path;
17use std::time::UNIX_EPOCH;
18
19#[derive(Clone, Debug, PartialEq, Eq)]
20pub struct PathFingerprint {
21 pub len: u64,
22 pub modified_ns: u128,
23}
24
25pub fn path_fingerprint(path: &Path) -> Result<PathFingerprint, CoreError> {
31 let metadata = std::fs::metadata(path).map_err(|err| {
32 CoreError::sys(err.raw_os_error().unwrap_or(libc::EIO), "path_fingerprint")
33 })?;
34 let modified_ns = metadata
35 .modified()
36 .ok()
37 .and_then(|modified| modified.duration_since(UNIX_EPOCH).ok())
38 .map(|duration| duration.as_nanos())
39 .unwrap_or_default();
40 Ok(PathFingerprint {
41 len: metadata.len(),
42 modified_ns,
43 })
44}
45
46pub fn path_fingerprint_nofollow(path: &Path) -> Result<PathFingerprint, CoreError> {
59 let target = path;
60 let file_name = basename(target)?;
61 let dir = open_parent_nofollow(target)?;
62 let fd = openat_raw(
63 dir.as_raw_fd(),
64 &file_name,
65 libc::O_RDONLY | libc::O_NOFOLLOW | libc::O_CLOEXEC,
66 0,
67 )?;
68 let metadata = unsafe { fs::File::from_raw_fd(fd) }.metadata()?;
70 let modified_ns = metadata
71 .modified()
72 .ok()
73 .and_then(|modified| modified.duration_since(UNIX_EPOCH).ok())
74 .map(|duration| duration.as_nanos())
75 .unwrap_or_default();
76 Ok(PathFingerprint {
77 len: metadata.len(),
78 modified_ns,
79 })
80}
81
82pub fn path_exists(path: &str) -> bool {
89 match std::ffi::CString::new(path) {
90 Ok(c) => unsafe { libc::access(c.as_ptr(), libc::F_OK) == 0 },
91 Err(_) => false,
92 }
93}
94
95pub fn path_lstat_exists(path: &str) -> bool {
99 match std::ffi::CString::new(path) {
100 Ok(c) => unsafe {
101 let mut stat = std::mem::zeroed();
102 libc::lstat(c.as_ptr(), &mut stat) == 0
103 },
104 Err(_) => false,
105 }
106}
107
108pub fn read_to_string(path: &str) -> Result<String, CoreError> {
119 std::fs::read_to_string(path)
120 .map_err(|err| CoreError::sys(err.raw_os_error().unwrap_or(libc::EIO), "read_to_string"))
121}
122
123pub fn readahead(fd: impl AsRawFd, offset: u64, len: usize) -> Result<(), CoreError> {
138 readahead_raw(fd.as_raw_fd(), offset, len)
139}
140
141pub const FADV_NORMAL: i32 = libc::POSIX_FADV_NORMAL;
144pub const FADV_RANDOM: i32 = libc::POSIX_FADV_RANDOM;
145pub const FADV_SEQUENTIAL: i32 = libc::POSIX_FADV_SEQUENTIAL;
146pub const FADV_WILLNEED: i32 = libc::POSIX_FADV_WILLNEED;
147pub const FADV_DONTNEED: i32 = libc::POSIX_FADV_DONTNEED;
148pub const FADV_NOREUSE: i32 = libc::POSIX_FADV_NOREUSE;
149
150pub fn fadvise(fd: impl AsRawFd, offset: u64, len: usize, advice: i32) -> Result<(), CoreError> {
163 let ret = unsafe {
164 libc::posix_fadvise(
165 fd.as_raw_fd(),
166 offset as libc::off_t,
167 len as libc::off_t,
168 advice,
169 )
170 };
171 if ret == 0 {
172 Ok(())
173 } else {
174 Err(CoreError::sys(ret, "posix_fadvise"))
175 }
176}
177
178pub fn mmap_madvise(
189 fd: impl AsRawFd,
190 offset: u64,
191 len: usize,
192 touch: bool,
193) -> Result<(), CoreError> {
194 mmap_madvise_raw(fd.as_raw_fd(), offset, len, touch)
195}
196
197#[cfg(any(target_os = "linux", target_os = "android"))]
198fn mmap_madvise_raw(
199 fd: libc::c_int,
200 offset: u64,
201 len: usize,
202 touch: bool,
203) -> Result<(), CoreError> {
204 if len == 0 {
205 return Ok(());
206 }
207
208 let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
209 if page_size <= 0 {
210 return Err(CoreError::sys(libc::EINVAL, "sysconf(_SC_PAGESIZE)"));
211 }
212 let page_size = page_size as u64;
213 if !offset.is_multiple_of(page_size) || offset > libc::off_t::MAX as u64 {
214 return Err(CoreError::sys(libc::EINVAL, "mmap"));
215 }
216
217 let file_len = {
221 let mut st: libc::stat = unsafe { std::mem::zeroed() };
222 if unsafe { libc::fstat(fd, &mut st) } == -1 {
223 let code = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
224 return Err(CoreError::sys(code, "fstat"));
225 }
226 st.st_size.max(0) as u64
227 };
228 let len = len.min(file_len.saturating_sub(offset) as usize);
229 if len == 0 {
230 return Ok(());
231 }
232
233 let ptr = unsafe {
234 libc::mmap(
235 std::ptr::null_mut(),
236 len,
237 libc::PROT_READ,
238 libc::MAP_PRIVATE,
239 fd,
240 offset as libc::off_t,
241 )
242 };
243 if ptr == libc::MAP_FAILED {
244 let code = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
245 return Err(CoreError::sys(code, "mmap"));
246 }
247
248 let result = if unsafe { libc::madvise(ptr, len, libc::MADV_WILLNEED) } == -1 {
249 let code = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
250 Err(CoreError::sys(code, "madvise"))
251 } else {
252 if touch {
253 let page_size = page_size as usize;
262 let mut pos = 0usize;
263 while pos < len {
264 let mut st: libc::stat = unsafe { std::mem::zeroed() };
265 if unsafe { libc::fstat(fd, &mut st) } == -1 {
266 break;
267 }
268 let file_len = st.st_size.max(0) as u64;
269 let page_start = offset.saturating_add(pos as u64);
270 if page_start >= file_len {
271 break;
272 }
273 unsafe {
274 std::ptr::read_volatile((ptr as *const u8).add(pos));
275 }
276 pos = pos.saturating_add(page_size);
277 }
278 }
279 Ok(())
280 };
281
282 if unsafe { libc::munmap(ptr, len) } == -1 {
283 let code = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
284 return Err(CoreError::sys(code, "munmap"));
285 }
286 result
287}
288
289#[cfg(not(any(target_os = "linux", target_os = "android")))]
290fn mmap_madvise_raw(
291 _fd: libc::c_int,
292 _offset: u64,
293 _len: usize,
294 _touch: bool,
295) -> Result<(), CoreError> {
296 Err(CoreError::sys(libc::ENOSYS, "mmap"))
297}
298
299#[cfg(any(target_os = "linux", target_os = "android"))]
300fn readahead_raw(fd: libc::c_int, offset: u64, len: usize) -> Result<(), CoreError> {
301 if offset > libc::off64_t::MAX as u64 {
302 return Err(CoreError::sys(libc::EINVAL, "readahead"));
303 }
304
305 let count = len as libc::size_t;
306 let offset = offset as libc::off64_t;
307
308 loop {
309 let ret = unsafe { libc::syscall(readahead_syscall_number(), fd, offset, count) };
310 if ret == -1 {
311 let code = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
312 if code == libc::EINTR {
313 continue;
314 }
315 return Err(CoreError::sys(code, "readahead"));
316 }
317 return Ok(());
318 }
319}
320
321#[cfg(not(any(target_os = "linux", target_os = "android")))]
322fn readahead_raw(_fd: libc::c_int, _offset: u64, _len: usize) -> Result<(), CoreError> {
323 Err(CoreError::sys(libc::ENOSYS, "readahead"))
324}
325
326#[cfg(target_os = "linux")]
327#[inline(always)]
328const fn readahead_syscall_number() -> libc::c_long {
329 libc::SYS_readahead
330}
331
332#[cfg(all(target_os = "android", target_arch = "aarch64"))]
333#[inline(always)]
334const fn readahead_syscall_number() -> libc::c_long {
335 213
336}
337
338#[cfg(all(target_os = "android", target_arch = "arm"))]
339#[inline(always)]
340const fn readahead_syscall_number() -> libc::c_long {
341 225
342}
343
344#[cfg(all(target_os = "android", target_arch = "x86_64"))]
345#[inline(always)]
346const fn readahead_syscall_number() -> libc::c_long {
347 187
348}
349
350#[cfg(all(target_os = "android", target_arch = "x86"))]
351#[inline(always)]
352const fn readahead_syscall_number() -> libc::c_long {
353 225
354}
355
356const TEMP_ATTEMPTS: usize = 32;
357
358fn getrandom_bytes(buf: &mut [u8]) -> Result<(), CoreError> {
364 let mut off = 0;
365 while off < buf.len() {
366 let n = unsafe { libc::getrandom(buf[off..].as_mut_ptr() as *mut _, buf.len() - off, 0) };
369 if n < 0 {
370 if std::io::Error::last_os_error().raw_os_error() == Some(libc::EINTR) {
371 continue;
372 }
373 return Err(std::io::Error::last_os_error().into());
374 }
375 off += n as usize;
376 }
377 Ok(())
378}
379
380fn hex(bytes: &[u8]) -> String {
382 let mut out = String::with_capacity(bytes.len() * 2);
383 for b in bytes {
384 out.push_str(&format!("{b:02x}"));
385 }
386 out
387}
388
389fn cstr(s: &str) -> Result<CString, CoreError> {
390 CString::new(s).map_err(|_| CoreError::sys(libc::EINVAL, "path:nul_byte"))
391}
392
393fn openat_raw(dirfd: RawFd, name: &str, flags: i32, mode: u32) -> Result<RawFd, CoreError> {
394 let c = cstr(name)?;
395 let fd = unsafe { libc::openat(dirfd, c.as_ptr(), flags, mode as libc::c_uint) };
398 if fd < 0 {
399 Err(std::io::Error::last_os_error().into())
400 } else {
401 Ok(fd)
402 }
403}
404
405fn openat_dir(dirfd: RawFd, name: &str) -> Result<OwnedFd, CoreError> {
406 let fd = openat_raw(
407 dirfd,
408 name,
409 libc::O_RDONLY | libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_CLOEXEC,
410 0,
411 )?;
412 Ok(unsafe { OwnedFd::from_raw_fd(fd) })
414}
415
416fn walk_dir(path: &Path, create_mode: Option<u32>) -> Result<OwnedFd, CoreError> {
423 let abs = if path.is_absolute() {
424 path.to_path_buf()
425 } else {
426 std::env::current_dir()?.join(path)
427 };
428 let mut dir = openat_dir(libc::AT_FDCWD, "/")?;
429 for comp in abs.components() {
430 use std::path::Component;
431 match comp {
432 Component::RootDir | Component::CurDir => {}
433 Component::Normal(name) => {
434 let name = name
435 .to_str()
436 .ok_or_else(|| CoreError::sys(libc::EINVAL, "path:non_utf8"))?;
437 let next = match openat_dir(dir.as_raw_fd(), name) {
438 Ok(fd) => fd,
439 Err(e) if e.raw_os_error() == Some(libc::ENOENT) && create_mode.is_some() => {
440 let c = cstr(name)?;
441 if unsafe {
445 libc::mkdirat(
446 dir.as_raw_fd(),
447 c.as_ptr(),
448 create_mode.unwrap() as libc::mode_t,
449 )
450 } != 0
451 {
452 if std::io::Error::last_os_error().raw_os_error() == Some(libc::EEXIST)
456 {
457 openat_dir(dir.as_raw_fd(), name)?
458 } else {
459 return Err(std::io::Error::last_os_error().into());
460 }
461 } else {
462 openat_dir(dir.as_raw_fd(), name)?
463 }
464 }
465 Err(e) => return Err(e),
466 };
467 dir = next;
468 }
469 Component::ParentDir => {
470 return Err(CoreError::sys(libc::EINVAL, "path:parent_component"));
471 }
472 Component::Prefix(_) => unreachable!("non-Windows path"),
473 }
474 }
475 Ok(dir)
476}
477
478fn open_dir_nofollow(path: &Path) -> Result<OwnedFd, CoreError> {
481 walk_dir(path, None)
482}
483
484fn fstat(fd: RawFd) -> Result<libc::stat, CoreError> {
485 let mut st: libc::stat = unsafe { std::mem::zeroed() };
486 if unsafe { libc::fstat(fd, &mut st) } != 0 {
488 Err(std::io::Error::last_os_error().into())
489 } else {
490 Ok(st)
491 }
492}
493
494fn unlink_name(dirfd: RawFd, name: &str) -> Result<(), CoreError> {
495 let c = cstr(name)?;
496 if unsafe { libc::unlinkat(dirfd, c.as_ptr(), 0) } != 0 {
499 Err(std::io::Error::last_os_error().into())
500 } else {
501 Ok(())
502 }
503}
504
505fn basename(target: &Path) -> Result<String, CoreError> {
506 target
507 .file_name()
508 .map(|n| n.to_string_lossy().into_owned())
509 .ok_or_else(|| CoreError::sys(libc::EINVAL, "path:no_file_name"))
510}
511
512fn parent_dir(target: &Path) -> &Path {
513 target
514 .parent()
515 .filter(|p| !p.as_os_str().is_empty())
516 .unwrap_or_else(|| Path::new("."))
517}
518
519fn open_parent_nofollow(target: &Path) -> Result<OwnedFd, CoreError> {
527 let dir = open_dir_nofollow(parent_dir(target))?;
528 let st = fstat(dir.as_raw_fd())?;
529 let euid = unsafe { libc::geteuid() };
530 if st.st_uid != euid {
531 return Err(CoreError::sys(libc::EACCES, "parent_dir_owner"));
532 }
533 Ok(dir)
534}
535
536struct TmpGuard {
537 dirfd: RawFd,
538 name: String,
539}
540
541impl Drop for TmpGuard {
542 fn drop(&mut self) {
543 let _ = unlink_name(self.dirfd, &self.name);
544 }
545}
546
547pub fn write_atomic(path: impl AsRef<Path>, content: &[u8]) -> Result<(), CoreError> {
555 let target = path.as_ref();
556 let file_name = basename(target)?;
557 let dir = open_parent_nofollow(target)?;
558 let dirfd = dir.as_raw_fd();
559
560 for attempt in 0..TEMP_ATTEMPTS {
561 let mut rand_bytes = [0u8; 16];
565 getrandom_bytes(&mut rand_bytes)?;
566 let tmp_name = format!(
567 ".{file_name}.{}.{}.{attempt:x}",
568 std::process::id(),
569 hex(&rand_bytes)
570 );
571 match openat_raw(
572 dirfd,
573 &tmp_name,
574 libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL | libc::O_CLOEXEC,
575 0o600,
576 ) {
577 Ok(raw) => {
578 let mut file = unsafe { fs::File::from_raw_fd(raw) };
581 let _guard = TmpGuard {
582 dirfd,
583 name: tmp_name.clone(),
584 };
585 file.write_all(content)?;
586 file.sync_all()?;
587 drop(file);
588 let c_tmp = cstr(&tmp_name)?;
589 let c_final = cstr(&file_name)?;
590 if unsafe { libc::renameat(dirfd, c_tmp.as_ptr(), dirfd, c_final.as_ptr()) } != 0 {
594 return Err(std::io::Error::last_os_error().into());
595 }
596 std::mem::forget(_guard);
598 if unsafe { libc::fsync(dirfd) } != 0 {
603 return Err(std::io::Error::last_os_error().into());
604 }
605 return Ok(());
606 }
607 Err(e) if e.raw_os_error() == Some(libc::EEXIST) => continue,
608 Err(e) => return Err(e),
609 }
610 }
611
612 Err(CoreError::sys(libc::EEXIST, "temp:exhausted"))
613}
614
615pub fn read_nofollow(path: impl AsRef<Path>) -> Result<String, CoreError> {
622 let target = path.as_ref();
623 let file_name = basename(target)?;
624 let dir = open_parent_nofollow(target)?;
625 let fd = openat_raw(
626 dir.as_raw_fd(),
627 &file_name,
628 libc::O_RDONLY | libc::O_NOFOLLOW | libc::O_CLOEXEC,
629 0,
630 )?;
631 let mut file = unsafe { fs::File::from_raw_fd(fd) };
633 let mut content = String::new();
634 file.read_to_string(&mut content)?;
635 Ok(content)
636}
637
638pub fn open_append_nofollow(path: impl AsRef<Path>) -> Result<fs::File, CoreError> {
644 let target = path.as_ref();
645 let file_name = basename(target)?;
646 let dir = open_parent_nofollow(target)?;
647 let fd = openat_raw(
648 dir.as_raw_fd(),
649 &file_name,
650 libc::O_WRONLY | libc::O_CREAT | libc::O_APPEND | libc::O_NOFOLLOW | libc::O_CLOEXEC,
651 0o644,
652 )?;
653 Ok(unsafe { fs::File::from_raw_fd(fd) })
655}
656
657pub fn remove_nofollow(path: impl AsRef<Path>) -> Result<(), CoreError> {
661 let target = path.as_ref();
662 let file_name = basename(target)?;
663 let dir = open_parent_nofollow(target)?;
664 unlink_name(dir.as_raw_fd(), &file_name)
665}
666
667pub fn ensure_state_dir(dir: impl AsRef<Path>) -> Result<OwnedFd, CoreError> {
672 let dir = dir.as_ref();
673 let fd = walk_dir(dir, Some(0o700))?;
677 let st = fstat(fd.as_raw_fd())?;
678 let euid = unsafe { libc::geteuid() };
679 if st.st_uid != euid {
680 return Err(CoreError::sys(libc::EACCES, "state_dir_owner"));
681 }
682 Ok(fd)
683}
684
685#[cfg(test)]
686mod tests {
687 #[cfg(target_os = "linux")]
688 #[test]
689 fn test_readahead_syscall_number_linux_matches_libc() {
690 assert_eq!(super::readahead_syscall_number(), libc::SYS_readahead);
691 }
692
693 #[cfg(target_os = "linux")]
694 #[test]
695 fn test_mmap_madvise_touch_past_eof_does_not_sigbus() {
696 use std::os::unix::io::{AsRawFd, FromRawFd};
697
698 let dir =
702 std::env::temp_dir().join(format!("coreshift_mmap_past_eof_{}", std::process::id()));
703 let _ = std::fs::remove_file(&dir);
704 std::fs::write(&dir, b"x").unwrap();
705
706 let f = std::fs::File::open(&dir).unwrap();
707 let fd = f.as_raw_fd();
708 let dup = unsafe { libc::fcntl(fd, libc::F_DUPFD_CLOEXEC, 0) };
710 assert!(dup >= 0);
711 let owned = unsafe { std::fs::File::from_raw_fd(dup) };
712
713 let page = unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as u64;
714 let result = super::mmap_madvise(owned.as_raw_fd(), 0, (page * 4) as usize, true);
715 drop(owned);
716 let _ = std::fs::remove_file(&dir);
717
718 assert!(result.is_ok(), "touch past EOF must not SIGBUS: {result:?}");
721 }
722
723 #[cfg(all(target_os = "android", target_arch = "aarch64"))]
724 #[test]
725 fn test_readahead_syscall_number_android_aarch64() {
726 assert_eq!(super::readahead_syscall_number(), 213);
727 }
728
729 #[cfg(all(target_os = "android", target_arch = "arm"))]
730 #[test]
731 fn test_readahead_syscall_number_android_arm() {
732 assert_eq!(super::readahead_syscall_number(), 225);
733 }
734
735 #[cfg(all(target_os = "android", target_arch = "x86_64"))]
736 #[test]
737 fn test_readahead_syscall_number_android_x86_64() {
738 assert_eq!(super::readahead_syscall_number(), 187);
739 }
740
741 #[cfg(all(target_os = "android", target_arch = "x86"))]
742 #[test]
743 fn test_readahead_syscall_number_android_x86() {
744 assert_eq!(super::readahead_syscall_number(), 225);
745 }
746}
747
748#[cfg(test)]
749mod safe_fs_tests {
750
751 use super::*;
752 use std::os::unix::fs::symlink;
753 use std::path::PathBuf;
754
755 fn tmpdir(name: &str) -> PathBuf {
756 let d = std::env::temp_dir().join(format!(
757 "coreshift_safe_fs_dir_{}_{name}",
758 std::process::id()
759 ));
760 let _ = fs::remove_dir_all(&d);
761 fs::create_dir_all(&d).unwrap();
762 d
763 }
764
765 #[test]
766 fn test_write_atomic_creates_regular_file() {
767 let dir = tmpdir("w");
768 let p = dir.join("out.txt");
769
770 write_atomic(&p, b"hello").unwrap();
771 assert_eq!(fs::read_to_string(&p).unwrap(), "hello");
772 assert!(fs::symlink_metadata(&p).unwrap().file_type().is_file());
773 }
774
775 #[test]
776 fn test_write_atomic_replaces_existing_symlink_not_target() {
777 let dir = tmpdir("s1");
778 let target = dir.join("victim");
779 let link = dir.join("link");
780
781 fs::write(&target, b"precious").unwrap();
782 symlink(&target, &link).unwrap();
783
784 write_atomic(&link, b"new").unwrap();
787
788 assert_eq!(fs::read_to_string(&target).unwrap(), "precious");
789 assert_eq!(fs::read_to_string(&link).unwrap(), "new");
790 assert!(fs::symlink_metadata(&link).unwrap().file_type().is_file());
791 }
792
793 #[test]
794 fn test_read_nofollow_refuses_symlink() {
795 let dir = tmpdir("r");
796 let target = dir.join("victim2");
797 let link = dir.join("link2");
798
799 fs::write(&target, b"secret").unwrap();
800 symlink(&target, &link).unwrap();
801
802 assert_eq!(read_nofollow(&target).unwrap(), "secret");
803 assert!(read_nofollow(&link).is_err());
804 }
805
806 #[test]
807 fn test_path_fingerprint_nofollow_refuses_symlink() {
808 let dir = tmpdir("fp");
809 let target = dir.join("target_fp");
810 let link = dir.join("link_fp");
811
812 fs::write(&target, b"secret").unwrap();
813 symlink(&target, &link).unwrap();
814
815 assert!(path_fingerprint_nofollow(&target).is_ok());
817 assert!(path_fingerprint_nofollow(&link).is_err());
818 let parent_link = dir.join("plink");
820 symlink(&dir, &parent_link).unwrap();
821 assert!(path_fingerprint_nofollow(&parent_link.join("target_fp")).is_err());
822 }
823
824 #[test]
825 fn test_open_append_nofollow_refuses_symlink() {
826 let dir = tmpdir("a");
827 let target = dir.join("target3");
828 let link = dir.join("link3");
829
830 fs::write(&target, b"x").unwrap();
831 symlink(&target, &link).unwrap();
832
833 assert!(open_append_nofollow(&target).is_ok());
835 assert!(open_append_nofollow(&link).is_err());
837
838 let _ = fs::remove_file(&target);
839 let _ = fs::remove_file(&link);
840 }
841
842 #[test]
843 fn test_write_atomic_refuses_symlinked_parent() {
844 let dir = tmpdir("parent_symlink");
848 let elsewhere = tmpdir("parent_dest");
849 let link = dir.join("coreshift");
850 symlink(&elsewhere, &link).unwrap();
851
852 assert!(write_atomic(link.join("payload.txt"), b"boom").is_err());
853 assert!(!elsewhere.join("payload.txt").exists());
854 assert!(!link.join("payload.txt").exists());
855 }
856
857 #[test]
858 fn test_read_nofollow_refuses_symlinked_parent() {
859 let dir = tmpdir("read_parent_symlink");
860 let elsewhere = tmpdir("read_parent_dest");
861 fs::write(elsewhere.join("conf"), b"injected").unwrap();
862 let link = dir.join("coreshift");
863 symlink(&elsewhere, &link).unwrap();
864
865 assert!(read_nofollow(link.join("conf")).is_err());
866 }
867
868 #[test]
869 fn test_open_append_nofollow_refuses_symlinked_parent() {
870 let dir = tmpdir("append_parent_symlink");
871 let elsewhere = tmpdir("append_parent_dest");
872 let link = dir.join("coreshift");
873 symlink(&elsewhere, &link).unwrap();
874
875 assert!(open_append_nofollow(link.join("daemon.log")).is_err());
876 assert!(!elsewhere.join("daemon.log").exists());
877 }
878
879 #[test]
880 fn test_ensure_state_dir_refuses_symlink() {
881 let dir = tmpdir("state_symlink");
882 let elsewhere = tmpdir("state_dest");
883 let link = dir.join("state");
884 symlink(&elsewhere, &link).unwrap();
885
886 assert!(ensure_state_dir(&link).is_err());
887 let real = tmpdir("state_real");
889 assert!(ensure_state_dir(&real).is_ok());
890 }
891
892 #[test]
893 fn test_remove_nofollow_removes_entry_not_target() {
894 let dir = tmpdir("unlink");
895 let target = dir.join("victim4");
896 let link = dir.join("link4");
897 fs::write(&target, b"keep").unwrap();
898 symlink(&target, &link).unwrap();
899
900 remove_nofollow(&link).unwrap();
901 assert!(!link.exists());
902 assert_eq!(fs::read_to_string(&target).unwrap(), "keep");
903 }
904
905 #[test]
906 fn test_write_atomic_requires_parent_to_exist() {
907 let dir = tmpdir("missing_parent");
908 let p = dir.join("nope").join("file.txt");
909
910 assert!(write_atomic(&p, b"x").is_err());
911 assert!(!p.exists());
912 }
913
914 #[test]
915 fn test_ops_refuse_foreign_owned_parent() {
916 if unsafe { libc::geteuid() } != 0 {
921 return;
922 }
923 let dir = tmpdir("foreign_owner");
924 let path = dir.join("f");
925 let owned = cstr(&dir.to_string_lossy()).unwrap();
926 assert_eq!(unsafe { libc::chown(owned.as_ptr(), 65534, 65534) }, 0);
928
929 assert!(write_atomic(&path, b"x").is_err());
930 assert!(read_nofollow(&path).is_err());
931 assert!(open_append_nofollow(&path).is_err());
932 assert!(remove_nofollow(&path).is_err());
933 assert!(ensure_state_dir(&dir).is_err());
934 assert!(!path.exists());
935 }
936
937 fn temp_leftovers(dir: &Path, file_name: &str) -> Vec<String> {
938 let prefix = format!(".{file_name}.");
939 fs::read_dir(dir)
940 .map(|rd| {
941 rd.filter_map(|e| e.ok())
942 .filter_map(|e| e.file_name().to_str().map(str::to_owned))
943 .filter(|n| n.starts_with(&prefix))
944 .collect()
945 })
946 .unwrap_or_default()
947 }
948
949 #[test]
950 fn test_write_atomic_cleans_temp_on_rename_failure() {
951 let dir = tmpdir("rename_fail");
955 let dest = dir.join("dest");
956 fs::create_dir_all(&dest).unwrap();
957 fs::write(dest.join("keep"), b"x").unwrap();
958
959 assert!(write_atomic(&dest, b"boom").is_err());
960 assert_eq!(fs::read_to_string(dest.join("keep")).unwrap(), "x");
961 assert!(
962 temp_leftovers(&dir, "dest").is_empty(),
963 "temp file must be cleaned up"
964 );
965 }
966
967 #[test]
968 fn test_write_atomic_leaves_no_temp_on_success() {
969 let dir = tmpdir("no_temp_success");
970 let p = dir.join("out.txt");
971
972 write_atomic(&p, b"hello").unwrap();
973 assert_eq!(fs::read_to_string(&p).unwrap(), "hello");
974 assert!(
975 temp_leftovers(&dir, "out.txt").is_empty(),
976 "no temp left behind"
977 );
978 }
979
980 #[test]
981 fn test_write_atomic_retries_when_temp_name_exists() {
982 let dir = tmpdir("temp_collision");
983 let p = dir.join("out.txt");
984 let pid = std::process::id();
985 let collided = dir.join(format!(".out.txt.{pid}.0"));
986 fs::write(&collided, b"not mine").unwrap();
987
988 write_atomic(&p, b"hello").unwrap();
989 assert_eq!(fs::read_to_string(&p).unwrap(), "hello");
990 assert_eq!(fs::read_to_string(&collided).unwrap(), "not mine");
993 let leftovers = temp_leftovers(&dir, "out.txt");
994 assert_eq!(
995 leftovers.len(),
996 1,
997 "only the pre-existing colliding temp remains"
998 );
999 assert_eq!(leftovers[0], format!(".out.txt.{pid}.0"));
1000 }
1001
1002 #[test]
1003 fn test_ops_refuse_foreign_owned_writable_parent() {
1004 use std::os::unix::fs::MetadataExt;
1010 let euid = unsafe { libc::geteuid() };
1011 if euid == 0 {
1012 return;
1013 }
1014 let tmp = std::env::temp_dir();
1015 let meta = match fs::symlink_metadata(&tmp) {
1016 Ok(m) => m,
1017 Err(_) => return,
1018 };
1019 if meta.uid() == euid {
1020 return; }
1022 if meta.mode() & 0o002 == 0 && meta.mode() & 0o020 == 0 {
1023 return; }
1025 let p = tmp.join(format!(
1026 "coreshift_fs_foreign_{}_{}",
1027 std::process::id(),
1028 "out"
1029 ));
1030 let _ = fs::remove_file(&p);
1031 fs::write(&p, b"probe").unwrap();
1032
1033 assert!(read_nofollow(&p).is_err());
1034 assert!(open_append_nofollow(&p).is_err());
1035 assert!(write_atomic(&p, b"boom").is_err());
1036 assert!(remove_nofollow(&p).is_err());
1037 assert!(ensure_state_dir(&tmp).is_err());
1038 assert_eq!(fs::read_to_string(&p).unwrap(), "probe");
1039 let _ = fs::remove_file(&p);
1040 }
1041
1042 #[test]
1043 fn test_ensure_state_dir_creates_fresh_dir() {
1044 let base = tmpdir("fresh_base");
1045 let nested = base.join("a").join("b").join("state");
1046
1047 let fd = ensure_state_dir(&nested).unwrap();
1048 assert!(nested.is_dir());
1049 drop(fd);
1050 assert!(ensure_state_dir(&nested).is_ok());
1051 }
1052
1053 #[test]
1054 fn test_open_append_nofollow_refuses_dangling_symlink() {
1055 let dir = tmpdir("dangling_append");
1056 let missing = dir.join("not_there.txt");
1057 let link = dir.join("linkd");
1058 symlink(&missing, &link).unwrap();
1059
1060 assert!(open_append_nofollow(&link).is_err());
1061 assert!(
1062 !missing.exists(),
1063 "must not create the target through a dangling link"
1064 );
1065 }
1066
1067 #[test]
1068 fn test_read_nofollow_refuses_dangling_symlink() {
1069 let dir = tmpdir("dangling_read");
1070 let missing = dir.join("not_there2.txt");
1071 let link = dir.join("linkd2");
1072 symlink(&missing, &link).unwrap();
1073
1074 assert!(read_nofollow(&link).is_err());
1075 assert!(!missing.exists());
1076 }
1077
1078 #[test]
1079 fn test_ops_refuse_regular_file_parent() {
1080 let dir = tmpdir("regfile_parent");
1083 let f = dir.join("notadir");
1084 fs::write(&f, b"x").unwrap();
1085
1086 assert!(write_atomic(f.join("out"), b"y").is_err());
1087 assert!(read_nofollow(f.join("out")).is_err());
1088 assert!(open_append_nofollow(f.join("out")).is_err());
1089 assert!(remove_nofollow(f.join("out")).is_err());
1090 assert!(ensure_state_dir(&f).is_err());
1091 assert_eq!(fs::read_to_string(&f).unwrap(), "x");
1092 }
1093}