1use std::path::{Path, PathBuf};
7use std::os::unix::io::{AsRawFd, RawFd};
8use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
9use std::sync::OnceLock;
10use std::ffi::CString;
11use uuid::Uuid;
12use libc;
13use nix::sys::statfs;
14use tracing::debug;
15
16use std::os::unix::fs::MetadataExt;
17use std::sync::Arc;
18use dashmap::DashMap;
19use lazy_static::lazy_static;
20
21use super::container::{ContainerInfo, DmStackInfo, detect_container, probe_dm_stack};
22
23pub(crate) const NFS_SUPER_MAGIC: u64 = 0x6969;
24pub(crate) const BTRFS_SUPER_MAGIC: u64 = 0x9123683E;
25pub(crate) const TMPFS_SUPER_MAGIC: u64 = 0x01021994;
26pub(crate) const RAMFS_SUPER_MAGIC: u64 = 0x09041934;
27pub(crate) const HUGETLBFS_MAGIC: u64 = 0x958458f6_u64;
28pub(crate) const OVERLAYFS_MAGIC: u64 = 0x794c7630;
29pub(crate) const FS_IOC_GETFLAGS: u64 = 0x80086601;
30pub(crate) const FS_IOC_SETFLAGS: u64 = 0x40086602;
31#[allow(dead_code)]
32pub(crate) const FS_IMMUTABLE_FL: u32 = 0x00000010;
33#[allow(dead_code)]
34pub(crate) const FS_APPEND_FL: u32 = 0x00000020;
35pub(crate) const FS_NOCOW_FL: u32 = 0x00800000;
36const F2FS_SUPER_MAGIC: u64 = 0xF2F52010;
37const FICLONE: u64 = crate::constants::FICLONE_IOCTL;
38const STATX_WRITE_ATOMIC: u32 = 0x00010000;
39const STATX_DIOALIGN: u32 = 0x00002000;
40const BTRFS_IOC_FS_INFO: u64 = 0x8400941F;
41
42lazy_static! {
43 static ref GLOBAL_CAPS_CACHE: DashMap<PathBuf, Arc<Capabilities>> = DashMap::new();
44}
45
46#[repr(C)]
47struct StatxAtomic {
48 stx_mask: u32,
49 stx_blksize: u32,
50 stx_attributes: u64,
51 stx_nlink: u32,
52 stx_uid: u32,
53 stx_gid: u32,
54 stx_mode: u16,
55 __spare0: [u16; 1],
56 stx_ino: u64,
57 stx_size: u64,
58 stx_blocks: u64,
59 stx_attributes_mask: u64,
60 stx_atime: libc::statx_timestamp,
61 stx_btime: libc::statx_timestamp,
62 stx_ctime: libc::statx_timestamp,
63 stx_mtime: libc::statx_timestamp,
64 stx_rdev_major: u32,
65 stx_rdev_minor: u32,
66 stx_dev_major: u32,
67 stx_dev_minor: u32,
68 stx_mnt_id: u64,
69 stx_dio_mem_align: u32,
70 stx_dio_offset_align: u32,
71 stx_atomic_write_unit_min: u32,
72 stx_atomic_write_unit_max: u32,
73 stx_atomic_write_segments_max: u32,
74 __spare1: [u64; 9],
75}
76
77#[repr(C)]
78struct BtrfsIoctlFsInfoArgs {
79 max_id: u64,
80 num_devices: u64,
81 fsid: [u8; 16],
82 nodesize: u32,
83 sectorsize: u32,
84 clone_alignment: u32,
85 reserved32: u32,
86 reserved: [u64; 122],
87}
88
89pub struct Capabilities {
91 pub atomic_writes: AtomicBool,
93 pub atomic_min_bytes: AtomicU32,
95 pub atomic_max_bytes: AtomicU32,
97 pub uncached_io: AtomicBool,
99 pub exchange_range: AtomicBool,
101 pub reflink: AtomicBool,
103 pub seek_hole: AtomicBool,
105 pub btrfs_subvol: AtomicBool,
107 pub btrfs_quotas: AtomicBool,
109 pub f2fs_atomic_legacy: AtomicBool,
111 pub is_nfs: AtomicBool,
113 pub dio_mem_align: AtomicU32,
115 pub dio_offset_align: AtomicU32,
117 pub dm_stack: Option<DmStackInfo>,
119 pub container: Option<ContainerInfo>,
121}
122
123impl Default for Capabilities {
124 fn default() -> Self {
125 Self {
126 atomic_writes: AtomicBool::new(false),
127 atomic_min_bytes: AtomicU32::new(0),
128 atomic_max_bytes: AtomicU32::new(0),
129 uncached_io: AtomicBool::new(false),
130 exchange_range: AtomicBool::new(true),
131 reflink: AtomicBool::new(false),
132 seek_hole: AtomicBool::new(false),
133 btrfs_subvol: AtomicBool::new(false),
134 btrfs_quotas: AtomicBool::new(false),
135 f2fs_atomic_legacy: AtomicBool::new(false),
136 is_nfs: AtomicBool::new(false),
137 dio_mem_align: AtomicU32::new(0),
138 dio_offset_align: AtomicU32::new(0),
139 dm_stack: None,
140 container: None,
141 }
142 }
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146pub(crate) enum CopyStrategy {
147 Reflink,
148 StandardCopy,
149}
150
151pub(crate) fn probe_reflink_support(target_root: &Path) -> bool {
152 let uuid = Uuid::new_v4();
153 let src_path = target_root.join(format!(".foxing_probe_src_{}", uuid));
154 let dst_path = target_root.join(format!(".foxing_probe_dst_{}", uuid));
155
156 let _guard = super::CleanupGuard::new(vec![src_path.clone(), dst_path.clone()]);
157
158 let mut src_file = match std::fs::File::create(&src_path) {
159 Ok(f) => f,
160 Err(_) => return false,
161 };
162 use std::io::Write;
163 let buf = [0xAAu8; 4096];
164 if src_file.write_all(&buf).is_err() { return false; }
165 if src_file.sync_all().is_err() { return false; }
166
167 let dst_file = match std::fs::File::create(&dst_path) {
168 Ok(f) => f,
169 Err(_) => return false,
170 };
171
172 let src_fd = src_file.as_raw_fd();
173 let dst_fd = dst_file.as_raw_fd();
174
175 let ret = unsafe { libc::ioctl(dst_fd, FICLONE, src_fd) };
177
178 let supported = if ret == 0 {
179 if let Ok(meta) = dst_file.metadata() {
180 meta.len() == 4096
181 } else {
182 false
183 }
184 } else {
185 false
186 };
187
188 drop(src_file);
189 drop(dst_file);
190
191 supported
192}
193
194fn has_nocow_flag(path: &Path) -> bool {
195 if let Ok(f) = std::fs::File::open(path) {
196 let fd = f.as_raw_fd();
197 let mut flags: u32 = 0;
198 let ret = unsafe { libc::ioctl(fd, FS_IOC_GETFLAGS, &mut flags) };
200 if ret == 0 {
201 return (flags & FS_NOCOW_FL) != 0;
202 }
203 }
204 false
205}
206
207pub(crate) fn determine_copy_strategy(
208 src_path: &Path,
209 dst_path: &Path,
210 src_meta: &std::fs::Metadata,
211 caps: &Capabilities
212) -> CopyStrategy {
213 if !caps.reflink.load(Ordering::Relaxed) {
214 return CopyStrategy::StandardCopy;
215 }
216
217 let dst_dev = if let Ok(m) = std::fs::metadata(dst_path) {
218 m.dev()
219 } else if let Some(parent) = dst_path.parent() {
220 if let Ok(m) = std::fs::metadata(parent) {
221 m.dev()
222 } else {
223 return CopyStrategy::StandardCopy;
224 }
225 } else {
226 return CopyStrategy::StandardCopy;
227 };
228
229 if src_meta.dev() != dst_dev {
230 return CopyStrategy::StandardCopy;
231 }
232
233 if has_nocow_flag(src_path) {
234 debug!("Strategy: Skipping reflink for NOCOW file: {:?}", src_path);
235 return CopyStrategy::StandardCopy;
236 }
237
238 if src_meta.len() < 4096 {
239 return CopyStrategy::StandardCopy;
240 }
241
242 CopyStrategy::Reflink
243}
244
245pub fn probe_capabilities(path: &Path) -> Arc<Capabilities> {
247 let cache_key = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
248 if let Some(cached) = GLOBAL_CAPS_CACHE.get(&cache_key) {
249 return cached.clone();
250 }
251
252 debug!("probe_capabilities: START {:?}", path);
253 let mut caps_inner = Capabilities::default();
254
255 if let Ok(c_path) = CString::new(path.to_string_lossy().as_bytes()) {
256 let mut stx: StatxAtomic = unsafe { std::mem::zeroed() };
259 let ret = unsafe {
262 libc::syscall(
263 libc::SYS_statx,
264 libc::AT_FDCWD,
265 c_path.as_ptr(),
266 libc::AT_EMPTY_PATH | libc::AT_NO_AUTOMOUNT,
267 STATX_WRITE_ATOMIC | STATX_DIOALIGN,
268 &mut stx as *mut _
269 )
270 };
271
272 if ret == 0 {
273 if (stx.stx_mask & STATX_WRITE_ATOMIC) != 0 && stx.stx_atomic_write_unit_max > 0 {
274 caps_inner.atomic_min_bytes.store(stx.stx_atomic_write_unit_min, Ordering::Relaxed);
275 caps_inner.atomic_max_bytes.store(stx.stx_atomic_write_unit_max, Ordering::Relaxed);
276 caps_inner.atomic_writes.store(true, Ordering::Relaxed);
277 debug!("probe_capabilities: Atomic writes detected");
278 }
279 if (stx.stx_mask & STATX_DIOALIGN) != 0 {
280 let mut mem_align = stx.stx_dio_mem_align;
281 let mut off_align = stx.stx_dio_offset_align;
282 if let Some(ref dm) = caps_inner.dm_stack
284 && dm.has_vdo {
285 mem_align = 4096;
286 off_align = 4096;
287 debug!("probe_capabilities: kvdo detected, clamping DIO alignment to 4096");
288 }
289 caps_inner.dio_mem_align.store(mem_align, Ordering::Relaxed);
290 caps_inner.dio_offset_align.store(off_align, Ordering::Relaxed);
291 debug!("probe_capabilities: DIO alignment: mem={} offset={}", mem_align, off_align);
292 }
293 }
294 }
295
296 debug!("probe_capabilities: checking uncached_io");
297 caps_inner.uncached_io.store(crate::security::probe_rwf_uncached(path), Ordering::Relaxed);
298
299 debug!("probe_capabilities: checking statfs magic");
300 if let Ok(s) = statfs::statfs(path) {
301 let magic = s.filesystem_type().0 as u64;
302 if magic == NFS_SUPER_MAGIC {
303 caps_inner.is_nfs.store(true, Ordering::Relaxed);
304 debug!("probe_capabilities: NFS detected");
305 }
306 if magic == F2FS_SUPER_MAGIC {
307 caps_inner.f2fs_atomic_legacy.store(true, Ordering::Relaxed);
308 if !caps_inner.atomic_writes.load(Ordering::Relaxed) {
309 caps_inner.atomic_writes.store(true, Ordering::Relaxed);
310 caps_inner.atomic_min_bytes.store(4096, Ordering::Relaxed);
311 caps_inner.atomic_max_bytes.store(u32::MAX, Ordering::Relaxed);
312 debug!("Probe: F2FS Detected. Enabling Legacy Atomic Writes (IOCTL).");
313 }
314 }
315 if magic == TMPFS_SUPER_MAGIC || magic == RAMFS_SUPER_MAGIC || magic == HUGETLBFS_MAGIC {
316 debug!("Probe: tmpfs/ramfs/hugetlbfs detected -- disabling O_DIRECT and reflink");
318 }
319 if magic == OVERLAYFS_MAGIC {
320 debug!("Probe: overlayfs detected");
321 }
322 }
323
324 debug!("probe_capabilities: checking file IOCTLs");
325 if let Ok(f) = std::fs::File::open(path) {
326 let fd = f.as_raw_fd();
327 if unsafe { libc::lseek(fd, 0, libc::SEEK_DATA) } >= 0 {
329 caps_inner.seek_hole.store(true, Ordering::Relaxed);
330 }
331 if probe_btrfs_quotas(fd) {
332 caps_inner.btrfs_quotas.store(true, Ordering::Relaxed);
333 debug!("Probe: Btrfs Qgroups detected on {:?}", path);
334 }
335 }
336
337 debug!("probe_capabilities: checking reflink");
338 if probe_reflink_support(path) {
339 caps_inner.reflink.store(true, Ordering::Relaxed);
340 debug!("probe_capabilities: Reflink supported");
341 }
342
343 debug!("probe_capabilities: checking dm-stack");
344 caps_inner.dm_stack = probe_dm_stack(path);
345 caps_inner.container = Some(detect_container());
346 if let Some(ref dm) = caps_inner.dm_stack {
347 debug!("probe_capabilities: dm-stack depth={} crypt={} integrity={} base={:?}",
348 dm.stack_depth, dm.has_crypt, dm.has_integrity, dm.base_device);
349 }
350
351 let caps = Arc::new(caps_inner);
352 debug!("probe_capabilities: END {:?}", path);
353 GLOBAL_CAPS_CACHE.insert(cache_key, caps.clone());
354 caps
355}
356
357
358fn probe_btrfs_quotas(fd: RawFd) -> bool {
359 let mut info_args: BtrfsIoctlFsInfoArgs = unsafe { std::mem::zeroed() };
361 if unsafe { libc::ioctl(fd, BTRFS_IOC_FS_INFO, &mut info_args) } < 0 {
363 return false;
364 }
365
366 let uuid_bytes = info_args.fsid;
367 let uuid_str = format!(
368 "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
369 uuid_bytes[0], uuid_bytes[1], uuid_bytes[2], uuid_bytes[3],
370 uuid_bytes[4], uuid_bytes[5], uuid_bytes[6], uuid_bytes[7],
371 uuid_bytes[8], uuid_bytes[9], uuid_bytes[10], uuid_bytes[11],
372 uuid_bytes[12], uuid_bytes[13], uuid_bytes[14], uuid_bytes[15]
373 );
374
375 let sysfs_path = PathBuf::from(format!("/sys/fs/btrfs/{}/qgroups", uuid_str));
376 if sysfs_path.exists() {
377 return true;
378 }
379
380 let sysfs_path_old = PathBuf::from(format!("/sys/fs/btrfs/{}/quota_override", uuid_str));
381 if sysfs_path_old.exists() {
382 return true;
383 }
384
385 false
386}
387
388static CACHESTAT_AVAILABLE: OnceLock<bool> = OnceLock::new();
391
392pub fn probe_cachestat() -> bool {
395 *CACHESTAT_AVAILABLE.get_or_init(|| {
396 let dummy_range = [0u64; 2]; let dummy_cs = [0u64; 5];
399 let ret = unsafe {
402 libc::syscall(451, -1i64, dummy_range.as_ptr(), dummy_cs.as_ptr(), 0u32)
403 };
404 ret != -1 || unsafe { *libc::__errno_location() } != libc::ENOSYS
405 })
406}
407
408pub fn query_cache_residency(fd: RawFd, file_size: u64) -> Option<f64> {
411 if !probe_cachestat() { return None; }
412
413 #[repr(C)]
414 struct CachestatRange { off: u64, len: u64 }
415 #[repr(C)]
416 struct Cachestat { nr_cache: u64, nr_dirty: u64, nr_writeback: u64, nr_evicted: u64, nr_recently_evicted: u64 }
417
418 let range = CachestatRange { off: 0, len: 0 }; let mut cs = Cachestat {
420 nr_cache: 0, nr_dirty: 0, nr_writeback: 0,
421 nr_evicted: 0, nr_recently_evicted: 0,
422 };
423 let ret = unsafe {
426 libc::syscall(451, fd as i64, &range as *const _, &mut cs as *mut _, 0u32)
427 };
428 if ret != 0 { return None; }
429
430 let total_pages = file_size.div_ceil(4096);
431 if total_pages == 0 { return None; }
432 Some(cs.nr_cache as f64 / total_pages as f64)
433}
434
435#[cfg(test)]
436mod tests {
437 #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
438 use super::*;
439 use std::sync::atomic::Ordering;
440
441 fn caps_with_reflink(reflink: bool) -> Capabilities {
442 let caps = Capabilities::default();
443 caps.reflink.store(reflink, Ordering::Relaxed);
444 caps
445 }
446
447 #[test]
448 fn test_strategy_no_reflink_returns_standard() {
449 let dir = tempfile::tempdir().unwrap();
450 let src = dir.path().join("src.bin");
451 let dst = dir.path().join("dst.bin");
452 std::fs::write(&src, vec![0u8; 8192]).unwrap();
453 std::fs::write(&dst, b"").unwrap();
454
455 let meta = std::fs::metadata(&src).unwrap();
456 let caps = caps_with_reflink(false);
457 assert_eq!(determine_copy_strategy(&src, &dst, &meta, &caps), CopyStrategy::StandardCopy);
458 }
459
460 #[test]
461 fn test_strategy_reflink_same_device_large_file() {
462 let dir = tempfile::tempdir().unwrap();
463 let src = dir.path().join("src.bin");
464 let dst = dir.path().join("dst.bin");
465 std::fs::write(&src, vec![0u8; 8192]).unwrap();
466 std::fs::write(&dst, b"").unwrap();
467
468 let meta = std::fs::metadata(&src).unwrap();
469 let caps = caps_with_reflink(true);
470 assert_eq!(determine_copy_strategy(&src, &dst, &meta, &caps), CopyStrategy::Reflink);
471 }
472
473 #[test]
474 fn test_strategy_reflink_small_file_returns_standard() {
475 let dir = tempfile::tempdir().unwrap();
476 let src = dir.path().join("src_small.bin");
477 let dst = dir.path().join("dst_small.bin");
478 std::fs::write(&src, vec![0u8; 100]).unwrap();
479 std::fs::write(&dst, b"").unwrap();
480
481 let meta = std::fs::metadata(&src).unwrap();
482 let caps = caps_with_reflink(true);
483 assert_eq!(determine_copy_strategy(&src, &dst, &meta, &caps), CopyStrategy::StandardCopy);
484 }
485
486 #[test]
487 fn test_strategy_exactly_4096_returns_reflink() {
488 let dir = tempfile::tempdir().unwrap();
489 let src = dir.path().join("src_4k.bin");
490 let dst = dir.path().join("dst_4k.bin");
491 std::fs::write(&src, vec![0u8; 4096]).unwrap();
492 std::fs::write(&dst, b"").unwrap();
493
494 let meta = std::fs::metadata(&src).unwrap();
495 let caps = caps_with_reflink(true);
496 assert_eq!(determine_copy_strategy(&src, &dst, &meta, &caps), CopyStrategy::Reflink);
497 }
498
499 #[test]
500 fn test_strategy_dst_not_exist_uses_parent_device() {
501 let dir = tempfile::tempdir().unwrap();
502 let src = dir.path().join("src.bin");
503 let dst = dir.path().join("nonexistent.bin");
504 std::fs::write(&src, vec![0u8; 8192]).unwrap();
505
506 let meta = std::fs::metadata(&src).unwrap();
507 let caps = caps_with_reflink(true);
508 assert_eq!(determine_copy_strategy(&src, &dst, &meta, &caps), CopyStrategy::Reflink);
509 }
510
511 #[test]
512 fn test_strategy_dst_no_parent_returns_standard() {
513 let src_dir = tempfile::tempdir().unwrap();
514 let src = src_dir.path().join("src.bin");
515 std::fs::write(&src, vec![0u8; 8192]).unwrap();
516
517 let meta = std::fs::metadata(&src).unwrap();
518 let caps = caps_with_reflink(true);
519 let dst = Path::new("/nonexistent_mount_abc123/sub/file.bin");
520 assert_eq!(determine_copy_strategy(&src, dst, &meta, &caps), CopyStrategy::StandardCopy);
521 }
522
523 #[test]
524 fn test_capabilities_default() {
525 let caps = Capabilities::default();
526 assert!(!caps.reflink.load(Ordering::Relaxed));
527 assert!(!caps.atomic_writes.load(Ordering::Relaxed));
528 assert!(!caps.is_nfs.load(Ordering::Relaxed));
529 assert!(!caps.seek_hole.load(Ordering::Relaxed));
530 assert!(caps.exchange_range.load(Ordering::Relaxed));
531 assert_eq!(caps.atomic_min_bytes.load(Ordering::Relaxed), 0);
532 assert_eq!(caps.atomic_max_bytes.load(Ordering::Relaxed), 0);
533 assert!(caps.dm_stack.is_none());
534 assert!(caps.container.is_none());
535 }
536
537 #[test]
538 fn test_copy_strategy_debug_and_eq() {
539 assert_eq!(CopyStrategy::Reflink, CopyStrategy::Reflink);
540 assert_eq!(CopyStrategy::StandardCopy, CopyStrategy::StandardCopy);
541 assert_ne!(CopyStrategy::Reflink, CopyStrategy::StandardCopy);
542 let _ = format!("{:?}", CopyStrategy::Reflink);
543 }
544
545 #[test]
546 fn test_probe_cachestat_does_not_panic() {
547 let available = probe_cachestat();
548 assert_eq!(available, probe_cachestat());
551 }
552
553 #[test]
554 fn test_query_cache_residency_on_real_file() {
555 if !probe_cachestat() { return; }
556 let dir = tempfile::tempdir().unwrap();
557 let path = dir.path().join("cached_test.bin");
558 std::fs::write(&path, vec![0xABu8; 65536]).unwrap();
559 let f = std::fs::File::open(&path).unwrap();
561 let residency = query_cache_residency(f.as_raw_fd(), 65536);
562 assert!(residency.is_some());
563 let r = residency.unwrap();
564 assert!((0.0..=1.0).contains(&r));
565 assert!(r > 0.5, "expected >50% cache residency for just-written file, got {:.0}%", r * 100.0);
566 }
567
568 #[test]
569 fn test_query_cache_residency_invalid_fd() {
570 if !probe_cachestat() { return; }
571 assert!(query_cache_residency(-1, 4096).is_none());
573 }
574
575 #[test]
576 fn test_query_cache_residency_zero_size() {
577 if !probe_cachestat() { return; }
578 let dir = tempfile::tempdir().unwrap();
579 let path = dir.path().join("empty.bin");
580 std::fs::write(&path, b"").unwrap();
581 let f = std::fs::File::open(&path).unwrap();
582 assert!(query_cache_residency(f.as_raw_fd(), 0).is_none());
584 }
585
586 #[test]
587 fn test_strategy_cross_device_returns_standard() {
588 let dir = tempfile::tempdir().unwrap();
589 let src = dir.path().join("src.bin");
590 let dst = dir.path().join("dst.bin");
591 std::fs::write(&src, vec![0u8; 8192]).unwrap();
592 std::fs::write(&dst, vec![0u8; 8192]).unwrap();
593
594 let meta = std::fs::metadata(&src).unwrap();
595 let caps = caps_with_reflink(true);
596 let strategy = determine_copy_strategy(&src, &dst, &meta, &caps);
597 assert_eq!(strategy, CopyStrategy::Reflink,
598 "same-device large file with reflink caps should return Reflink");
599 }
600
601 #[test]
602 fn test_strategy_file_below_4096_returns_standard() {
603 let dir = tempfile::tempdir().unwrap();
604 let src = dir.path().join("tiny.bin");
605 let dst = dir.path().join("dst.bin");
606 std::fs::write(&src, vec![0u8; 4095]).unwrap();
607 std::fs::write(&dst, vec![0u8; 4095]).unwrap();
608
609 let meta = std::fs::metadata(&src).unwrap();
610 let caps = caps_with_reflink(true);
611 let strategy = determine_copy_strategy(&src, &dst, &meta, &caps);
612 assert_eq!(strategy, CopyStrategy::StandardCopy,
613 "files < 4096 bytes must use StandardCopy (FICLONE overhead exceeds benefit)");
614 }
615}