diff --git a/alioth/src/fuse/fuse.rs b/alioth/src/fuse/fuse.rs index b44b0dbc..341c9347 100644 --- a/alioth/src/fuse/fuse.rs +++ b/alioth/src/fuse/fuse.rs @@ -133,7 +133,7 @@ pub trait Fuse { fuse_method!(get_attr, &FuseGetattrIn, FuseAttrOut); fuse_method!(open, &FuseOpenIn, FuseOpenOut); fuse_method!(open_dir, &FuseOpenIn, FuseOpenOut); - fuse_method!(read_dir, &FuseReadIn, &mut [u8]); + fuse_method!(read_dir, &FuseReadIn, &mut [IoSliceMut]); fuse_method!(release_dir, &FuseReleaseIn, ()); fuse_method!(lookup, &[u8], FuseEntryOut); fuse_method!(forget, &FuseForgetIn, ()); @@ -143,7 +143,7 @@ pub trait Fuse { fuse_method!(release, &FuseReleaseIn, ()); fuse_method!(syncfs, &FuseSyncfsIn, ()); fuse_method!(ioctl, &FuseIoctlIn, FuseIoctlOut); - fuse_method!(get_xattr, &[u8], &mut [u8]); + fuse_method!(get_xattr, &[u8], &mut [IoSliceMut]); fuse_method!(set_xattr, &[u8], ()); fuse_method!(create, &FuseCreateIn, &[u8], FuseCreateOut); fuse_method!(write, &FuseWriteIn, &[IoSlice], FuseWriteOut); diff --git a/alioth/src/fuse/passthrough.rs b/alioth/src/fuse/passthrough.rs index da6e1996..f3451db0 100644 --- a/alioth/src/fuse/passthrough.rs +++ b/alioth/src/fuse/passthrough.rs @@ -229,7 +229,7 @@ impl Fuse for Passthrough { &mut self, hdr: &FuseInHeader, in_: &FuseReadIn, - mut buf: &mut [u8], + bufs: &mut [IoSliceMut], ) -> Result { let node = self.get_node_mut(hdr.nodeid)?; log::trace!("read_dir: {:?}", node.path); @@ -245,6 +245,9 @@ impl Fuse for Passthrough { } let mut total_len = 0; + let mut cur = 0usize; + let mut off = 0usize; + let mut scratch = [0u8; size_of::() + 264]; while let Some((index, entry)) = read_dir.peek() { let e = entry.as_ref()?; @@ -260,15 +263,25 @@ impl Fuse for Passthrough { }; let aligned_namelen = align_up_ty!(namelen, FuseDirent); let len = size_of_val(&dir_entry) + aligned_namelen; - let Some((p1, p2)) = buf.split_at_mut_checked(len) else { + if len > scratch.len() { break; - }; - let (b_entry, b_name) = p1.split_at_mut(size_of_val(&dir_entry)); + } + scratch[..size_of_val(&dir_entry)].copy_from_slice(dir_entry.as_bytes()); + scratch[size_of_val(&dir_entry)..size_of_val(&dir_entry) + namelen] + .copy_from_slice(name.as_encoded_bytes()); + for b in &mut scratch[size_of_val(&dir_entry) + namelen..len] { + *b = 0; + } log::trace!("read_dir: {dir_entry:?} {name:?}"); - b_entry.copy_from_slice(dir_entry.as_bytes()); - b_name[..namelen].copy_from_slice(name.as_encoded_bytes()); - - buf = p2; + while cur < bufs.len() && bufs[cur].len() - off < len { + cur += 1; + off = 0; + } + if cur >= bufs.len() { + break; + } + bufs[cur][off..off + len].copy_from_slice(&scratch[..len]); + off += len; total_len += len; read_dir.next(); } diff --git a/alioth/src/virtio/dev/fs/fs.rs b/alioth/src/virtio/dev/fs/fs.rs index 649310af..19f1c9dc 100644 --- a/alioth/src/virtio/dev/fs/fs.rs +++ b/alioth/src/virtio/dev/fs/fs.rs @@ -208,6 +208,15 @@ where log::trace!("{name}: {opcode:?}\n{in_s:?}\nsize = {size:?}",); Ok(size) }}; + ($func:ident, &[u8], &mut[IoSliceMut]) => {{ + let [in_] = in_ else { + return Err(io::Error::from_raw_os_error(libc::EINVAL))?; + }; + let size = self.fuse.$func(hdr, in_, out)?; + let in_s = String::from_utf8_lossy(in_); + log::trace!("{name}: {opcode:?}\n{in_s:?}\nsize = {size:?}",); + Ok(size) + }}; ($func:ident, &_, &mut[u8]) => {{ let [out] = out else { return Err(io::Error::from_raw_os_error(libc::EINVAL))?; @@ -250,7 +259,7 @@ where FuseOpcode::GETATTR => opcode_branch!(get_attr, &_, _), FuseOpcode::OPEN => opcode_branch!(open, &_, _), FuseOpcode::OPENDIR => opcode_branch!(open_dir, &_, _), - FuseOpcode::READDIR => opcode_branch!(read_dir, &_, &mut [u8]), + FuseOpcode::READDIR => opcode_branch!(read_dir, &_, &mut [IoSliceMut]), FuseOpcode::RELEASEDIR => opcode_branch!(release_dir, &_, _), FuseOpcode::LOOKUP => opcode_branch!(lookup, &[u8], _), FuseOpcode::FORGET => opcode_branch!(forget, &_, _), @@ -260,7 +269,7 @@ where FuseOpcode::RELEASE => opcode_branch!(release, &_, _), FuseOpcode::SYNCFS => opcode_branch!(syncfs, &_, _), FuseOpcode::IOCTL => opcode_branch!(ioctl, &_, _), - FuseOpcode::GETXATTR => opcode_branch!(get_xattr, &[u8], &mut [u8]), + FuseOpcode::GETXATTR => opcode_branch!(get_xattr, &[u8], &mut [IoSliceMut]), FuseOpcode::SETXATTR => opcode_branch!(set_xattr, &[u8], _), FuseOpcode::CREATE => opcode_branch!(create, &_, &[u8], _), FuseOpcode::UNLINK => opcode_branch!(unlink, &[u8], _), diff --git a/alioth/tests/passthrough_test.rs b/alioth/tests/passthrough_test.rs new file mode 100644 index 00000000..d284b3b5 --- /dev/null +++ b/alioth/tests/passthrough_test.rs @@ -0,0 +1,160 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::fs::File; +use std::io::IoSliceMut; + +use alioth::fuse::Fuse; +use alioth::fuse::bindings::{ + FUSE_ROOT_ID, FuseDirent, FuseDirentType, FuseInHeader, FuseOpcode, FuseOpenIn, FuseReadIn, +}; +use alioth::fuse::passthrough::Passthrough; +use tempfile::TempDir; +use zerocopy::FromBytes; + +const NUM_FILES: usize = 6; +// 24-byte FuseDirent + "file" (5 bytes) padded to 8 +const DIRENT_SIZE: usize = 32; + +fn hdr(opcode: FuseOpcode) -> FuseInHeader { + FuseInHeader { + len: 0, + opcode, + unique: 0, + nodeid: FUSE_ROOT_ID, + uid: 0, + gid: 0, + pid: 0, + total_extlen: 0, + padding: 0, + } +} + +fn create_files(dir: &TempDir) -> Vec { + (0..NUM_FILES) + .map(|i| { + let name = format!("file{i}"); + File::create(dir.path().join(&name)).unwrap(); + name + }) + .collect() +} + +/// Parses whole dirents from one buffer, stopping when the remainder is too +/// short for another one. Returns the names, offsets and bytes consumed. +fn parse_dirents(buf: &[u8]) -> (Vec, Vec, usize) { + let mut names = vec![]; + let mut offs = vec![]; + let mut off = 0; + while off + size_of::() <= buf.len() { + let (dirent, rest) = FuseDirent::ref_from_prefix(&buf[off..]).unwrap(); + let namelen = dirent.namelen as usize; + // zero-initialized buffer tail past the replied data is not a dirent; + // parsed == total below still catches corruption + if dirent.type_ != FuseDirentType::REG || namelen == 0 { + break; + } + let aligned_namelen = (namelen + 7) & !7; + if rest.len() < aligned_namelen { + break; + } + assert_eq!(dirent.type_, FuseDirentType::REG); + names.push(String::from_utf8(rest[..namelen].to_vec()).unwrap()); + offs.push(dirent.off); + off += size_of::() + aligned_namelen; + } + (names, offs, off) +} + +fn read_dir(dir: &TempDir, slice_lens: &[usize]) -> (usize, Vec) { + let mut fs = Passthrough::new(dir.path().into()).unwrap(); + let open = fs + .open_dir(&hdr(FuseOpcode::OPENDIR), &FuseOpenIn::default()) + .unwrap(); + + let mut storage: Vec> = slice_lens.iter().map(|&len| vec![0u8; len]).collect(); + let mut slices: Vec = storage.iter_mut().map(|b| IoSliceMut::new(b)).collect(); + let total = fs + .read_dir( + &hdr(FuseOpcode::READDIR), + &FuseReadIn { + fh: open.fh, + ..Default::default() + }, + &mut slices, + ) + .unwrap(); + + // every replied byte must belong to a whole dirent inside a single buffer + let mut names = vec![]; + let mut offs = vec![]; + let mut parsed = 0; + for buf in &storage { + let (n, o, bytes) = parse_dirents(buf); + names.extend(n); + offs.extend(o); + parsed += bytes; + } + assert_eq!(parsed, total); + assert_eq!(offs, (1..=offs.len() as u64).collect::>()); + (total, names) +} + +fn sorted(mut names: Vec) -> Vec { + names.sort(); + names +} + +#[test] +fn read_dir_single_buffer_test() { + let dir = TempDir::new().unwrap(); + let mut expected = create_files(&dir); + expected.sort(); + let (total, names) = read_dir(&dir, &[4096]); + assert_eq!(total, NUM_FILES * DIRENT_SIZE); + assert_eq!(sorted(names), expected); +} + +#[test] +fn read_dir_multiple_buffers_test() { + let dir = TempDir::new().unwrap(); + let mut expected = create_files(&dir); + expected.sort(); + let (total, names) = read_dir(&dir, &[64, 64, 64]); + assert_eq!(total, NUM_FILES * DIRENT_SIZE); + assert_eq!(sorted(names), expected); +} + +#[test] +fn read_dir_skips_buffer_remainder_test() { + // a 40-byte buffer fits one dirent; the next dirent must not be split + // into the trailing 8 bytes but start in the next buffer + let dir = TempDir::new().unwrap(); + let mut expected = create_files(&dir); + expected.sort(); + let (total, names) = read_dir(&dir, &[40; NUM_FILES]); + assert_eq!(total, NUM_FILES * DIRENT_SIZE); + assert_eq!(sorted(names), expected); +} + +#[test] +fn read_dir_stops_when_buffers_full_test() { + let dir = TempDir::new().unwrap(); + let mut expected = create_files(&dir); + expected.sort(); + let (total, names) = read_dir(&dir, &[128]); + assert_eq!(total, 4 * DIRENT_SIZE); + assert_eq!(names.len(), 4); + assert!(names.iter().all(|n| expected.contains(n))); +}