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_exists(path: &str) -> bool {
53 match std::ffi::CString::new(path) {
54 Ok(c) => unsafe { libc::access(c.as_ptr(), libc::F_OK) == 0 },
55 Err(_) => false,
56 }
57}
58
59pub fn path_lstat_exists(path: &str) -> bool {
63 match std::ffi::CString::new(path) {
64 Ok(c) => unsafe {
65 let mut stat = std::mem::zeroed();
66 libc::lstat(c.as_ptr(), &mut stat) == 0
67 },
68 Err(_) => false,
69 }
70}
71
72pub fn read_to_string(path: &str) -> Result<String, CoreError> {
83 std::fs::read_to_string(path)
84 .map_err(|err| CoreError::sys(err.raw_os_error().unwrap_or(libc::EIO), "read_to_string"))
85}
86
87pub fn readahead(fd: impl AsRawFd, offset: u64, len: usize) -> Result<(), CoreError> {
102 readahead_raw(fd.as_raw_fd(), offset, len)
103}
104
105pub const FADV_NORMAL: i32 = libc::POSIX_FADV_NORMAL;
108pub const FADV_RANDOM: i32 = libc::POSIX_FADV_RANDOM;
109pub const FADV_SEQUENTIAL: i32 = libc::POSIX_FADV_SEQUENTIAL;
110pub const FADV_WILLNEED: i32 = libc::POSIX_FADV_WILLNEED;
111pub const FADV_DONTNEED: i32 = libc::POSIX_FADV_DONTNEED;
112pub const FADV_NOREUSE: i32 = libc::POSIX_FADV_NOREUSE;
113
114pub fn fadvise(fd: impl AsRawFd, offset: u64, len: usize, advice: i32) -> Result<(), CoreError> {
127 let ret = unsafe {
128 libc::posix_fadvise(
129 fd.as_raw_fd(),
130 offset as libc::off_t,
131 len as libc::off_t,
132 advice,
133 )
134 };
135 if ret == 0 {
136 Ok(())
137 } else {
138 Err(CoreError::sys(ret, "posix_fadvise"))
139 }
140}
141
142pub fn mmap_madvise(
153 fd: impl AsRawFd,
154 offset: u64,
155 len: usize,
156 touch: bool,
157) -> Result<(), CoreError> {
158 mmap_madvise_raw(fd.as_raw_fd(), offset, len, touch)
159}
160
161#[cfg(any(target_os = "linux", target_os = "android"))]
162fn mmap_madvise_raw(
163 fd: libc::c_int,
164 offset: u64,
165 len: usize,
166 touch: bool,
167) -> Result<(), CoreError> {
168 if len == 0 {
169 return Ok(());
170 }
171
172 let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
173 if page_size <= 0 {
174 return Err(CoreError::sys(libc::EINVAL, "sysconf(_SC_PAGESIZE)"));
175 }
176 let page_size = page_size as u64;
177 if offset % page_size != 0 || offset > libc::off_t::MAX as u64 {
178 return Err(CoreError::sys(libc::EINVAL, "mmap"));
179 }
180
181 let ptr = unsafe {
182 libc::mmap(
183 std::ptr::null_mut(),
184 len,
185 libc::PROT_READ,
186 libc::MAP_PRIVATE,
187 fd,
188 offset as libc::off_t,
189 )
190 };
191 if ptr == libc::MAP_FAILED {
192 let code = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
193 return Err(CoreError::sys(code, "mmap"));
194 }
195
196 let result = if unsafe { libc::madvise(ptr, len, libc::MADV_WILLNEED) } == -1 {
197 let code = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
198 Err(CoreError::sys(code, "madvise"))
199 } else {
200 if touch {
201 let mut pos = 0usize;
202 let page_size = page_size as usize;
203 while pos < len {
204 unsafe {
205 std::ptr::read_volatile((ptr as *const u8).add(pos));
206 }
207 pos = pos.saturating_add(page_size);
208 }
209 }
210 Ok(())
211 };
212
213 if unsafe { libc::munmap(ptr, len) } == -1 {
214 let code = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
215 return Err(CoreError::sys(code, "munmap"));
216 }
217 result
218}
219
220#[cfg(not(any(target_os = "linux", target_os = "android")))]
221fn mmap_madvise_raw(
222 _fd: libc::c_int,
223 _offset: u64,
224 _len: usize,
225 _touch: bool,
226) -> Result<(), CoreError> {
227 Err(CoreError::sys(libc::ENOSYS, "mmap"))
228}
229
230#[cfg(any(target_os = "linux", target_os = "android"))]
231fn readahead_raw(fd: libc::c_int, offset: u64, len: usize) -> Result<(), CoreError> {
232 if offset > libc::off64_t::MAX as u64 {
233 return Err(CoreError::sys(libc::EINVAL, "readahead"));
234 }
235
236 let count = len as libc::size_t;
237 let offset = offset as libc::off64_t;
238
239 loop {
240 let ret = unsafe { libc::syscall(readahead_syscall_number(), fd, offset, count) };
241 if ret == -1 {
242 let code = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
243 if code == libc::EINTR {
244 continue;
245 }
246 return Err(CoreError::sys(code, "readahead"));
247 }
248 return Ok(());
249 }
250}
251
252#[cfg(not(any(target_os = "linux", target_os = "android")))]
253fn readahead_raw(_fd: libc::c_int, _offset: u64, _len: usize) -> Result<(), CoreError> {
254 Err(CoreError::sys(libc::ENOSYS, "readahead"))
255}
256
257#[cfg(target_os = "linux")]
258#[inline(always)]
259const fn readahead_syscall_number() -> libc::c_long {
260 libc::SYS_readahead
261}
262
263#[cfg(all(target_os = "android", target_arch = "aarch64"))]
264#[inline(always)]
265const fn readahead_syscall_number() -> libc::c_long {
266 213
267}
268
269#[cfg(all(target_os = "android", target_arch = "arm"))]
270#[inline(always)]
271const fn readahead_syscall_number() -> libc::c_long {
272 225
273}
274
275#[cfg(all(target_os = "android", target_arch = "x86_64"))]
276#[inline(always)]
277const fn readahead_syscall_number() -> libc::c_long {
278 187
279}
280
281#[cfg(all(target_os = "android", target_arch = "x86"))]
282#[inline(always)]
283const fn readahead_syscall_number() -> libc::c_long {
284 225
285}
286
287const TEMP_ATTEMPTS: usize = 32;
288
289fn cstr(s: &str) -> Result<CString, CoreError> {
290 CString::new(s).map_err(|_| CoreError::sys(libc::EINVAL, "path:nul_byte"))
291}
292
293fn openat_raw(dirfd: RawFd, name: &str, flags: i32, mode: u32) -> Result<RawFd, CoreError> {
294 let c = cstr(name)?;
295 let fd = unsafe { libc::openat(dirfd, c.as_ptr(), flags, mode as libc::c_uint) };
298 if fd < 0 {
299 Err(std::io::Error::last_os_error().into())
300 } else {
301 Ok(fd)
302 }
303}
304
305fn openat_dir(dirfd: RawFd, name: &str) -> Result<OwnedFd, CoreError> {
306 let fd = openat_raw(
307 dirfd,
308 name,
309 libc::O_RDONLY | libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_CLOEXEC,
310 0,
311 )?;
312 Ok(unsafe { OwnedFd::from_raw_fd(fd) })
314}
315
316fn walk_dir(path: &Path, create_mode: Option<u32>) -> Result<OwnedFd, CoreError> {
323 let abs = if path.is_absolute() {
324 path.to_path_buf()
325 } else {
326 std::env::current_dir()?.join(path)
327 };
328 let mut dir = openat_dir(libc::AT_FDCWD, "/")?;
329 for comp in abs.components() {
330 use std::path::Component;
331 match comp {
332 Component::RootDir | Component::CurDir => {}
333 Component::Normal(name) => {
334 let name = name
335 .to_str()
336 .ok_or_else(|| CoreError::sys(libc::EINVAL, "path:non_utf8"))?;
337 let next = match openat_dir(dir.as_raw_fd(), name) {
338 Ok(fd) => fd,
339 Err(e) if e.raw_os_error() == Some(libc::ENOENT) && create_mode.is_some() => {
340 let c = cstr(name)?;
341 if unsafe {
345 libc::mkdirat(
346 dir.as_raw_fd(),
347 c.as_ptr(),
348 create_mode.unwrap() as libc::mode_t,
349 )
350 } != 0
351 {
352 if std::io::Error::last_os_error().raw_os_error() == Some(libc::EEXIST)
356 {
357 openat_dir(dir.as_raw_fd(), name)?
358 } else {
359 return Err(std::io::Error::last_os_error().into());
360 }
361 } else {
362 openat_dir(dir.as_raw_fd(), name)?
363 }
364 }
365 Err(e) => return Err(e),
366 };
367 dir = next;
368 }
369 Component::ParentDir => {
370 return Err(CoreError::sys(libc::EINVAL, "path:parent_component"));
371 }
372 Component::Prefix(_) => unreachable!("non-Windows path"),
373 }
374 }
375 Ok(dir)
376}
377
378fn open_dir_nofollow(path: &Path) -> Result<OwnedFd, CoreError> {
381 walk_dir(path, None)
382}
383
384fn fstat(fd: RawFd) -> Result<libc::stat, CoreError> {
385 let mut st: libc::stat = unsafe { std::mem::zeroed() };
386 if unsafe { libc::fstat(fd, &mut st) } != 0 {
388 Err(std::io::Error::last_os_error().into())
389 } else {
390 Ok(st)
391 }
392}
393
394fn unlink_name(dirfd: RawFd, name: &str) -> Result<(), CoreError> {
395 let c = cstr(name)?;
396 if unsafe { libc::unlinkat(dirfd, c.as_ptr(), 0) } != 0 {
399 Err(std::io::Error::last_os_error().into())
400 } else {
401 Ok(())
402 }
403}
404
405fn basename(target: &Path) -> Result<String, CoreError> {
406 target
407 .file_name()
408 .map(|n| n.to_string_lossy().into_owned())
409 .ok_or_else(|| CoreError::sys(libc::EINVAL, "path:no_file_name"))
410}
411
412fn parent_dir(target: &Path) -> &Path {
413 target
414 .parent()
415 .filter(|p| !p.as_os_str().is_empty())
416 .unwrap_or_else(|| Path::new("."))
417}
418
419fn open_parent_nofollow(target: &Path) -> Result<OwnedFd, CoreError> {
427 let dir = open_dir_nofollow(parent_dir(target))?;
428 let st = fstat(dir.as_raw_fd())?;
429 let euid = unsafe { libc::geteuid() };
430 if st.st_uid != euid {
431 return Err(CoreError::sys(libc::EACCES, "parent_dir_owner"));
432 }
433 Ok(dir)
434}
435
436struct TmpGuard {
437 dirfd: RawFd,
438 name: String,
439}
440
441impl Drop for TmpGuard {
442 fn drop(&mut self) {
443 let _ = unlink_name(self.dirfd, &self.name);
444 }
445}
446
447pub fn write_atomic(path: impl AsRef<Path>, content: &[u8]) -> Result<(), CoreError> {
455 let target = path.as_ref();
456 let file_name = basename(target)?;
457 let dir = open_parent_nofollow(target)?;
458 let dirfd = dir.as_raw_fd();
459
460 for attempt in 0..TEMP_ATTEMPTS {
461 let tmp_name = format!(".{file_name}.{}.{attempt}", std::process::id());
462 match openat_raw(
463 dirfd,
464 &tmp_name,
465 libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL | libc::O_CLOEXEC,
466 0o600,
467 ) {
468 Ok(raw) => {
469 let mut file = unsafe { fs::File::from_raw_fd(raw) };
472 let _guard = TmpGuard {
473 dirfd,
474 name: tmp_name.clone(),
475 };
476 file.write_all(content)?;
477 file.sync_all()?;
478 drop(file);
479 let c_tmp = cstr(&tmp_name)?;
480 let c_final = cstr(&file_name)?;
481 if unsafe { libc::renameat(dirfd, c_tmp.as_ptr(), dirfd, c_final.as_ptr()) } != 0 {
485 return Err(std::io::Error::last_os_error().into());
486 }
487 std::mem::forget(_guard);
489 return Ok(());
490 }
491 Err(e) if e.raw_os_error() == Some(libc::EEXIST) => continue,
492 Err(e) => return Err(e),
493 }
494 }
495
496 Err(CoreError::sys(libc::EEXIST, "temp:exhausted"))
497}
498
499pub fn read_nofollow(path: impl AsRef<Path>) -> Result<String, CoreError> {
506 let target = path.as_ref();
507 let file_name = basename(target)?;
508 let dir = open_parent_nofollow(target)?;
509 let fd = openat_raw(
510 dir.as_raw_fd(),
511 &file_name,
512 libc::O_RDONLY | libc::O_NOFOLLOW | libc::O_CLOEXEC,
513 0,
514 )?;
515 let mut file = unsafe { fs::File::from_raw_fd(fd) };
517 let mut content = String::new();
518 file.read_to_string(&mut content)?;
519 Ok(content)
520}
521
522pub fn open_append_nofollow(path: impl AsRef<Path>) -> Result<fs::File, CoreError> {
528 let target = path.as_ref();
529 let file_name = basename(target)?;
530 let dir = open_parent_nofollow(target)?;
531 let fd = openat_raw(
532 dir.as_raw_fd(),
533 &file_name,
534 libc::O_WRONLY | libc::O_CREAT | libc::O_APPEND | libc::O_NOFOLLOW | libc::O_CLOEXEC,
535 0o644,
536 )?;
537 Ok(unsafe { fs::File::from_raw_fd(fd) })
539}
540
541pub fn remove_nofollow(path: impl AsRef<Path>) -> Result<(), CoreError> {
545 let target = path.as_ref();
546 let file_name = basename(target)?;
547 let dir = open_parent_nofollow(target)?;
548 unlink_name(dir.as_raw_fd(), &file_name)
549}
550
551pub fn ensure_state_dir(dir: impl AsRef<Path>) -> Result<OwnedFd, CoreError> {
556 let dir = dir.as_ref();
557 let fd = walk_dir(dir, Some(0o700))?;
561 let st = fstat(fd.as_raw_fd())?;
562 let euid = unsafe { libc::geteuid() };
563 if st.st_uid != euid {
564 return Err(CoreError::sys(libc::EACCES, "state_dir_owner"));
565 }
566 Ok(fd)
567}
568
569#[cfg(test)]
570mod tests {
571 #[cfg(target_os = "linux")]
572 #[test]
573 fn test_readahead_syscall_number_linux_matches_libc() {
574 assert_eq!(super::readahead_syscall_number(), libc::SYS_readahead);
575 }
576
577 #[cfg(all(target_os = "android", target_arch = "aarch64"))]
578 #[test]
579 fn test_readahead_syscall_number_android_aarch64() {
580 assert_eq!(super::readahead_syscall_number(), 213);
581 }
582
583 #[cfg(all(target_os = "android", target_arch = "arm"))]
584 #[test]
585 fn test_readahead_syscall_number_android_arm() {
586 assert_eq!(super::readahead_syscall_number(), 225);
587 }
588
589 #[cfg(all(target_os = "android", target_arch = "x86_64"))]
590 #[test]
591 fn test_readahead_syscall_number_android_x86_64() {
592 assert_eq!(super::readahead_syscall_number(), 187);
593 }
594
595 #[cfg(all(target_os = "android", target_arch = "x86"))]
596 #[test]
597 fn test_readahead_syscall_number_android_x86() {
598 assert_eq!(super::readahead_syscall_number(), 225);
599 }
600}
601
602#[cfg(test)]
603mod safe_fs_tests {
604
605 use super::*;
606 use std::os::unix::fs::symlink;
607 use std::path::PathBuf;
608
609 fn tmpdir(name: &str) -> PathBuf {
610 let d = std::env::temp_dir().join(format!(
611 "coreshift_safe_fs_dir_{}_{name}",
612 std::process::id()
613 ));
614 let _ = fs::remove_dir_all(&d);
615 fs::create_dir_all(&d).unwrap();
616 d
617 }
618
619 #[test]
620 fn test_write_atomic_creates_regular_file() {
621 let dir = tmpdir("w");
622 let p = dir.join("out.txt");
623
624 write_atomic(&p, b"hello").unwrap();
625 assert_eq!(fs::read_to_string(&p).unwrap(), "hello");
626 assert!(fs::symlink_metadata(&p).unwrap().file_type().is_file());
627 }
628
629 #[test]
630 fn test_write_atomic_replaces_existing_symlink_not_target() {
631 let dir = tmpdir("s1");
632 let target = dir.join("victim");
633 let link = dir.join("link");
634
635 fs::write(&target, b"precious").unwrap();
636 symlink(&target, &link).unwrap();
637
638 write_atomic(&link, b"new").unwrap();
641
642 assert_eq!(fs::read_to_string(&target).unwrap(), "precious");
643 assert_eq!(fs::read_to_string(&link).unwrap(), "new");
644 assert!(fs::symlink_metadata(&link).unwrap().file_type().is_file());
645 }
646
647 #[test]
648 fn test_read_nofollow_refuses_symlink() {
649 let dir = tmpdir("r");
650 let target = dir.join("victim2");
651 let link = dir.join("link2");
652
653 fs::write(&target, b"secret").unwrap();
654 symlink(&target, &link).unwrap();
655
656 assert_eq!(read_nofollow(&target).unwrap(), "secret");
657 assert!(read_nofollow(&link).is_err());
658 }
659
660 #[test]
661 fn test_open_append_nofollow_refuses_symlink() {
662 let dir = tmpdir("a");
663 let target = dir.join("target3");
664 let link = dir.join("link3");
665
666 fs::write(&target, b"x").unwrap();
667 symlink(&target, &link).unwrap();
668
669 assert!(open_append_nofollow(&target).is_ok());
671 assert!(open_append_nofollow(&link).is_err());
673
674 let _ = fs::remove_file(&target);
675 let _ = fs::remove_file(&link);
676 }
677
678 #[test]
679 fn test_write_atomic_refuses_symlinked_parent() {
680 let dir = tmpdir("parent_symlink");
684 let elsewhere = tmpdir("parent_dest");
685 let link = dir.join("coreshift");
686 symlink(&elsewhere, &link).unwrap();
687
688 assert!(write_atomic(link.join("payload.txt"), b"boom").is_err());
689 assert!(!elsewhere.join("payload.txt").exists());
690 assert!(!link.join("payload.txt").exists());
691 }
692
693 #[test]
694 fn test_read_nofollow_refuses_symlinked_parent() {
695 let dir = tmpdir("read_parent_symlink");
696 let elsewhere = tmpdir("read_parent_dest");
697 fs::write(elsewhere.join("conf"), b"injected").unwrap();
698 let link = dir.join("coreshift");
699 symlink(&elsewhere, &link).unwrap();
700
701 assert!(read_nofollow(link.join("conf")).is_err());
702 }
703
704 #[test]
705 fn test_open_append_nofollow_refuses_symlinked_parent() {
706 let dir = tmpdir("append_parent_symlink");
707 let elsewhere = tmpdir("append_parent_dest");
708 let link = dir.join("coreshift");
709 symlink(&elsewhere, &link).unwrap();
710
711 assert!(open_append_nofollow(link.join("daemon.log")).is_err());
712 assert!(!elsewhere.join("daemon.log").exists());
713 }
714
715 #[test]
716 fn test_ensure_state_dir_refuses_symlink() {
717 let dir = tmpdir("state_symlink");
718 let elsewhere = tmpdir("state_dest");
719 let link = dir.join("state");
720 symlink(&elsewhere, &link).unwrap();
721
722 assert!(ensure_state_dir(&link).is_err());
723 let real = tmpdir("state_real");
725 assert!(ensure_state_dir(&real).is_ok());
726 }
727
728 #[test]
729 fn test_remove_nofollow_removes_entry_not_target() {
730 let dir = tmpdir("unlink");
731 let target = dir.join("victim4");
732 let link = dir.join("link4");
733 fs::write(&target, b"keep").unwrap();
734 symlink(&target, &link).unwrap();
735
736 remove_nofollow(&link).unwrap();
737 assert!(!link.exists());
738 assert_eq!(fs::read_to_string(&target).unwrap(), "keep");
739 }
740
741 #[test]
742 fn test_write_atomic_requires_parent_to_exist() {
743 let dir = tmpdir("missing_parent");
744 let p = dir.join("nope").join("file.txt");
745
746 assert!(write_atomic(&p, b"x").is_err());
747 assert!(!p.exists());
748 }
749
750 #[test]
751 fn test_ops_refuse_foreign_owned_parent() {
752 if unsafe { libc::geteuid() } != 0 {
757 return;
758 }
759 let dir = tmpdir("foreign_owner");
760 let path = dir.join("f");
761 let owned = cstr(&dir.to_string_lossy()).unwrap();
762 assert_eq!(unsafe { libc::chown(owned.as_ptr(), 65534, 65534) }, 0);
764
765 assert!(write_atomic(&path, b"x").is_err());
766 assert!(read_nofollow(&path).is_err());
767 assert!(open_append_nofollow(&path).is_err());
768 assert!(remove_nofollow(&path).is_err());
769 assert!(ensure_state_dir(&dir).is_err());
770 assert!(!path.exists());
771 }
772
773 fn temp_leftovers(dir: &Path, file_name: &str) -> Vec<String> {
774 let prefix = format!(".{file_name}.");
775 fs::read_dir(dir)
776 .map(|rd| {
777 rd.filter_map(|e| e.ok())
778 .filter_map(|e| e.file_name().to_str().map(str::to_owned))
779 .filter(|n| n.starts_with(&prefix))
780 .collect()
781 })
782 .unwrap_or_default()
783 }
784
785 #[test]
786 fn test_write_atomic_cleans_temp_on_rename_failure() {
787 let dir = tmpdir("rename_fail");
791 let dest = dir.join("dest");
792 fs::create_dir_all(&dest).unwrap();
793 fs::write(dest.join("keep"), b"x").unwrap();
794
795 assert!(write_atomic(&dest, b"boom").is_err());
796 assert_eq!(fs::read_to_string(dest.join("keep")).unwrap(), "x");
797 assert!(
798 temp_leftovers(&dir, "dest").is_empty(),
799 "temp file must be cleaned up"
800 );
801 }
802
803 #[test]
804 fn test_write_atomic_leaves_no_temp_on_success() {
805 let dir = tmpdir("no_temp_success");
806 let p = dir.join("out.txt");
807
808 write_atomic(&p, b"hello").unwrap();
809 assert_eq!(fs::read_to_string(&p).unwrap(), "hello");
810 assert!(
811 temp_leftovers(&dir, "out.txt").is_empty(),
812 "no temp left behind"
813 );
814 }
815
816 #[test]
817 fn test_write_atomic_retries_when_temp_name_exists() {
818 let dir = tmpdir("temp_collision");
819 let p = dir.join("out.txt");
820 let pid = std::process::id();
821 let collided = dir.join(format!(".out.txt.{pid}.0"));
822 fs::write(&collided, b"not mine").unwrap();
823
824 write_atomic(&p, b"hello").unwrap();
825 assert_eq!(fs::read_to_string(&p).unwrap(), "hello");
826 assert_eq!(fs::read_to_string(&collided).unwrap(), "not mine");
829 let leftovers = temp_leftovers(&dir, "out.txt");
830 assert_eq!(
831 leftovers.len(),
832 1,
833 "only the pre-existing colliding temp remains"
834 );
835 assert_eq!(leftovers[0], format!(".out.txt.{pid}.0"));
836 }
837
838 #[test]
839 fn test_ops_refuse_foreign_owned_writable_parent() {
840 use std::os::unix::fs::MetadataExt;
846 let euid = unsafe { libc::geteuid() };
847 if euid == 0 {
848 return;
849 }
850 let tmp = std::env::temp_dir();
851 let meta = match fs::symlink_metadata(&tmp) {
852 Ok(m) => m,
853 Err(_) => return,
854 };
855 if meta.uid() == euid {
856 return; }
858 if meta.mode() & 0o002 == 0 && meta.mode() & 0o020 == 0 {
859 return; }
861 let p = tmp.join(format!(
862 "coreshift_fs_foreign_{}_{}",
863 std::process::id(),
864 "out"
865 ));
866 let _ = fs::remove_file(&p);
867 fs::write(&p, b"probe").unwrap();
868
869 assert!(read_nofollow(&p).is_err());
870 assert!(open_append_nofollow(&p).is_err());
871 assert!(write_atomic(&p, b"boom").is_err());
872 assert!(remove_nofollow(&p).is_err());
873 assert!(ensure_state_dir(&tmp).is_err());
874 assert_eq!(fs::read_to_string(&p).unwrap(), "probe");
875 let _ = fs::remove_file(&p);
876 }
877
878 #[test]
879 fn test_ensure_state_dir_creates_fresh_dir() {
880 let base = tmpdir("fresh_base");
881 let nested = base.join("a").join("b").join("state");
882
883 let fd = ensure_state_dir(&nested).unwrap();
884 assert!(nested.is_dir());
885 drop(fd);
886 assert!(ensure_state_dir(&nested).is_ok());
887 }
888
889 #[test]
890 fn test_open_append_nofollow_refuses_dangling_symlink() {
891 let dir = tmpdir("dangling_append");
892 let missing = dir.join("not_there.txt");
893 let link = dir.join("linkd");
894 symlink(&missing, &link).unwrap();
895
896 assert!(open_append_nofollow(&link).is_err());
897 assert!(
898 !missing.exists(),
899 "must not create the target through a dangling link"
900 );
901 }
902
903 #[test]
904 fn test_read_nofollow_refuses_dangling_symlink() {
905 let dir = tmpdir("dangling_read");
906 let missing = dir.join("not_there2.txt");
907 let link = dir.join("linkd2");
908 symlink(&missing, &link).unwrap();
909
910 assert!(read_nofollow(&link).is_err());
911 assert!(!missing.exists());
912 }
913
914 #[test]
915 fn test_ops_refuse_regular_file_parent() {
916 let dir = tmpdir("regfile_parent");
919 let f = dir.join("notadir");
920 fs::write(&f, b"x").unwrap();
921
922 assert!(write_atomic(f.join("out"), b"y").is_err());
923 assert!(read_nofollow(f.join("out")).is_err());
924 assert!(open_append_nofollow(f.join("out")).is_err());
925 assert!(remove_nofollow(f.join("out")).is_err());
926 assert!(ensure_state_dir(&f).is_err());
927 assert_eq!(fs::read_to_string(&f).unwrap(), "x");
928 }
929}