From 80e10f008c80173b1be727976b4db9619a4dbbbe Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 14 Sep 2026 09:46:59 +0200 Subject: [PATCH] du: fix EOVERFLOW on 32-bit Linux by passing O_LARGEFILE On 32-bit Linux the libc open()/openat() bindings do not add O_LARGEFILE the way glibc's large-file entry points do, so the kernel rejects a directory whose metadata overflows the 32-bit ranges with EOVERFLOW. du then failed with "Value too large for defined data type" on any directory traversal. Add the flag to the directory opens in DirFd, and to open_file_at() so a destination file over 2 GiB does not hit the same wall. It is defined as 0 on 64-bit, so no cfg gating is needed beyond Linux/Android, and the opens stay on nix: this module deliberately goes through libc so LD_PRELOAD tools (fakeroot, fakechroot, pseudo) can interpose them. Fixes #11848 --- src/uucore/src/lib/features/safe_traversal.rs | 49 +++++++++++++++++-- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/src/uucore/src/lib/features/safe_traversal.rs b/src/uucore/src/lib/features/safe_traversal.rs index ec5d54f84f..46612c5d49 100644 --- a/src/uucore/src/lib/features/safe_traversal.rs +++ b/src/uucore/src/lib/features/safe_traversal.rs @@ -11,6 +11,7 @@ // spell-checker:ignore CLOEXEC RDONLY TOCTOU closedir dirp fdopendir fstatat openat REMOVEDIR unlinkat smallfile // spell-checker:ignore RAII dirfd fchownat fchown FchmodatFlags fchmodat fchmod mkdirat CREAT WRONLY ELOOP ENOTDIR // spell-checker:ignore atimensec mtimensec ctimensec opath chmods fakeroot fakechroot +// spell-checker:ignore LARGEFILE #[cfg(test)] use std::os::unix::ffi::OsStringExt; @@ -134,6 +135,17 @@ pub struct DirFd { fd: OwnedFd, } +/// `O_LARGEFILE`, or nothing where the platform has no such flag. +/// +/// The libc `open`/`openat` bindings do not add it the way glibc's large-file +/// entry points do, so on 32-bit Linux the kernel rejects anything whose +/// metadata overflows the 32-bit ranges with `EOVERFLOW`. The flag is 0 on +/// 64-bit, so adding it unconditionally costs nothing there. +#[cfg(any(target_os = "linux", target_os = "android"))] +const LARGEFILE: OFlag = OFlag::O_LARGEFILE; +#[cfg(not(any(target_os = "linux", target_os = "android")))] +const LARGEFILE: OFlag = OFlag::empty(); + impl DirFd { /// Open a directory and return a file descriptor /// @@ -141,7 +153,7 @@ impl DirFd { /// * `path` - The path to the directory to open /// * `symlink_behavior` - Whether to follow symlinks when opening pub fn open(path: &Path, symlink_behavior: SymlinkBehavior) -> io::Result { - let mut flags = OFlag::O_RDONLY | OFlag::O_DIRECTORY | OFlag::O_CLOEXEC; + let mut flags = OFlag::O_RDONLY | OFlag::O_DIRECTORY | OFlag::O_CLOEXEC | LARGEFILE; if !symlink_behavior.should_follow() { flags |= OFlag::O_NOFOLLOW; } @@ -162,7 +174,7 @@ impl DirFd { pub fn open_subdir(&self, name: &OsStr, symlink_behavior: SymlinkBehavior) -> io::Result { let name_cstr = CString::new(name.as_bytes()).map_err(|_| SafeTraversalError::PathContainsNull)?; - let mut flags = OFlag::O_RDONLY | OFlag::O_DIRECTORY | OFlag::O_CLOEXEC; + let mut flags = OFlag::O_RDONLY | OFlag::O_DIRECTORY | OFlag::O_CLOEXEC | LARGEFILE; if !symlink_behavior.should_follow() { flags |= OFlag::O_NOFOLLOW; } @@ -466,7 +478,8 @@ impl DirFd { | OFlag::O_WRONLY | OFlag::O_TRUNC | OFlag::O_CLOEXEC - | OFlag::O_NOFOLLOW; + | OFlag::O_NOFOLLOW + | LARGEFILE; let mode = Mode::from_bits_truncate(0o666); // Default file permissions let fd: OwnedFd = openat(self.fd.as_fd(), name_cstr.as_c_str(), flags, mode) @@ -1028,6 +1041,33 @@ mod tests { ); } + /// A directory fd must end up with the same open-file status flags as one + /// `std::fs::File` would produce, `O_LARGEFILE` included. On 32-bit Linux a + /// missing `O_LARGEFILE` makes the kernel reject directories whose metadata + /// overflows the 32-bit ranges with EOVERFLOW. + #[test] + #[cfg(any(target_os = "linux", target_os = "android"))] + fn test_dirfd_open_status_flags_match_std() { + use nix::fcntl::{FcntlArg, fcntl}; + + let temp_dir = TempDir::new().unwrap(); + fs::create_dir(temp_dir.path().join("nested")).unwrap(); + + let reference = fs::File::open(temp_dir.path().join("nested")).unwrap(); + // F_GETFL reports O_DIRECTORY back for a directory fd; std opens the + // same directory as a plain file, so add it to the expected flags. + let expected = fcntl(&reference, FcntlArg::F_GETFL).unwrap() | libc::O_DIRECTORY; + + let parent = DirFd::open(temp_dir.path(), SymlinkBehavior::Follow).unwrap(); + let direct = DirFd::open(&temp_dir.path().join("nested"), SymlinkBehavior::Follow).unwrap(); + let sub = parent + .open_subdir(OsStr::new("nested"), SymlinkBehavior::Follow) + .unwrap(); + + assert_eq!(fcntl(&direct.fd, FcntlArg::F_GETFL).unwrap(), expected); + assert_eq!(fcntl(&sub.fd, FcntlArg::F_GETFL).unwrap(), expected); + } + #[test] fn test_dirfd_open_nonexistent_subdir() { let temp_dir = TempDir::new().unwrap(); @@ -1239,6 +1279,9 @@ mod tests { if let Err(e) = result { // Should be InvalidInput for null byte error assert_eq!(e.kind(), io::ErrorKind::InvalidInput); + // Reported as PathContainsNull, not as a raw EINVAL coming back + // from the openat call itself. + assert_eq!(e.raw_os_error(), None); } }