Alioth Code Coverage

passthrough.rs0.00%

1// Copyright 2025 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
15use std::collections::HashMap;
16use std::ffi::{CStr, OsStr};
17use std::fmt::Debug;
18use std::fs::{
19 File, FileType, Metadata, OpenOptions, ReadDir, read_dir, remove_dir, remove_file, rename,
20};
21use std::io::{IoSlice, IoSliceMut, Read, Seek, SeekFrom, Write};
22use std::iter::{Enumerate, Peekable};
23use std::marker::PhantomData;
24use std::os::fd::AsRawFd;
25use std::os::unix::ffi::OsStrExt;
26use std::os::unix::fs::{DirEntryExt, FileTypeExt, MetadataExt, OpenOptionsExt};
27use std::path::Path;
28
29use zerocopy::{FromBytes, IntoBytes};
30
31use crate::align_up_ty;
32use crate::fuse::bindings::{
33 FUSE_KERNEL_MINOR_VERSION, FUSE_KERNEL_VERSION, FUSE_ROOT_ID, FuseAttr, FuseAttrOut,
34 FuseCreateIn, FuseCreateOut, FuseDirent, FuseDirentType, FuseEntryOut, FuseFlushIn,
35 FuseForgetIn, FuseGetattrFlag, FuseGetattrIn, FuseInHeader, FuseInitIn, FuseInitOut,
36 FuseOpcode, FuseOpenIn, FuseOpenOut, FuseReadIn, FuseReleaseIn, FuseRemovemappingIn,
37 FuseRemovemappingOne, FuseRename2In, FuseRenameIn, FuseSetupmappingFlag, FuseSetupmappingIn,
38 FuseSyncfsIn, FuseWriteIn, FuseWriteOut, RenameFlag,
39};
40use crate::fuse::{DaxRegion, Fuse, Result, error};
41
42const MAX_BUFFER_SIZE: u32 = 1 << 20;
43
44fn fuse_dir_type(e: FileType) -> FuseDirentType {
45 if e.is_dir() {
46 FuseDirentType::DIR
47 } else if e.is_file() {
48 FuseDirentType::REG
49 } else if e.is_symlink() {
50 FuseDirentType::LNK
51 } else if e.is_socket() {
52 FuseDirentType::SOCK
53 } else if e.is_file() {
54 FuseDirentType::FIFO
55 } else if e.is_char_device() {
56 FuseDirentType::CHR
57 } else if e.is_block_device() {
58 FuseDirentType::BLK
59 } else {
60 FuseDirentType::UNKNOWN
61 }
62}
63
64fn convert_o_flags(flags: i32) -> Result<OpenOptions> {
65 let mut opts = OpenOptions::new();
66 match flags & libc::O_ACCMODE {
67 libc::O_RDONLY => opts.read(true),
68 libc::O_WRONLY => opts.write(true),
69 libc::O_RDWR => opts.read(true).write(true),
70 mode => return error::InvalidAccMode { mode }.fail(),
71 };
72 opts.append(flags & libc::O_APPEND == libc::O_APPEND);
73 opts.truncate(flags & libc::O_TRUNC == libc::O_TRUNC);
74 opts.create(flags & libc::O_CREAT == libc::O_CREAT);
75 opts.create_new(flags & (libc::O_CREAT | libc::O_EXCL) == libc::O_CREAT | libc::O_EXCL);
76 let all = libc::O_ACCMODE | libc::O_APPEND | libc::O_TRUNC | libc::O_CREAT | libc::O_EXCL;
77 opts.custom_flags(flags & !all);
78 Ok(opts)
79}
80
81#[derive(Debug)]
82enum Handle {
83 ReadDir(Box<Peekable<Enumerate<ReadDir>>>),
84 File(File),
85}
86
87impl Handle {
88 fn fh(&self) -> u64 {
89 match self {
90 Handle::File(f) => f.as_raw_fd() as u64,
91 Handle::ReadDir(rd) => rd.as_ref() as *const Peekable<_> as u64,
92 }
93 }
94}
95
96#[derive(Debug)]
97struct Node {
98 lookup_count: u64,
99 path: Box<Path>,
100 handle: Option<Handle>,
101}
102
103#[derive(Debug)]
104pub struct Passthrough {
105 nodes: HashMap<u64, Node>,
106 dax_region: Option<Box<dyn DaxRegion>>,
107}
108
109impl Passthrough {
110 pub fn new(path: Box<Path>) -> Result<Self> {
111 let node = Node {
112 lookup_count: 1,
113 path,
114 handle: None,
115 };
116 let nodes = HashMap::from([(FUSE_ROOT_ID, node)]);
117 Ok(Passthrough {
118 nodes,
119 dax_region: None,
120 })
121 }
122
123 fn get_node(&self, id: u64) -> Result<&Node> {
124 match self.nodes.get(&id) {
125 Some(node) => Ok(node),
126 None => error::NodeId { id }.fail(),
127 }
128 }
129
130 fn get_node_mut(&mut self, id: u64) -> Result<&mut Node> {
131 match self.nodes.get_mut(&id) {
132 Some(node) => Ok(node),
133 None => error::NodeId { id }.fail(),
134 }
135 }
136
137 fn join_path(&self, parent: u64, name: &[u8]) -> Result<Box<Path>> {
138 let parent = self.get_node(parent)?;
139 let p = OsStr::from_bytes(CStr::from_bytes_until_nul(name)?.to_bytes());
140 Ok(parent.path.join(p).into_boxed_path())
141 }
142
143 fn convert_meta(&self, meta: &Metadata) -> FuseAttr {
144 FuseAttr {
145 ino: meta.ino(),
146 size: meta.size(),
147 blocks: meta.blocks(),
148 atime: meta.atime() as _,
149 mtime: meta.mtime() as _,
150 ctime: meta.ctime() as _,
151 atimensec: meta.atime_nsec() as _,
152 mtimensec: meta.mtime_nsec() as _,
153 ctimensec: meta.ctime_nsec() as _,
154 mode: meta.mode(),
155 nlink: meta.nlink() as _,
156 uid: meta.uid(),
157 gid: meta.gid(),
158 rdev: meta.rdev() as _,
159 blksize: meta.blksize() as _,
160 flags: 0,
161 }
162 }
163}
164
165impl Fuse for Passthrough {
166 fn init(&mut self, _hdr: &FuseInHeader, in_: &FuseInitIn) -> Result<FuseInitOut> {
167 Ok(FuseInitOut {
168 major: FUSE_KERNEL_VERSION,
169 minor: FUSE_KERNEL_MINOR_VERSION,
170 max_readahead: in_.max_readahead,
171 flags: 0,
172 max_background: u16::MAX,
173 congestion_threshold: (u16::MAX / 4) * 3,
174 max_write: MAX_BUFFER_SIZE,
175 time_gran: 1,
176 max_pages: 256,
177 map_alignment: 0,
178 flags2: 0,
179 ..Default::default()
180 })
181 }
182
183 fn set_dax_region(&mut self, dax_window: Box<dyn DaxRegion>) {
184 self.dax_region = Some(dax_window)
185 }
186
187 fn get_attr(&mut self, hdr: &FuseInHeader, in_: &FuseGetattrIn) -> Result<FuseAttrOut> {
188 let node = self.get_node(hdr.nodeid)?;
189 log::trace!("get_attr: {in_:?} {:?}", node.path);
190
191 let flag = FuseGetattrFlag::from_bits_retain(in_.getattr_flags);
192 if flag.contains(FuseGetattrFlag::FH) {
193 let Some(handle) = &node.handle else {
194 return error::InvalidFileHandle.fail();
195 };
196 let fh = handle.fh();
197 if in_.fh != fh {
198 return error::InvalidFileHandle.fail();
199 }
200 }
201
202 let file = OpenOptions::new()
203 .read(true)
204 .custom_flags(libc::O_NOFOLLOW)
205 .open(&node.path)?;
206 let meta = file.metadata()?;
207 Ok(FuseAttrOut {
208 attr_valid: 1,
209 attr_valid_nsec: 0,
210 attr: self.convert_meta(&meta),
211 dummy: 0,
212 })
213 }
214
215 fn open_dir(&mut self, hdr: &FuseInHeader, in_: &FuseOpenIn) -> Result<FuseOpenOut> {
216 let node = self.get_node_mut(hdr.nodeid)?;
217 log::trace!("open_dir: {in_:?} {:?}", node.path);
218 let handle = Handle::ReadDir(Box::new(read_dir(&node.path)?.enumerate().peekable()));
219 let fh = handle.fh();
220 node.handle = Some(handle);
221 Ok(FuseOpenOut {
222 fh,
223 open_flags: 0,
224 backing_id: 0,
225 })
226 }
227
228 fn read_dir(
229 &mut self,
230 hdr: &FuseInHeader,
231 in_: &FuseReadIn,
232 mut buf: &mut [u8],
233 ) -> Result<usize> {
234 let node = self.get_node_mut(hdr.nodeid)?;
235 log::trace!("read_dir: {:?}", node.path);
236
237 let Some(Handle::ReadDir(read_dir)) = &mut node.handle else {
238 return error::DirNotOpened.fail();
239 };
240 let Some((index, _)) = read_dir.peek() else {
241 return Ok(0);
242 };
243 if *index as u64 != in_.offset {
244 todo!("in_offset = {}, != {}", in_.offset, *index);
245 }
246
247 let mut total_len = 0;
248
249 while let Some((index, entry)) = read_dir.peek() {
250 let e = entry.as_ref()?;
251 let name = e.file_name();
252 let namelen = name.len();
253
254 let dir_entry = FuseDirent {
255 ino: e.ino(),
256 off: *index as u64 + 1,
257 namelen: namelen as _,
258 type_: fuse_dir_type(e.file_type()?),
259 name: PhantomData,
260 };
261 let aligned_namelen = align_up_ty!(namelen, FuseDirent);
262 let len = size_of_val(&dir_entry) + aligned_namelen;
263 let Some((p1, p2)) = buf.split_at_mut_checked(len) else {
264 break;
265 };
266 let (b_entry, b_name) = p1.split_at_mut(size_of_val(&dir_entry));
267 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;
272 total_len += len;
273 read_dir.next();
274 }
275
276 Ok(total_len)
277 }
278
279 fn release_dir(&mut self, hdr: &FuseInHeader, in_: &FuseReleaseIn) -> Result<()> {
280 let node = self.get_node_mut(hdr.nodeid)?;
281 node.handle = None;
282 log::trace!("release_dir: {in_:?} {:?}", node.path);
283 Ok(())
284 }
285
286 fn release(&mut self, hdr: &FuseInHeader, in_: &FuseReleaseIn) -> Result<()> {
287 let node = self.get_node_mut(hdr.nodeid)?;
288 node.handle = None;
289 log::trace!("release: {in_:?} {:?}", node.path);
290 Ok(())
291 }
292
293 fn lookup(&mut self, hdr: &FuseInHeader, in_: &[u8]) -> Result<FuseEntryOut> {
294 let path = self.join_path(hdr.nodeid, in_)?;
295
296 log::trace!("lookup: {path:?}");
297 let file = OpenOptions::new()
298 .read(true)
299 .custom_flags(libc::O_NOFOLLOW)
300 .open(&path)?;
301 let meta = file.metadata()?;
302 let nodeid =
303 if let Some((nodeid, node)) = self.nodes.iter_mut().find(|(_, n)| n.path == path) {
304 node.lookup_count += 1;
305 *nodeid
306 } else {
307 let nodeid = path.as_os_str().as_bytes().as_ptr() as u64;
308 let node = Node {
309 lookup_count: 1,
310 path,
311 handle: None,
312 };
313 self.nodes.insert(nodeid, node);
314 nodeid
315 };
316 Ok(FuseEntryOut {
317 nodeid,
318 generation: 0,
319 entry_valid: 0,
320 attr_valid: 0,
321 entry_valid_nsec: 0,
322 attr_valid_nsec: 0,
323 attr: self.convert_meta(&meta),
324 })
325 }
326
327 fn forget(&mut self, hdr: &FuseInHeader, in_: &FuseForgetIn) -> Result<()> {
328 let node = self.get_node_mut(hdr.nodeid)?;
329 log::trace!(
330 "forget: {:?}, ref_count {}, remove {}",
331 node.path,
332 node.lookup_count,
333 in_.nlookup
334 );
335 node.lookup_count -= in_.nlookup;
336 if node.lookup_count == 0 {
337 self.nodes.remove(&hdr.nodeid);
338 }
339 Ok(())
340 }
341
342 fn open(&mut self, hdr: &FuseInHeader, in_: &FuseOpenIn) -> Result<FuseOpenOut> {
343 let node = self.get_node_mut(hdr.nodeid)?;
344 log::trace!("open: {:?} {in_:?}", node.path);
345 let opts = convert_o_flags(in_.flags as i32)?;
346 let handle = Handle::File(opts.open(&node.path)?);
347 let fh = handle.fh();
348 node.handle = Some(handle);
349 Ok(FuseOpenOut {
350 fh,
351 open_flags: 0,
352 backing_id: 0,
353 })
354 }
355
356 fn read(
357 &mut self,
358 hdr: &FuseInHeader,
359 in_: &FuseReadIn,
360 iov: &mut [IoSliceMut],
361 ) -> Result<usize> {
362 let node = self.get_node(hdr.nodeid)?;
363 log::trace!("read: {hdr:?} {in_:?} {:?}", node.path);
364 let Some(Handle::File(f)) = &node.handle else {
365 return error::FileNotOpened.fail();
366 };
367 let mut file = f;
368 // TODO: use `read_vectored_at`
369 // https://github.com/rust-lang/rust/issues/89517
370 file.seek(SeekFrom::Start(in_.offset))?;
371 let size = file.read_vectored(iov)?;
372 Ok(size)
373 }
374
375 fn flush(&mut self, hdr: &FuseInHeader, in_: &FuseFlushIn) -> Result<()> {
376 log::error!("flush: {hdr:?} {in_:?}");
377 Ok(())
378 }
379
380 fn syncfs(&mut self, hdr: &FuseInHeader, in_: &FuseSyncfsIn) -> Result<()> {
381 log::error!("syncfs: {hdr:?} {in_:?}");
382 Ok(())
383 }
384
385 fn create(
386 &mut self,
387 hdr: &FuseInHeader,
388 in_: &FuseCreateIn,
389 buf: &[u8],
390 ) -> Result<FuseCreateOut> {
391 let opts = convert_o_flags(in_.flags as i32)?;
392 let path = self.join_path(hdr.nodeid, buf)?;
393 let nodeid = path.as_os_str().as_bytes().as_ptr() as u64;
394 let f = opts.open(&path)?;
395 let meta = f.metadata()?;
396 let handle = Handle::File(f);
397 let fh = handle.fh();
398 let node = Node {
399 lookup_count: 1,
400 path,
401 handle: Some(handle),
402 };
403 self.nodes.insert(nodeid, node);
404 Ok(FuseCreateOut {
405 entry: FuseEntryOut {
406 nodeid,
407 generation: 0,
408 entry_valid: 0,
409 attr_valid: 0,
410 entry_valid_nsec: 0,
411 attr_valid_nsec: 0,
412 attr: self.convert_meta(&meta),
413 },
414 open: FuseOpenOut {
415 fh,
416 open_flags: 0,
417 backing_id: 0,
418 },
419 })
420 }
421
422 fn write(
423 &mut self,
424 hdr: &FuseInHeader,
425 in_: &FuseWriteIn,
426 buf: &[IoSlice],
427 ) -> Result<FuseWriteOut> {
428 let node = self.get_node(hdr.nodeid)?;
429 let Some(Handle::File(f)) = &node.handle else {
430 return error::FileNotOpened.fail();
431 };
432 let mut file = f;
433 // TODO: use `write_vectored_at`
434 // https://github.com/rust-lang/rust/issues/89517
435 file.seek(SeekFrom::Start(in_.offset))?;
436 let size = file.write_vectored(buf)? as u32;
437 Ok(FuseWriteOut { size, padding: 0 })
438 }
439
440 fn unlink(&mut self, hdr: &FuseInHeader, in_: &[u8]) -> Result<()> {
441 let path = self.join_path(hdr.nodeid, in_)?;
442 remove_file(&path)?;
443 log::trace!("unlink: {path:?}");
444 Ok(())
445 }
446
447 fn rmdir(&mut self, hdr: &FuseInHeader, in_: &[u8]) -> Result<()> {
448 let path = self.join_path(hdr.nodeid, in_)?;
449 remove_dir(path)?;
450 Ok(())
451 }
452
453 fn rename(&mut self, hdr: &FuseInHeader, in_: &FuseRenameIn, buf: &[u8]) -> Result<()> {
454 let in2 = FuseRename2In {
455 newdir: in_.newdir,
456 flags: 0,
457 padding: 0,
458 };
459 self.rename2(hdr, &in2, buf)
460 }
461
462 fn rename2(&mut self, hdr: &FuseInHeader, in_: &FuseRename2In, buf: &[u8]) -> Result<()> {
463 // TODO: use split_once
464 // https://github.com/rust-lang/rust/issues/112811
465 let mut paths = buf.split_inclusive(|b| *b == b'\0');
466 let Some(p1) = paths.next() else {
467 return Err(std::io::Error::from_raw_os_error(libc::EINVAL))?;
468 };
469 let Some(p2) = paths.next() else {
470 return Err(std::io::Error::from_raw_os_error(libc::EINVAL))?;
471 };
472 let flag = RenameFlag::from_bits_retain(in_.flags);
473 if !flag.is_empty() {
474 return error::Unsupported {
475 op: FuseOpcode::RENAME2,
476 flag: flag.bits(),
477 }
478 .fail();
479 }
480 let src = self.join_path(hdr.nodeid, p1)?;
481 let dst = self.join_path(in_.newdir, p2)?;
482 rename(src, dst)?;
483 Ok(())
484 }
485
486 fn setup_mapping(&mut self, hdr: &FuseInHeader, in_: &FuseSetupmappingIn) -> Result<()> {
487 let Some(dax_region) = &self.dax_region else {
488 return Err(std::io::Error::from_raw_os_error(libc::ENOSYS))?;
489 };
490
491 let node = self.get_node(hdr.nodeid)?;
492 let Some(Handle::File(fd)) = &node.handle else {
493 return error::FileNotOpened.fail();
494 };
495
496 let flag = FuseSetupmappingFlag::from_bits_retain(in_.flags);
497 dax_region.map(in_.moffset, fd, in_.foffset, in_.len, flag)?;
498
499 log::trace!(
500 "setup_mapping: offset = {:#x}, file = {:?}",
501 in_.moffset,
502 node.path
503 );
504
505 Ok(())
506 }
507
508 fn remove_mapping(&mut self, _hdr: &FuseInHeader, in_: &[u8]) -> Result<()> {
509 let Some(dax_region) = &self.dax_region else {
510 return Err(std::io::Error::from_raw_os_error(libc::ENOSYS))?;
511 };
512
513 let Ok((h, mut remain)) = FuseRemovemappingIn::ref_from_prefix(in_) else {
514 return Err(std::io::Error::from_raw_os_error(libc::EINVAL))?;
515 };
516
517 for _ in 0..h.count {
518 let Ok((one, r)) = FuseRemovemappingOne::read_from_prefix(remain) else {
519 return Err(std::io::Error::from_raw_os_error(libc::EINVAL))?;
520 };
521 dax_region.unmap(one.moffset, one.len)?;
522 log::trace!(
523 "remove_mapping: offset = {:#x}, size = {:#x}",
524 one.moffset,
525 one.len
526 );
527
528 remain = r;
529 }
530
531 Ok(())
532 }
533}
534