From 32b1094ec353fda9a602aa84e5c53224f994fca8 Mon Sep 17 00:00:00 2001 From: Zhengchao An Date: Thu, 9 Jul 2026 03:43:05 +0800 Subject: [PATCH] fix(object-capacity): resolve a symlinked scan root instead of silently counting zero (#4564) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit walkdir defaults to follow_root_links=true precisely so a root that is itself a symlink still descends, but the scanner overrode it with the general follow_symlinks flag (default false). For the common indirection layout (/data/rustfs0 -> /mnt/vol0) the walk yielded a single skipped symlink entry and returned used_bytes=0, file_count=0, is_estimated=false with no error and no log — an exact 0 committed straight into the per-disk baseline (S05). Canonicalize the scan root before walking (also covering nested indirection and keeping the dedicated-mount statvfs check on the real path); resolution failures surface as scan errors instead of a silent zero. Inner-symlink policy is unchanged. Ref: rustfs/backlog#1015 (S05 from audit rustfs/backlog#1010) --- crates/object-capacity/src/scan.rs | 34 ++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/crates/object-capacity/src/scan.rs b/crates/object-capacity/src/scan.rs index 1429208b6..4e29798c1 100644 --- a/crates/object-capacity/src/scan.rs +++ b/crates/object-capacity/src/scan.rs @@ -733,6 +733,16 @@ fn scan_dir_blocking(path: &Path, limits: &ScanLimits, cancelled: &AtomicBool) - )); } + // Resolve the scan root before walking. A drive_path that is itself a + // symlink (container/k8s indirection) combined with + // follow_root_links(false) used to yield a single skipped symlink + // entry and commit an exact 0-byte result into the per-disk baseline + // with no error and no log (backlog#1015 S05). Resolution failures are + // surfaced instead of silently counting zero. + let resolved_root = std::fs::canonicalize(path) + .map_err(|err| std::io::Error::new(err.kind(), format!("failed to resolve scan root {path:?}: {err}")))?; + let path = resolved_root.as_path(); + let start_time = Instant::now(); let mut exact_prefix_bytes = 0u64; let mut overflow_sampled_bytes = 0u64; @@ -1419,6 +1429,30 @@ mod tests { assert_eq!(outer_scan_budget(&tight_limits(Duration::ZERO)), Duration::from_secs(5)); } + #[cfg(unix)] + #[test] + fn test_scan_dir_blocking_resolves_symlink_root() { + use std::fs::File; + use std::io::Write; + + let target_dir = tempfile::TempDir::new().unwrap(); + for i in 0..3 { + let mut file = File::create(target_dir.path().join(format!("f{i}"))).unwrap(); + file.write_all(b"data").unwrap(); + } + + // drive_path is a symlink to the real data directory; with symlink + // following disabled this used to return exact 0 bytes / 0 files. + let link_parent = tempfile::TempDir::new().unwrap(); + let link = link_parent.path().join("rustfs0"); + std::os::unix::fs::symlink(target_dir.path(), &link).unwrap(); + + let result = scan_dir_blocking(&link, &tight_limits(Duration::from_secs(600)), &AtomicBool::new(false)).unwrap(); + assert_eq!(result.used_bytes, 12); + assert_eq!(result.file_count, 3); + assert!(!result.is_estimated); + } + #[test] fn test_scan_dir_blocking_generous_budget_still_exact() { use std::fs::File;