From de2d989fddd9b7a488a13d21e291c1032f9281b5 Mon Sep 17 00:00:00 2001 From: duanhongyi Date: Mon, 6 Jul 2026 16:20:04 +0800 Subject: [PATCH] passthrough: handle NFS directory cookies exceeding i64::MAX NFSv4 directory cookies (nfs_cookie4) are unsigned 64-bit values. Roughly half of them exceed INT64_MAX, so the current code that casts the cookie to off64_t and calls lseek64() receives EINVAL from nfs_llseek_dir() on the host kernel. Fall back to a linear scan: rewind to the start of the directory and walk getdents64() batches until we find the entry whose d_off equals the target cookie. To avoid returning the target entry itself and causing an infinite loop, skip past it before returning the buffer to the caller. This mirrors the approach taken by virtiofsd in MR !319. Fixes #TODO. Signed-off-by: duanhongyi --- src/passthrough/sync_io.rs | 214 +++++++++++++++++++++++++++++++++---- 1 file changed, 193 insertions(+), 21 deletions(-) diff --git a/src/passthrough/sync_io.rs b/src/passthrough/sync_io.rs index e092e6d5..365a3041 100644 --- a/src/passthrough/sync_io.rs +++ b/src/passthrough/sync_io.rs @@ -58,6 +58,33 @@ impl PassthroughFs { Ok(()) } + /// Skip entries in a getdents64 buffer up to and including the entry whose + /// `d_off` equals `offset`. After this returns, `buf` starts with the entry + /// immediately after the matched one. Returns `true` if the target cookie + /// was found, `false` otherwise. + fn skip_to_cookie(buf: &mut Vec, offset: u64) -> bool { + let mut cur: usize = 0; + let mut found = false; + let mut target_reclen: usize = 0; + while cur + size_of::() <= buf.len() { + let front = &buf[cur..cur + size_of::()]; + let dirent64 = LinuxDirent64::from_slice(front) + .expect("fuse: unable to get LinuxDirent64 from slice"); + if dirent64.d_off as u64 == offset { + found = true; + target_reclen = dirent64.d_reclen as usize; + break; + } + cur += dirent64.d_reclen as usize; + } + + if found { + cur += target_reclen; + buf.drain(..cur); + } + found + } + fn do_readdir( &self, inode: Inode, @@ -79,29 +106,89 @@ impl PassthroughFs { // changes the kernel offset while we are using it. let (guard, dir) = data.get_file_mut(); - // Safe because this doesn't modify any memory and we check the return value. - let res = - unsafe { libc::lseek64(dir.as_raw_fd(), offset as libc::off64_t, libc::SEEK_SET) }; - if res < 0 { - return Err(io::Error::last_os_error()); - } - - // Safe because the kernel guarantees that it will only write to `buf` and we check the - // return value. - let res = unsafe { - libc::syscall( - libc::SYS_getdents64, - dir.as_raw_fd(), - buf.as_mut_ptr() as *mut LinuxDirent64, - size as libc::c_int, - ) + // NFSv4 directory cookies (`nfs_cookie4`, RFC 7530) are unsigned 64-bit and roughly + // half exceed `INT64_MAX`. When the guest resumes a readdir from such a cookie, the + // `u64` -> `off64_t` cast produces a negative value and the host kernel's + // `nfs_llseek_dir()` rejects it with `EINVAL`. In that case fall back to a linear scan + // from the beginning, skipping past the target cookie so the caller receives exactly the + // entries it has not seen yet. + let seek_ok = if offset > i64::MAX as u64 { + false + } else { + // Safe because this doesn't modify any memory and we check the return value. + let res = unsafe { + libc::lseek64(dir.as_raw_fd(), offset as libc::off64_t, libc::SEEK_SET) + }; + if res >= 0 { + true + } else { + let err = io::Error::last_os_error(); + // Only the "offset too large" case is recoverable here. + if err.raw_os_error() != Some(libc::EINVAL) { + return Err(err); + } + false + } }; - if res < 0 { - return Err(io::Error::last_os_error()); - } - // Safe because we trust the value returned by kernel. - unsafe { buf.set_len(res as usize) }; + if seek_ok { + // Safe because the kernel guarantees that it will only write to `buf` and we check + // the return value. + let res = unsafe { + libc::syscall( + libc::SYS_getdents64, + dir.as_raw_fd(), + buf.as_mut_ptr() as *mut LinuxDirent64, + size as libc::c_int, + ) + }; + if res < 0 { + return Err(io::Error::last_os_error()); + } + // Safe because we trust the value returned by kernel. + unsafe { buf.set_len(res as usize) }; + } else { + // Fallback for cookies the kernel cannot `lseek()` to: rewind and walk batches with + // `getdents64` until the entry whose `d_off == offset` is found, then return that + // batch from just past it. We never re-seek nor discard an already-read batch, so + // no entries are lost. + // + // Safe because this doesn't modify any memory and we check the return value. + let res = unsafe { libc::lseek64(dir.as_raw_fd(), 0, libc::SEEK_SET) }; + if res < 0 { + return Err(io::Error::last_os_error()); + } + + loop { + // Safe because the kernel guarantees that it will only write to `buf` and we + // check the return value. + let res = unsafe { + libc::syscall( + libc::SYS_getdents64, + dir.as_raw_fd(), + buf.as_mut_ptr() as *mut LinuxDirent64, + size as libc::c_int, + ) + }; + if res < 0 { + return Err(io::Error::last_os_error()); + } + // Safe because we trust the value returned by kernel. + unsafe { buf.set_len(res as usize) }; + + if res == 0 { + // EOF without finding the target cookie: the entry is gone (directory + // changed, or the cookie was stale). Return an empty buffer so the guest + // stops iterating instead of looping forever. + break; + } + + if Self::skip_to_cookie(&mut buf, offset) { + break; + } + buf.clear(); + } + } // Explicitly drop the lock so that it's not held while we fill in the fuse buffer. mem::drop(guard); @@ -1629,3 +1716,88 @@ mod tests { assert!(fs.fsyncdir(&ctx, ROOT_ID, false, 0).is_ok()); } } + +#[cfg(test)] +mod readdir_cookie_tests { + use super::*; + use std::ffi::CStr; + + /// Build a buffer containing the serialized dirent records described by + /// `entries`. Each entry is (d_off, name). The inode number and type + /// are dummies. + fn build_dirent_buf(entries: &[(u64, &[u8])]) -> Vec { + let header = size_of::(); + // Allocate enough for the entries plus padding to 8-byte alignment. + let mut buf = Vec::new(); + for (off, name) in entries { + let name_len = name.len() + 1; // include trailing NUL + let reclen = (header + name_len + 7) & !7; + let dirent = LinuxDirent64 { + d_ino: 1, + d_off: *off as libc::off64_t, + d_reclen: reclen as libc::c_ushort, + d_ty: libc::DT_REG as libc::c_uchar, + }; + let mut entry = dirent.as_slice().to_vec(); + entry.resize(header, 0); + entry.extend_from_slice(name); + entry.resize(reclen, 0); + buf.extend_from_slice(&entry); + } + buf + } + + fn names(buf: &Vec) -> Vec { + let mut out = Vec::new(); + let mut pos = 0; + while pos + size_of::() <= buf.len() { + let front = &buf[pos..pos + size_of::()]; + let dirent = LinuxDirent64::from_slice(front).unwrap(); + let namelen = dirent.d_reclen as usize - size_of::(); + let name_start = pos + size_of::(); + let name_slice = &buf[name_start..name_start + namelen]; + let cstr = CStr::from_bytes_until_nul(name_slice).unwrap(); + out.push(String::from_utf8_lossy(cstr.to_bytes()).to_string()); + pos += dirent.d_reclen as usize; + } + out + } + + #[test] + fn skip_to_cookie_finds_middle_entry() { + let mut buf = build_dirent_buf(&[(1, b"a"), (2, b"b"), (3, b"c")]); + assert!(PassthroughFs::<()>::skip_to_cookie(&mut buf, 2)); + assert_eq!(names(&buf), vec!["c".to_string()]); + } + + #[test] + fn skip_to_cookie_finds_first_entry() { + let mut buf = build_dirent_buf(&[(1, b"a"), (2, b"b")]); + assert!(PassthroughFs::<()>::skip_to_cookie(&mut buf, 1)); + assert_eq!(names(&buf), vec!["b".to_string()]); + } + + #[test] + fn skip_to_cookie_finds_last_entry() { + let mut buf = build_dirent_buf(&[(1, b"a"), (2, b"b")]); + assert!(PassthroughFs::<()>::skip_to_cookie(&mut buf, 2)); + assert!(buf.is_empty()); + } + + #[test] + fn skip_to_cookie_not_found() { + let mut buf = build_dirent_buf(&[(1, b"a"), (2, b"b")]); + assert!(!PassthroughFs::<()>::skip_to_cookie(&mut buf, 99)); + // Buffer should be left untouched when the cookie is not present. + assert_eq!(names(&buf), vec!["a".to_string(), "b".to_string()]); + } + + #[test] + fn skip_to_cookie_large_cookie() { + // Simulate the NFS case: a cookie whose high bit is set. + let large = u64::MAX - 42; + let mut buf = build_dirent_buf(&[(1, b"a"), (large, b"b"), (3, b"c")]); + assert!(PassthroughFs::<()>::skip_to_cookie(&mut buf, large)); + assert_eq!(names(&buf), vec!["c".to_string()]); + } +}