Skip to content

Commit db8c7cc

Browse files
stephenjudkinsLencerf
authored andcommitted
fix(fs): support multi-descriptor reply buffers for READDIR and GETXATTR
Virtio-fs reply buffers for READDIR and GETXATTR may span multiple writable virtqueue descriptors (or zero payload descriptors when GETXATTR queries an attribute's size), which previously failed with EINVAL because opcode_branch! matched only a single output slice. Change Fuse::read_dir and Fuse::get_xattr to accept &mut [IoSliceMut], update Passthrough::read_dir to write directory entries across the slice list, and add unit tests for Passthrough::read_dir. Signed-off-by: Stephen Judkins <stephen.judkins@gmail.com> Assisted-by: Antigravity:Gemini-4-Argon Signed-off-by: Changyuan Lyu <changyuanl@google.com>
1 parent f3fe439 commit db8c7cc

4 files changed

Lines changed: 170 additions & 23 deletions

File tree

‎alioth/src/fuse/fuse.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ pub trait Fuse {
133133
fuse_method!(get_attr, &FuseGetattrIn, FuseAttrOut);
134134
fuse_method!(open, &FuseOpenIn, FuseOpenOut);
135135
fuse_method!(open_dir, &FuseOpenIn, FuseOpenOut);
136-
fuse_method!(read_dir, &FuseReadIn, &mut [u8]);
136+
fuse_method!(read_dir, &FuseReadIn, &mut [IoSliceMut]);
137137
fuse_method!(release_dir, &FuseReleaseIn, ());
138138
fuse_method!(lookup, &[u8], FuseEntryOut);
139139
fuse_method!(forget, &FuseForgetIn, ());
@@ -143,7 +143,7 @@ pub trait Fuse {
143143
fuse_method!(release, &FuseReleaseIn, ());
144144
fuse_method!(syncfs, &FuseSyncfsIn, ());
145145
fuse_method!(ioctl, &FuseIoctlIn, FuseIoctlOut);
146-
fuse_method!(get_xattr, &[u8], &mut [u8]);
146+
fuse_method!(get_xattr, &[u8], &mut [IoSliceMut]);
147147
fuse_method!(set_xattr, &[u8], ());
148148
fuse_method!(create, &FuseCreateIn, &[u8], FuseCreateOut);
149149
fuse_method!(write, &FuseWriteIn, &[IoSlice], FuseWriteOut);

‎alioth/src/fuse/passthrough.rs‎

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ impl Fuse for Passthrough {
229229
&mut self,
230230
hdr: &FuseInHeader,
231231
in_: &FuseReadIn,
232-
mut buf: &mut [u8],
232+
bufs: &mut [IoSliceMut],
233233
) -> Result<usize> {
234234
let node = self.get_node_mut(hdr.nodeid)?;
235235
log::trace!("read_dir: {:?}", node.path);
@@ -245,6 +245,9 @@ impl Fuse for Passthrough {
245245
}
246246

247247
let mut total_len = 0;
248+
let mut rem: usize = bufs.iter().map(|b| b.len()).sum();
249+
let mut cur = 0;
250+
let mut off = 0;
248251

249252
while let Some((index, entry)) = read_dir.peek() {
250253
let e = entry.as_ref()?;
@@ -260,15 +263,29 @@ impl Fuse for Passthrough {
260263
};
261264
let aligned_namelen = align_up_ty!(namelen, FuseDirent);
262265
let len = size_of_val(&dir_entry) + aligned_namelen;
263-
let Some((p1, p2)) = buf.split_at_mut_checked(len) else {
266+
if len > rem {
264267
break;
265-
};
266-
let (b_entry, b_name) = p1.split_at_mut(size_of_val(&dir_entry));
268+
}
267269
log::trace!("read_dir: {dir_entry:?} {name:?}");
268-
b_entry.copy_from_slice(dir_entry.as_bytes());
269-
b_name[..namelen].copy_from_slice(name.as_encoded_bytes());
270-
271-
buf = p2;
270+
let padding = [0u8; align_of::<FuseDirent>()];
271+
for mut src in [
272+
dir_entry.as_bytes(),
273+
name.as_encoded_bytes(),
274+
&padding[..aligned_namelen - namelen],
275+
] {
276+
while !src.is_empty() {
277+
let dst = &mut bufs[cur][off..];
278+
let n = dst.len().min(src.len());
279+
dst[..n].copy_from_slice(&src[..n]);
280+
src = &src[n..];
281+
off += n;
282+
if off == bufs[cur].len() {
283+
cur += 1;
284+
off = 0;
285+
}
286+
}
287+
}
288+
rem -= len;
272289
total_len += len;
273290
read_dir.next();
274291
}

‎alioth/src/virtio/dev/fs/fs.rs‎

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -198,24 +198,15 @@ where
198198
log::trace!("{name}: {opcode:?}\n{in_s:?}\n{ret:x?}");
199199
Ok(size)
200200
}};
201-
($func:ident, &[u8], &mut[u8]) => {{
202-
let ([in_], [out]) = (in_, out) else {
201+
($func:ident, &[u8], &mut[IoSliceMut]) => {{
202+
let [in_] = in_ else {
203203
return Err(io::Error::from_raw_os_error(libc::EINVAL))?;
204204
};
205205
let size = self.fuse.$func(hdr, in_, out)?;
206206
let in_s = String::from_utf8_lossy(in_);
207207
log::trace!("{name}: {opcode:?}\n{in_s:?}\nsize = {size:?}",);
208208
Ok(size)
209209
}};
210-
($func:ident, &_, &mut[u8]) => {{
211-
let [out] = out else {
212-
return Err(io::Error::from_raw_os_error(libc::EINVAL))?;
213-
};
214-
let (in_, _) = parse_in(in_)?;
215-
let size = self.fuse.$func(hdr, in_, out)?;
216-
log::trace!("{name}: {opcode:?}\n{in_:x?}\nsize = {size}");
217-
Ok(size)
218-
}};
219210
($func:ident, &_, &mut[IoSliceMut]) => {{
220211
let (in_, _) = parse_in(in_)?;
221212
let size = self.fuse.$func(hdr, in_, out)?;
@@ -249,7 +240,7 @@ where
249240
FuseOpcode::GETATTR => opcode_branch!(get_attr, &_, _),
250241
FuseOpcode::OPEN => opcode_branch!(open, &_, _),
251242
FuseOpcode::OPENDIR => opcode_branch!(open_dir, &_, _),
252-
FuseOpcode::READDIR => opcode_branch!(read_dir, &_, &mut [u8]),
243+
FuseOpcode::READDIR => opcode_branch!(read_dir, &_, &mut [IoSliceMut]),
253244
FuseOpcode::RELEASEDIR => opcode_branch!(release_dir, &_, _),
254245
FuseOpcode::LOOKUP => opcode_branch!(lookup, &[u8], _),
255246
FuseOpcode::FORGET => opcode_branch!(forget, &_, _),
@@ -259,7 +250,7 @@ where
259250
FuseOpcode::RELEASE => opcode_branch!(release, &_, _),
260251
FuseOpcode::SYNCFS => opcode_branch!(syncfs, &_, _),
261252
FuseOpcode::IOCTL => opcode_branch!(ioctl, &_, _),
262-
FuseOpcode::GETXATTR => opcode_branch!(get_xattr, &[u8], &mut [u8]),
253+
FuseOpcode::GETXATTR => opcode_branch!(get_xattr, &[u8], &mut [IoSliceMut]),
263254
FuseOpcode::SETXATTR => opcode_branch!(set_xattr, &[u8], _),
264255
FuseOpcode::CREATE => opcode_branch!(create, &_, &[u8], _),
265256
FuseOpcode::UNLINK => opcode_branch!(unlink, &[u8], _),

‎alioth/tests/passthrough_test.rs‎

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
use std::fs::File;
16+
use std::io::IoSliceMut;
17+
18+
use alioth::fuse::Fuse;
19+
use alioth::fuse::bindings::{
20+
FUSE_ROOT_ID, FuseDirent, FuseDirentType, FuseInHeader, FuseOpcode, FuseOpenIn, FuseReadIn,
21+
};
22+
use alioth::fuse::passthrough::Passthrough;
23+
use tempfile::TempDir;
24+
use zerocopy::FromBytes;
25+
26+
const NUM_FILES: usize = 6;
27+
// 24-byte FuseDirent + "file<N>" (5 bytes) padded to 8
28+
const DIRENT_SIZE: usize = 32;
29+
30+
fn hdr(opcode: FuseOpcode) -> FuseInHeader {
31+
FuseInHeader {
32+
len: 0,
33+
opcode,
34+
unique: 0,
35+
nodeid: FUSE_ROOT_ID,
36+
uid: 0,
37+
gid: 0,
38+
pid: 0,
39+
total_extlen: 0,
40+
padding: 0,
41+
}
42+
}
43+
44+
fn create_files(dir: &TempDir) -> Vec<String> {
45+
(0..NUM_FILES)
46+
.map(|i| {
47+
let name = format!("file{i}");
48+
File::create(dir.path().join(&name)).unwrap();
49+
name
50+
})
51+
.collect()
52+
}
53+
54+
/// Parses dirents from a contiguous reply buffer of length `buf.len()`.
55+
fn parse_dirents(mut buf: &[u8]) -> (Vec<String>, Vec<u64>) {
56+
let mut names = vec![];
57+
let mut offs = vec![];
58+
while !buf.is_empty() {
59+
let (dirent, rest) = FuseDirent::ref_from_prefix(buf).unwrap();
60+
let namelen = dirent.namelen as usize;
61+
assert!(namelen > 0);
62+
assert_eq!(dirent.type_, FuseDirentType::REG);
63+
let aligned_namelen = (namelen + 7) & !7;
64+
assert_eq!(
65+
&rest[namelen..aligned_namelen],
66+
&[0u8; 8][..aligned_namelen - namelen]
67+
);
68+
names.push(String::from_utf8(rest[..namelen].to_vec()).unwrap());
69+
offs.push(dirent.off);
70+
buf = &rest[aligned_namelen..];
71+
}
72+
(names, offs)
73+
}
74+
75+
fn read_dir(dir: &TempDir, slice_lens: &[usize]) -> (usize, Vec<String>) {
76+
let mut fs = Passthrough::new(dir.path().into()).unwrap();
77+
let open = fs
78+
.open_dir(&hdr(FuseOpcode::OPENDIR), &FuseOpenIn::default())
79+
.unwrap();
80+
81+
let mut storage: Vec<Vec<u8>> = slice_lens.iter().map(|&len| vec![0u8; len]).collect();
82+
let mut slices: Vec<IoSliceMut> = storage.iter_mut().map(|b| IoSliceMut::new(b)).collect();
83+
let total = fs
84+
.read_dir(
85+
&hdr(FuseOpcode::READDIR),
86+
&FuseReadIn {
87+
fh: open.fh,
88+
..Default::default()
89+
},
90+
&mut slices,
91+
)
92+
.unwrap();
93+
94+
let flat: Vec<u8> = storage.iter().flatten().copied().collect();
95+
assert!(flat[total..].iter().all(|&b| b == 0));
96+
let (mut names, offs) = parse_dirents(&flat[..total]);
97+
assert_eq!(offs, (1..=offs.len() as u64).collect::<Vec<_>>());
98+
names.sort();
99+
(total, names)
100+
}
101+
102+
#[test]
103+
fn read_dir_single_buffer_test() {
104+
let dir = TempDir::new().unwrap();
105+
let expected = create_files(&dir);
106+
let (total, names) = read_dir(&dir, &[4096]);
107+
assert_eq!(total, NUM_FILES * DIRENT_SIZE);
108+
assert_eq!(names, expected);
109+
}
110+
111+
#[test]
112+
fn read_dir_multiple_buffers_test() {
113+
let dir = TempDir::new().unwrap();
114+
let expected = create_files(&dir);
115+
let (total, names) = read_dir(&dir, &[64, 64, 64]);
116+
assert_eq!(total, NUM_FILES * DIRENT_SIZE);
117+
assert_eq!(names, expected);
118+
}
119+
120+
#[test]
121+
fn read_dir_straddles_buffer_boundary_test() {
122+
// A 40-byte buffer fits one 32-byte dirent and the first 8 bytes of the
123+
// next dirent, whose remaining 24 bytes spill into the following buffer.
124+
let dir = TempDir::new().unwrap();
125+
let expected = create_files(&dir);
126+
let (total, names) = read_dir(&dir, &[40; NUM_FILES]);
127+
assert_eq!(total, NUM_FILES * DIRENT_SIZE);
128+
assert_eq!(names, expected);
129+
}
130+
131+
#[test]
132+
fn read_dir_stops_when_buffers_full_test() {
133+
let dir = TempDir::new().unwrap();
134+
let expected = create_files(&dir);
135+
let (total, names) = read_dir(&dir, &[50, 50, 40]);
136+
assert_eq!(total, 4 * DIRENT_SIZE);
137+
assert_eq!(names.len(), 4);
138+
assert!(names.iter().all(|n| expected.contains(n)));
139+
}

0 commit comments

Comments
 (0)