1#![allow(
11 clippy::cast_sign_loss,
12 clippy::cast_possible_wrap,
13 clippy::cast_possible_truncation,
14 clippy::ptr_as_ptr,
15 clippy::borrow_as_ptr,
16 clippy::ref_as_ptr,
20 clippy::unnecessary_cast,
21 clippy::significant_drop_tightening,
22 clippy::needless_pass_by_ref_mut
23)]
24
25use crate::error::{FsError, Result};
26use crate::fuse::{
27 FATTR_ATIME, FATTR_GID, FATTR_MODE, FATTR_MTIME, FATTR_SIZE, FATTR_UID, FUSE_NO_FH,
28 FuseAccessIn, FuseAttr, FuseAttrOut, FuseCreateIn, FuseDirent, FuseEntryOut, FuseFallocateIn,
29 FuseFlushIn, FuseForgetIn, FuseFsyncIn, FuseGetxattrIn, FuseGetxattrOut, FuseInHeader,
30 FuseLinkIn, FuseLseekIn, FuseLseekOut, FuseMkdirIn, FuseMknodIn, FuseOpcode, FuseOpenIn,
31 FuseOpenOut, FuseOutHeader, FuseReadIn, FuseReleaseIn, FuseRenameIn, FuseSetattrIn,
32 FuseSetxattrIn, FuseStatfsOut, FuseWriteIn, FuseWriteOut,
33};
34use crate::passthrough::PassthroughFs;
35
36use std::ffi::OsStr;
37use std::mem::size_of;
38use std::os::unix::ffi::OsStrExt;
39use std::path::Path;
40use std::sync::Arc;
41
42pub trait DaxFsExt {
48 fn open_inode_for_dax(&self, inode: u64, writable: bool) -> std::io::Result<std::fs::File>;
63}
64
65impl DaxFsExt for PassthroughFs {
66 fn open_inode_for_dax(&self, inode: u64, writable: bool) -> std::io::Result<std::fs::File> {
67 use std::os::unix::fs::MetadataExt;
68
69 let registered_ino = self.kernel_ino_for(inode).ok_or_else(|| {
75 std::io::Error::new(
76 std::io::ErrorKind::NotFound,
77 format!("inode {inode} not found in passthrough table"),
78 )
79 })?;
80
81 let path = self
82 .inode_path(inode)
83 .map_err(|e| std::io::Error::new(std::io::ErrorKind::NotFound, e.to_string()))?;
84
85 let file = std::fs::OpenOptions::new()
89 .read(true)
90 .write(writable)
91 .open(&path)?;
92
93 let fd_ino = file.metadata()?.ino();
99 if fd_ino != registered_ino {
100 tracing::warn!(
101 inode,
102 path = %path.display(),
103 fd_ino,
104 registered_ino,
105 "TOCTOU mismatch: inode was swapped after registration; rejecting DAX mapping"
106 );
107 return Err(std::io::Error::from_raw_os_error(libc::EIO));
108 }
109
110 Ok(file)
111 }
112}
113
114#[derive(Debug, Clone)]
120pub struct DispatcherConfig {
121 pub entry_timeout: u64,
123 pub attr_timeout: u64,
125}
126
127impl Default for DispatcherConfig {
128 fn default() -> Self {
129 Self {
130 entry_timeout: 10,
131 attr_timeout: 10,
132 }
133 }
134}
135
136#[derive(Debug, Clone, Copy)]
142pub struct RequestContext {
143 pub unique: u64,
145 pub nodeid: u64,
147 pub uid: u32,
149 pub gid: u32,
151 pub pid: u32,
153}
154
155impl From<&FuseInHeader> for RequestContext {
156 fn from(header: &FuseInHeader) -> Self {
157 Self {
158 unique: header.unique,
159 nodeid: header.nodeid,
160 uid: header.uid,
161 gid: header.gid,
162 pid: header.pid,
163 }
164 }
165}
166
167pub struct ResponseBuilder {
173 buffer: Vec<u8>,
174}
175
176impl ResponseBuilder {
177 #[must_use]
179 pub fn new() -> Self {
180 Self {
181 buffer: Vec::with_capacity(4096),
182 }
183 }
184
185 pub fn write_error(&mut self, unique: u64, errno: i32) {
187 if errno != 0 {
188 tracing::debug!("FUSE: error response unique={} errno={}", unique, -errno);
189 }
190 self.buffer.clear();
191 let header = FuseOutHeader::error(unique, errno);
192 self.write_struct(&header);
193 }
194
195 pub fn write_empty(&mut self, unique: u64) {
197 self.buffer.clear();
198 let header = FuseOutHeader::success(unique, FuseOutHeader::SIZE as u32);
199 self.write_struct(&header);
200 }
201
202 pub fn write_data<T: Copy>(&mut self, unique: u64, data: &T) {
204 self.buffer.clear();
205 let len = (FuseOutHeader::SIZE + size_of::<T>()) as u32;
206 let header = FuseOutHeader::success(unique, len);
207 self.write_struct(&header);
208 self.write_struct(data);
209 }
210
211 pub fn write_bytes(&mut self, unique: u64, data: &[u8]) {
213 self.buffer.clear();
214 let len = (FuseOutHeader::SIZE + data.len()) as u32;
215 let header = FuseOutHeader::success(unique, len);
216 self.write_struct(&header);
217 self.buffer.extend_from_slice(data);
218 }
219
220 #[must_use]
222 pub fn finish(self) -> Vec<u8> {
223 self.buffer
224 }
225
226 #[must_use]
228 pub fn as_bytes(&self) -> &[u8] {
229 &self.buffer
230 }
231
232 fn write_struct<T: Copy>(&mut self, value: &T) {
233 let bytes = unsafe {
234 std::slice::from_raw_parts(std::ptr::from_ref::<T>(value) as *const u8, size_of::<T>())
235 };
236 self.buffer.extend_from_slice(bytes);
237 }
238}
239
240impl Default for ResponseBuilder {
241 fn default() -> Self {
242 Self::new()
243 }
244}
245
246pub struct FuseDispatcher {
270 fs: Arc<PassthroughFs>,
272 config: DispatcherConfig,
274 dax_mapper: Option<Arc<dyn crate::DaxMapper>>,
276}
277
278impl FuseDispatcher {
279 #[must_use]
281 pub fn new(fs: Arc<PassthroughFs>, config: DispatcherConfig) -> Self {
282 Self {
283 fs,
284 config,
285 dax_mapper: None,
286 }
287 }
288
289 pub fn set_dax_mapper(&mut self, mapper: Arc<dyn crate::DaxMapper>) {
291 self.dax_mapper = Some(mapper);
292 }
293
294 pub fn dispatch(&self, request: &[u8]) -> Result<Vec<u8>> {
300 if request.len() < FuseInHeader::SIZE {
302 return Err(FsError::Fuse("request too small".to_string()));
303 }
304
305 let header = unsafe { std::ptr::read_unaligned(request.as_ptr() as *const FuseInHeader) };
308 let body = &request[FuseInHeader::SIZE..];
309
310 let opcode = FuseOpcode::from_u32(header.opcode)
312 .ok_or_else(|| FsError::Fuse(format!("unknown opcode: {}", header.opcode)))?;
313
314 let ctx = RequestContext::from(&header);
315 let mut response = ResponseBuilder::new();
316
317 tracing::debug!(
318 "FUSE: {:?} nodeid={} unique={}",
319 opcode,
320 ctx.nodeid,
321 ctx.unique
322 );
323
324 match opcode {
326 FuseOpcode::Init => response.write_error(ctx.unique, libc::ENOSYS),
330 FuseOpcode::Destroy => self.handle_destroy(&ctx, &mut response),
331 FuseOpcode::Lookup => self.handle_lookup(&ctx, body, &mut response),
332 FuseOpcode::Forget => self.handle_forget(&ctx, body, &mut response),
333 FuseOpcode::Getattr => self.handle_getattr(&ctx, body, &mut response),
334 FuseOpcode::Setattr => self.handle_setattr(&ctx, body, &mut response),
335 FuseOpcode::Readlink => self.handle_readlink(&ctx, &mut response),
336 FuseOpcode::Mknod => self.handle_mknod(&ctx, body, &mut response),
337 FuseOpcode::Mkdir => self.handle_mkdir(&ctx, body, &mut response),
338 FuseOpcode::Unlink => self.handle_unlink(&ctx, body, &mut response),
339 FuseOpcode::Rmdir => self.handle_rmdir(&ctx, body, &mut response),
340 FuseOpcode::Symlink => self.handle_symlink(&ctx, body, &mut response),
341 FuseOpcode::Rename => self.handle_rename(&ctx, body, &mut response),
342 FuseOpcode::Link => self.handle_link(&ctx, body, &mut response),
343 FuseOpcode::Open => self.handle_open(&ctx, body, &mut response),
344 FuseOpcode::Read => self.handle_read(&ctx, body, &mut response),
345 FuseOpcode::Write => self.handle_write(&ctx, body, &mut response),
346 FuseOpcode::Statfs => self.handle_statfs(&ctx, &mut response),
347 FuseOpcode::Release => self.handle_release(&ctx, body, &mut response),
348 FuseOpcode::Fsync => self.handle_fsync(&ctx, body, &mut response),
349 FuseOpcode::Flush => self.handle_flush(&ctx, body, &mut response),
350 FuseOpcode::Opendir => self.handle_opendir(&ctx, body, &mut response),
351 FuseOpcode::Readdir => self.handle_readdir(&ctx, body, &mut response),
352 FuseOpcode::Readdirplus => self.handle_readdirplus(&ctx, body, &mut response),
353 FuseOpcode::Releasedir => self.handle_releasedir(&ctx, body, &mut response),
354 FuseOpcode::Fsyncdir => self.handle_fsyncdir(&ctx, body, &mut response),
355 FuseOpcode::Access => self.handle_access(&ctx, body, &mut response),
356 FuseOpcode::Create => self.handle_create(&ctx, body, &mut response),
357 FuseOpcode::Getxattr => self.handle_getxattr(&ctx, body, &mut response),
358 FuseOpcode::Setxattr => self.handle_setxattr(&ctx, body, &mut response),
359 FuseOpcode::Removexattr => self.handle_removexattr(&ctx, body, &mut response),
360 FuseOpcode::Lseek => self.handle_lseek(&ctx, body, &mut response),
361 FuseOpcode::Fallocate => self.handle_fallocate(&ctx, body, &mut response),
362 FuseOpcode::SetupMapping => self.handle_setup_mapping(&ctx, body, &mut response),
363 FuseOpcode::RemoveMapping => self.handle_remove_mapping(&ctx, body, &mut response),
364 _ => {
365 tracing::warn!(
366 "FUSE: unsupported opcode {:?} ({}), returning ENOSYS",
367 opcode,
368 header.opcode
369 );
370 response.write_error(ctx.unique, libc::ENOSYS);
371 }
372 }
373
374 Ok(response.finish())
375 }
376
377 fn handle_destroy(&self, ctx: &RequestContext, response: &mut ResponseBuilder) {
382 response.write_empty(ctx.unique);
383 }
384
385 fn handle_lookup(&self, ctx: &RequestContext, body: &[u8], response: &mut ResponseBuilder) {
390 let name = self.parse_name(body);
392
393 match self.fs.lookup(ctx.nodeid, name) {
394 Ok((inode, attr)) => {
395 let entry = self.make_entry_out(inode, &attr);
396 response.write_data(ctx.unique, &entry);
397 }
398 Err(e) => response.write_error(ctx.unique, e.to_errno()),
399 }
400 }
401
402 fn handle_forget(&self, ctx: &RequestContext, body: &[u8], response: &mut ResponseBuilder) {
403 if body.len() >= size_of::<FuseForgetIn>() {
404 let forget_in =
406 unsafe { std::ptr::read_unaligned(body.as_ptr() as *const FuseForgetIn) };
407 self.fs.forget(ctx.nodeid, forget_in.nlookup);
408 }
409 let _ = response;
411 }
412
413 fn handle_getattr(&self, ctx: &RequestContext, _body: &[u8], response: &mut ResponseBuilder) {
418 match self.fs.getattr(ctx.nodeid) {
419 Ok(attr) => {
420 let attr_out = FuseAttrOut {
421 attr_valid: self.config.attr_timeout,
422 attr_valid_nsec: 0,
423 dummy: 0,
424 attr,
425 };
426 response.write_data(ctx.unique, &attr_out);
427 }
428 Err(e) => response.write_error(ctx.unique, e.to_errno()),
429 }
430 }
431
432 fn handle_setattr(&self, ctx: &RequestContext, body: &[u8], response: &mut ResponseBuilder) {
433 if body.len() < size_of::<FuseSetattrIn>() {
434 response.write_error(ctx.unique, libc::EINVAL);
435 return;
436 }
437
438 let setattr_in = unsafe { std::ptr::read_unaligned(body.as_ptr() as *const FuseSetattrIn) };
440
441 let mode = if setattr_in.valid & FATTR_MODE != 0 {
442 Some(setattr_in.mode)
443 } else {
444 None
445 };
446
447 let uid = if setattr_in.valid & FATTR_UID != 0 {
448 Some(setattr_in.uid)
449 } else {
450 None
451 };
452
453 let gid = if setattr_in.valid & FATTR_GID != 0 {
454 Some(setattr_in.gid)
455 } else {
456 None
457 };
458
459 let size = if setattr_in.valid & FATTR_SIZE != 0 {
460 Some(setattr_in.size)
461 } else {
462 None
463 };
464
465 let atime = if setattr_in.valid & FATTR_ATIME != 0 {
466 Some((setattr_in.atime as i64, setattr_in.atimensec))
467 } else {
468 None
469 };
470
471 let mtime = if setattr_in.valid & FATTR_MTIME != 0 {
472 Some((setattr_in.mtime as i64, setattr_in.mtimensec))
473 } else {
474 None
475 };
476
477 match self
478 .fs
479 .setattr(ctx.nodeid, mode, uid, gid, size, atime, mtime)
480 {
481 Ok(attr) => {
482 let attr_out = FuseAttrOut {
483 attr_valid: self.config.attr_timeout,
484 attr_valid_nsec: 0,
485 dummy: 0,
486 attr,
487 };
488 response.write_data(ctx.unique, &attr_out);
489 }
490 Err(e) => response.write_error(ctx.unique, e.to_errno()),
491 }
492 }
493
494 fn handle_readlink(&self, ctx: &RequestContext, response: &mut ResponseBuilder) {
495 match self.fs.readlink(ctx.nodeid) {
496 Ok(target) => {
497 response.write_bytes(ctx.unique, target.as_os_str().as_bytes());
498 }
499 Err(e) => response.write_error(ctx.unique, e.to_errno()),
500 }
501 }
502
503 fn handle_mknod(&self, ctx: &RequestContext, body: &[u8], response: &mut ResponseBuilder) {
508 if body.len() < size_of::<FuseMknodIn>() {
509 response.write_error(ctx.unique, libc::EINVAL);
510 return;
511 }
512
513 let mknod_in = unsafe { std::ptr::read_unaligned(body.as_ptr() as *const FuseMknodIn) };
515 let name = self.parse_name(&body[size_of::<FuseMknodIn>()..]);
516
517 match self
518 .fs
519 .mknod(ctx.nodeid, name, mknod_in.mode, u64::from(mknod_in.rdev))
520 {
521 Ok((inode, attr)) => {
522 let entry = self.make_entry_out(inode, &attr);
523 response.write_data(ctx.unique, &entry);
524 }
525 Err(e) => response.write_error(ctx.unique, e.to_errno()),
526 }
527 }
528
529 fn handle_mkdir(&self, ctx: &RequestContext, body: &[u8], response: &mut ResponseBuilder) {
530 if body.len() < size_of::<FuseMkdirIn>() {
531 response.write_error(ctx.unique, libc::EINVAL);
532 return;
533 }
534
535 let mkdir_in = unsafe { std::ptr::read_unaligned(body.as_ptr() as *const FuseMkdirIn) };
537 let name = self.parse_name(&body[size_of::<FuseMkdirIn>()..]);
538
539 match self.fs.mkdir(ctx.nodeid, name, mkdir_in.mode) {
540 Ok((inode, attr)) => {
541 let entry = self.make_entry_out(inode, &attr);
542 response.write_data(ctx.unique, &entry);
543 }
544 Err(e) => response.write_error(ctx.unique, e.to_errno()),
545 }
546 }
547
548 fn handle_symlink(&self, ctx: &RequestContext, body: &[u8], response: &mut ResponseBuilder) {
549 let parts: Vec<&[u8]> = body.splitn(2, |&b| b == 0).collect();
551 if parts.len() < 2 {
552 response.write_error(ctx.unique, libc::EINVAL);
553 return;
554 }
555
556 let name = OsStr::from_bytes(parts[0]);
557 let link_target = Path::new(OsStr::from_bytes(
558 parts[1].split(|&b| b == 0).next().unwrap_or(&[]),
559 ));
560
561 match self.fs.symlink(ctx.nodeid, name, link_target) {
562 Ok((inode, attr)) => {
563 let entry = self.make_entry_out(inode, &attr);
564 response.write_data(ctx.unique, &entry);
565 }
566 Err(e) => response.write_error(ctx.unique, e.to_errno()),
567 }
568 }
569
570 fn handle_link(&self, ctx: &RequestContext, body: &[u8], response: &mut ResponseBuilder) {
571 if body.len() < size_of::<FuseLinkIn>() {
572 response.write_error(ctx.unique, libc::EINVAL);
573 return;
574 }
575
576 let link_in = unsafe { std::ptr::read_unaligned(body.as_ptr() as *const FuseLinkIn) };
578 let name = self.parse_name(&body[size_of::<FuseLinkIn>()..]);
579
580 match self.fs.link(link_in.oldnodeid, ctx.nodeid, name) {
581 Ok((inode, attr)) => {
582 let entry = self.make_entry_out(inode, &attr);
583 response.write_data(ctx.unique, &entry);
584 }
585 Err(e) => response.write_error(ctx.unique, e.to_errno()),
586 }
587 }
588
589 fn handle_create(&self, ctx: &RequestContext, body: &[u8], response: &mut ResponseBuilder) {
590 if body.len() < size_of::<FuseCreateIn>() {
591 response.write_error(ctx.unique, libc::EINVAL);
592 return;
593 }
594
595 let create_in = unsafe { std::ptr::read_unaligned(body.as_ptr() as *const FuseCreateIn) };
597 let name = self.parse_name(&body[size_of::<FuseCreateIn>()..]);
598
599 match self
600 .fs
601 .create(ctx.nodeid, name, create_in.mode, create_in.flags)
602 {
603 Ok((inode, attr, handle)) => {
604 let entry = self.make_entry_out(inode, &attr);
605 let open_out = FuseOpenOut {
606 fh: handle,
607 open_flags: 0,
608 padding: 0,
609 };
610
611 response.buffer.clear();
613 let len = (FuseOutHeader::SIZE
614 + size_of::<FuseEntryOut>()
615 + size_of::<FuseOpenOut>()) as u32;
616 let header = FuseOutHeader::success(ctx.unique, len);
617 response.write_struct(&header);
618 response.write_struct(&entry);
619 response.write_struct(&open_out);
620 }
621 Err(e) => response.write_error(ctx.unique, e.to_errno()),
622 }
623 }
624
625 fn handle_unlink(&self, ctx: &RequestContext, body: &[u8], response: &mut ResponseBuilder) {
630 let name = self.parse_name(body);
631
632 match self.fs.unlink(ctx.nodeid, name) {
633 Ok(()) => response.write_empty(ctx.unique),
634 Err(e) => response.write_error(ctx.unique, e.to_errno()),
635 }
636 }
637
638 fn handle_rmdir(&self, ctx: &RequestContext, body: &[u8], response: &mut ResponseBuilder) {
639 let name = self.parse_name(body);
640
641 match self.fs.rmdir(ctx.nodeid, name) {
642 Ok(()) => response.write_empty(ctx.unique),
643 Err(e) => response.write_error(ctx.unique, e.to_errno()),
644 }
645 }
646
647 fn handle_rename(&self, ctx: &RequestContext, body: &[u8], response: &mut ResponseBuilder) {
648 if body.len() < size_of::<FuseRenameIn>() {
649 response.write_error(ctx.unique, libc::EINVAL);
650 return;
651 }
652
653 let rename_in = unsafe { std::ptr::read_unaligned(body.as_ptr() as *const FuseRenameIn) };
655 let names = &body[size_of::<FuseRenameIn>()..];
656
657 let parts: Vec<&[u8]> = names.splitn(2, |&b| b == 0).collect();
659 if parts.len() < 2 {
660 response.write_error(ctx.unique, libc::EINVAL);
661 return;
662 }
663
664 let old_name = OsStr::from_bytes(parts[0]);
665 let new_name = OsStr::from_bytes(parts[1].split(|&b| b == 0).next().unwrap_or(&[]));
666
667 match self
668 .fs
669 .rename(ctx.nodeid, old_name, rename_in.newdir, new_name, 0)
670 {
671 Ok(()) => response.write_empty(ctx.unique),
672 Err(e) => response.write_error(ctx.unique, e.to_errno()),
673 }
674 }
675
676 fn handle_open(&self, ctx: &RequestContext, body: &[u8], response: &mut ResponseBuilder) {
681 if body.len() < size_of::<FuseOpenIn>() {
682 response.write_error(ctx.unique, libc::EINVAL);
683 return;
684 }
685
686 let open_in = unsafe { std::ptr::read_unaligned(body.as_ptr() as *const FuseOpenIn) };
688
689 match self.fs.open(ctx.nodeid, open_in.flags) {
690 Ok(handle) => {
691 let open_out = FuseOpenOut {
692 fh: handle,
693 open_flags: 0,
694 padding: 0,
695 };
696 response.write_data(ctx.unique, &open_out);
697 }
698 Err(e) => response.write_error(ctx.unique, e.to_errno()),
699 }
700 }
701
702 fn handle_read(&self, ctx: &RequestContext, body: &[u8], response: &mut ResponseBuilder) {
703 if body.len() < size_of::<FuseReadIn>() {
704 response.write_error(ctx.unique, libc::EINVAL);
705 return;
706 }
707
708 let read_in = unsafe { std::ptr::read_unaligned(body.as_ptr() as *const FuseReadIn) };
710
711 match self.fs.read(read_in.fh, read_in.offset, read_in.size) {
712 Ok(data) => response.write_bytes(ctx.unique, &data),
713 Err(e) => response.write_error(ctx.unique, e.to_errno()),
714 }
715 }
716
717 fn handle_write(&self, ctx: &RequestContext, body: &[u8], response: &mut ResponseBuilder) {
718 if body.len() < size_of::<FuseWriteIn>() {
719 response.write_error(ctx.unique, libc::EINVAL);
720 return;
721 }
722
723 let write_in = unsafe { std::ptr::read_unaligned(body.as_ptr() as *const FuseWriteIn) };
725 let data = &body[size_of::<FuseWriteIn>()..];
726
727 match self
728 .fs
729 .write(write_in.fh, write_in.offset, data, write_in.write_flags)
730 {
731 Ok(written) => {
732 let write_out = FuseWriteOut {
733 size: written,
734 padding: 0,
735 };
736 response.write_data(ctx.unique, &write_out);
737 }
738 Err(e) => response.write_error(ctx.unique, e.to_errno()),
739 }
740 }
741
742 fn handle_release(&self, ctx: &RequestContext, body: &[u8], response: &mut ResponseBuilder) {
743 if body.len() < size_of::<FuseReleaseIn>() {
744 response.write_error(ctx.unique, libc::EINVAL);
745 return;
746 }
747
748 let release_in = unsafe { std::ptr::read_unaligned(body.as_ptr() as *const FuseReleaseIn) };
750
751 match self.fs.release(release_in.fh) {
752 Ok(()) => response.write_empty(ctx.unique),
753 Err(e) => response.write_error(ctx.unique, e.to_errno()),
754 }
755 }
756
757 fn handle_flush(&self, ctx: &RequestContext, body: &[u8], response: &mut ResponseBuilder) {
758 if body.len() < size_of::<FuseFlushIn>() {
759 response.write_error(ctx.unique, libc::EINVAL);
760 return;
761 }
762
763 let flush_in = unsafe { std::ptr::read_unaligned(body.as_ptr() as *const FuseFlushIn) };
765
766 match self.fs.flush(flush_in.fh) {
767 Ok(()) => response.write_empty(ctx.unique),
768 Err(e) => response.write_error(ctx.unique, e.to_errno()),
769 }
770 }
771
772 fn handle_fsync(&self, ctx: &RequestContext, body: &[u8], response: &mut ResponseBuilder) {
773 if body.len() < size_of::<FuseFsyncIn>() {
774 response.write_error(ctx.unique, libc::EINVAL);
775 return;
776 }
777
778 let fsync_in = unsafe { std::ptr::read_unaligned(body.as_ptr() as *const FuseFsyncIn) };
780 let datasync = fsync_in.fsync_flags & 1 != 0;
781
782 match self.fs.fsync(fsync_in.fh, datasync) {
783 Ok(()) => response.write_empty(ctx.unique),
784 Err(e) => response.write_error(ctx.unique, e.to_errno()),
785 }
786 }
787
788 fn handle_lseek(&self, ctx: &RequestContext, body: &[u8], response: &mut ResponseBuilder) {
789 if body.len() < size_of::<FuseLseekIn>() {
790 response.write_error(ctx.unique, libc::EINVAL);
791 return;
792 }
793
794 let lseek_in = unsafe { std::ptr::read_unaligned(body.as_ptr() as *const FuseLseekIn) };
796
797 match self
798 .fs
799 .lseek(lseek_in.fh, lseek_in.offset as i64, lseek_in.whence)
800 {
801 Ok(offset) => {
802 let lseek_out = FuseLseekOut { offset };
803 response.write_data(ctx.unique, &lseek_out);
804 }
805 Err(e) => response.write_error(ctx.unique, e.to_errno()),
806 }
807 }
808
809 fn handle_fallocate(&self, ctx: &RequestContext, body: &[u8], response: &mut ResponseBuilder) {
810 if body.len() < size_of::<FuseFallocateIn>() {
811 response.write_error(ctx.unique, libc::EINVAL);
812 return;
813 }
814
815 let fallocate_in =
817 unsafe { std::ptr::read_unaligned(body.as_ptr() as *const FuseFallocateIn) };
818
819 match self.fs.fallocate(
820 fallocate_in.fh,
821 fallocate_in.mode,
822 fallocate_in.offset,
823 fallocate_in.length,
824 ) {
825 Ok(()) => response.write_empty(ctx.unique),
826 Err(e) => response.write_error(ctx.unique, e.to_errno()),
827 }
828 }
829
830 fn handle_setup_mapping(
835 &self,
836 ctx: &RequestContext,
837 body: &[u8],
838 response: &mut ResponseBuilder,
839 ) {
840 use crate::fuse::FuseSetupMappingIn;
841
842 let Some(ref mapper) = self.dax_mapper else {
843 response.write_error(ctx.unique, libc::ENOSYS);
844 return;
845 };
846
847 if body.len() < std::mem::size_of::<FuseSetupMappingIn>() {
848 response.write_error(ctx.unique, libc::EINVAL);
849 return;
850 }
851
852 let req = unsafe { std::ptr::read_unaligned(body.as_ptr().cast::<FuseSetupMappingIn>()) };
853
854 let writable = req.flags & crate::fuse::FUSE_SETUPMAPPING_FLAG_WRITE != 0;
855
856 let result = if req.fh == FUSE_NO_FH {
866 match DaxFsExt::open_inode_for_dax(self.fs.as_ref(), ctx.nodeid, writable) {
867 Ok(file) => {
868 use std::os::unix::io::AsRawFd;
869 let host_fd = file.as_raw_fd();
870 tracing::debug!(
871 "FUSE SETUPMAPPING (inode-fd): nodeid={} host_fd={} foffset={:#x} moffset={:#x} len={:#x} writable={}",
872 ctx.nodeid,
873 host_fd,
874 req.foffset,
875 req.moffset,
876 req.len,
877 writable,
878 );
879 mapper.setup_mapping(host_fd, req.foffset, req.moffset, req.len, writable)
880 }
883 Err(e) => {
884 tracing::warn!(
885 "FUSE SETUPMAPPING: failed to open nodeid={} for fh-sentinel mapping: {}",
886 ctx.nodeid,
887 e,
888 );
889 Err(libc::EBADF)
890 }
891 }
892 } else {
893 match self.fs.get_file_raw_fd(req.fh) {
894 Some(host_fd) => {
895 tracing::debug!(
896 "FUSE SETUPMAPPING: fh={:#x} host_fd={} foffset={:#x} moffset={:#x} len={:#x} writable={}",
897 req.fh,
898 host_fd,
899 req.foffset,
900 req.moffset,
901 req.len,
902 writable,
903 );
904 mapper.setup_mapping(host_fd, req.foffset, req.moffset, req.len, writable)
905 }
906 None => {
907 tracing::warn!(
908 "FUSE SETUPMAPPING: unknown fh={:#x} (foffset={:#x} moffset={:#x} len={:#x} flags={:#x})",
909 req.fh,
910 req.foffset,
911 req.moffset,
912 req.len,
913 req.flags,
914 );
915 Err(libc::EBADF)
916 }
917 }
918 };
919
920 match result {
921 Ok(()) => response.write_empty(ctx.unique),
922 Err(errno) => {
923 tracing::warn!(
924 "FUSE SETUPMAPPING: failed errno={} fh={:#x} nodeid={} foffset={:#x} moffset={:#x} len={:#x}",
925 errno,
926 req.fh,
927 ctx.nodeid,
928 req.foffset,
929 req.moffset,
930 req.len,
931 );
932 response.write_error(ctx.unique, errno);
933 }
934 }
935 }
936
937 fn handle_remove_mapping(
938 &self,
939 ctx: &RequestContext,
940 body: &[u8],
941 response: &mut ResponseBuilder,
942 ) {
943 use crate::fuse::{FuseRemoveMappingIn, FuseRemoveMappingOne};
944
945 let Some(ref mapper) = self.dax_mapper else {
946 response.write_error(ctx.unique, libc::ENOSYS);
947 return;
948 };
949
950 let hdr_size = std::mem::size_of::<FuseRemoveMappingIn>();
951 if body.len() < hdr_size {
952 response.write_error(ctx.unique, libc::EINVAL);
953 return;
954 }
955
956 let hdr = unsafe { std::ptr::read_unaligned(body.as_ptr().cast::<FuseRemoveMappingIn>()) };
957 let entry_size = std::mem::size_of::<FuseRemoveMappingOne>();
958 let entries = &body[hdr_size..];
959
960 for i in 0..hdr.count as usize {
961 let off = i * entry_size;
962 if off + entry_size > entries.len() {
963 break;
964 }
965 let entry = unsafe {
966 std::ptr::read_unaligned(entries[off..].as_ptr().cast::<FuseRemoveMappingOne>())
967 };
968 if let Err(errno) = mapper.remove_mapping(entry.moffset, entry.len) {
969 response.write_error(ctx.unique, errno);
970 return;
971 }
972 }
973
974 response.write_empty(ctx.unique);
975 }
976
977 fn handle_opendir(&self, ctx: &RequestContext, body: &[u8], response: &mut ResponseBuilder) {
982 if body.len() < size_of::<FuseOpenIn>() {
983 response.write_error(ctx.unique, libc::EINVAL);
984 return;
985 }
986
987 match self.fs.opendir(ctx.nodeid) {
988 Ok(handle) => {
989 let open_out = FuseOpenOut {
990 fh: handle,
991 open_flags: 0,
992 padding: 0,
993 };
994 response.write_data(ctx.unique, &open_out);
995 }
996 Err(e) => response.write_error(ctx.unique, e.to_errno()),
997 }
998 }
999
1000 fn handle_readdir(&self, ctx: &RequestContext, body: &[u8], response: &mut ResponseBuilder) {
1001 if body.len() < size_of::<FuseReadIn>() {
1002 response.write_error(ctx.unique, libc::EINVAL);
1003 return;
1004 }
1005
1006 let read_in = unsafe { std::ptr::read_unaligned(body.as_ptr() as *const FuseReadIn) };
1008
1009 match self.fs.readdir(read_in.fh, read_in.offset) {
1010 Ok(entries) => {
1011 let mut dirent_buf = Vec::new();
1012 let base_offset = read_in.offset + 1;
1013
1014 for (i, entry) in entries.into_iter().enumerate() {
1015 let name_bytes = entry.name.as_bytes();
1016 let entry_size = FuseDirent::size(name_bytes.len());
1017
1018 if dirent_buf.len() + entry_size > read_in.size as usize {
1020 break;
1021 }
1022
1023 let dirent = FuseDirent {
1024 ino: entry.ino,
1025 off: base_offset + i as u64,
1026 namelen: name_bytes.len() as u32,
1027 typ: entry.file_type.to_dirent_type(),
1028 };
1029
1030 let dirent_bytes = unsafe {
1032 std::slice::from_raw_parts(
1033 std::ptr::from_ref::<FuseDirent>(&dirent) as *const u8,
1034 size_of::<FuseDirent>(),
1035 )
1036 };
1037 dirent_buf.extend_from_slice(dirent_bytes);
1038
1039 dirent_buf.extend_from_slice(name_bytes);
1041
1042 let padding = entry_size - size_of::<FuseDirent>() - name_bytes.len();
1044 dirent_buf.extend(std::iter::repeat_n(0u8, padding));
1045 }
1046
1047 response.write_bytes(ctx.unique, &dirent_buf);
1048 }
1049 Err(e) => response.write_error(ctx.unique, e.to_errno()),
1050 }
1051 }
1052
1053 fn handle_readdirplus(
1058 &self,
1059 ctx: &RequestContext,
1060 body: &[u8],
1061 response: &mut ResponseBuilder,
1062 ) {
1063 if body.len() < size_of::<FuseReadIn>() {
1064 response.write_error(ctx.unique, libc::EINVAL);
1065 return;
1066 }
1067
1068 let read_in = unsafe { std::ptr::read_unaligned(body.as_ptr() as *const FuseReadIn) };
1070
1071 match self.fs.readdir(read_in.fh, read_in.offset) {
1072 Ok(entries) => {
1073 let mut buf = Vec::new();
1074 let mut offset = read_in.offset + 1;
1075
1076 for entry in entries {
1077 let name_bytes = entry.name.as_bytes();
1078 let dirent_size = FuseDirent::size(name_bytes.len());
1080 let entry_size = size_of::<FuseEntryOut>() + dirent_size;
1081
1082 if buf.len() + entry_size > read_in.size as usize {
1084 break;
1085 }
1086
1087 let attr = match self.fs.getattr(entry.ino) {
1092 Ok(attr) => attr,
1093 Err(_) => continue,
1094 };
1095
1096 let entry_out = self.make_entry_out(entry.ino, &attr);
1098
1099 let entry_out_bytes = unsafe {
1101 std::slice::from_raw_parts(
1102 std::ptr::from_ref::<FuseEntryOut>(&entry_out) as *const u8,
1103 size_of::<FuseEntryOut>(),
1104 )
1105 };
1106 buf.extend_from_slice(entry_out_bytes);
1107
1108 let dirent = FuseDirent {
1110 ino: entry.ino,
1111 off: offset,
1112 namelen: name_bytes.len() as u32,
1113 typ: entry.file_type.to_dirent_type(),
1114 };
1115 let dirent_bytes = unsafe {
1116 std::slice::from_raw_parts(
1117 std::ptr::from_ref::<FuseDirent>(&dirent) as *const u8,
1118 size_of::<FuseDirent>(),
1119 )
1120 };
1121 buf.extend_from_slice(dirent_bytes);
1122
1123 buf.extend_from_slice(name_bytes);
1125
1126 let padding = dirent_size - size_of::<FuseDirent>() - name_bytes.len();
1128 buf.extend(std::iter::repeat_n(0u8, padding));
1129
1130 offset += 1;
1131 }
1132
1133 response.write_bytes(ctx.unique, &buf);
1134 }
1135 Err(e) => response.write_error(ctx.unique, e.to_errno()),
1136 }
1137 }
1138
1139 fn handle_releasedir(&self, ctx: &RequestContext, body: &[u8], response: &mut ResponseBuilder) {
1140 if body.len() < size_of::<FuseReleaseIn>() {
1141 response.write_error(ctx.unique, libc::EINVAL);
1142 return;
1143 }
1144
1145 let release_in = unsafe { std::ptr::read_unaligned(body.as_ptr() as *const FuseReleaseIn) };
1147
1148 match self.fs.releasedir(release_in.fh) {
1149 Ok(()) => response.write_empty(ctx.unique),
1150 Err(e) => response.write_error(ctx.unique, e.to_errno()),
1151 }
1152 }
1153
1154 fn handle_fsyncdir(&self, ctx: &RequestContext, body: &[u8], response: &mut ResponseBuilder) {
1155 if body.len() < size_of::<FuseFsyncIn>() {
1156 response.write_error(ctx.unique, libc::EINVAL);
1157 return;
1158 }
1159
1160 let fsync_in = unsafe { std::ptr::read_unaligned(body.as_ptr() as *const FuseFsyncIn) };
1162 let datasync = fsync_in.fsync_flags & 1 != 0;
1163
1164 match self.fs.fsyncdir(fsync_in.fh, datasync) {
1165 Ok(()) => response.write_empty(ctx.unique),
1166 Err(e) => response.write_error(ctx.unique, e.to_errno()),
1167 }
1168 }
1169
1170 fn handle_getxattr(&self, ctx: &RequestContext, body: &[u8], response: &mut ResponseBuilder) {
1175 if body.len() < size_of::<FuseGetxattrIn>() {
1176 response.write_error(ctx.unique, libc::EINVAL);
1177 return;
1178 }
1179
1180 let getxattr_in =
1182 unsafe { std::ptr::read_unaligned(body.as_ptr() as *const FuseGetxattrIn) };
1183 let name = self.parse_name(&body[size_of::<FuseGetxattrIn>()..]);
1184
1185 match self.fs.getxattr(ctx.nodeid, name, getxattr_in.size) {
1186 Ok(value) => {
1187 if getxattr_in.size == 0 {
1188 let out = FuseGetxattrOut {
1190 size: value.len() as u32,
1191 padding: 0,
1192 };
1193 response.write_data(ctx.unique, &out);
1194 } else {
1195 response.write_bytes(ctx.unique, &value);
1196 }
1197 }
1198 Err(e) => response.write_error(ctx.unique, e.to_errno()),
1199 }
1200 }
1201
1202 fn handle_setxattr(&self, ctx: &RequestContext, body: &[u8], response: &mut ResponseBuilder) {
1203 if body.len() < size_of::<FuseSetxattrIn>() {
1204 response.write_error(ctx.unique, libc::EINVAL);
1205 return;
1206 }
1207
1208 let setxattr_in =
1210 unsafe { std::ptr::read_unaligned(body.as_ptr() as *const FuseSetxattrIn) };
1211 let rest = &body[size_of::<FuseSetxattrIn>()..];
1212
1213 if let Some(null_pos) = rest.iter().position(|&b| b == 0) {
1215 let name = OsStr::from_bytes(&rest[..null_pos]);
1216 let value = &rest[null_pos + 1..][..setxattr_in.size as usize];
1217
1218 match self.fs.setxattr(ctx.nodeid, name, value, setxattr_in.flags) {
1219 Ok(()) => response.write_empty(ctx.unique),
1220 Err(e) => response.write_error(ctx.unique, e.to_errno()),
1221 }
1222 } else {
1223 response.write_error(ctx.unique, libc::EINVAL);
1224 }
1225 }
1226
1227 fn handle_removexattr(
1228 &self,
1229 ctx: &RequestContext,
1230 body: &[u8],
1231 response: &mut ResponseBuilder,
1232 ) {
1233 let name = self.parse_name(body);
1234
1235 match self.fs.removexattr(ctx.nodeid, name) {
1236 Ok(()) => response.write_empty(ctx.unique),
1237 Err(e) => response.write_error(ctx.unique, e.to_errno()),
1238 }
1239 }
1240
1241 fn handle_statfs(&self, ctx: &RequestContext, response: &mut ResponseBuilder) {
1246 match self.fs.statfs() {
1247 Ok(st) => {
1248 let statfs_out = FuseStatfsOut { st };
1249 response.write_data(ctx.unique, &statfs_out);
1250 }
1251 Err(e) => response.write_error(ctx.unique, e.to_errno()),
1252 }
1253 }
1254
1255 fn handle_access(&self, ctx: &RequestContext, body: &[u8], response: &mut ResponseBuilder) {
1256 if body.len() < size_of::<FuseAccessIn>() {
1257 response.write_error(ctx.unique, libc::EINVAL);
1258 return;
1259 }
1260
1261 let access_in = unsafe { std::ptr::read_unaligned(body.as_ptr() as *const FuseAccessIn) };
1263
1264 match self.fs.access(ctx.nodeid, access_in.mask) {
1265 Ok(()) => response.write_empty(ctx.unique),
1266 Err(e) => response.write_error(ctx.unique, e.to_errno()),
1267 }
1268 }
1269
1270 fn parse_name<'a>(&self, body: &'a [u8]) -> &'a OsStr {
1275 let name_end = body.iter().position(|&b| b == 0).unwrap_or(body.len());
1276 OsStr::from_bytes(&body[..name_end])
1277 }
1278
1279 fn make_entry_out(&self, inode: u64, attr: &FuseAttr) -> FuseEntryOut {
1280 FuseEntryOut {
1281 nodeid: inode,
1282 generation: 0,
1283 entry_valid: self.config.entry_timeout,
1284 attr_valid: self.config.attr_timeout,
1285 entry_valid_nsec: 0,
1286 attr_valid_nsec: 0,
1287 attr: *attr,
1288 }
1289 }
1290}
1291
1292#[allow(clippy::missing_fields_in_debug)]
1295impl std::fmt::Debug for FuseDispatcher {
1296 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1297 f.debug_struct("FuseDispatcher")
1298 .field("config", &self.config)
1299 .finish()
1300 }
1301}
1302
1303#[cfg(test)]
1308mod tests {
1309 use super::*;
1310 use tempfile::TempDir;
1311
1312 fn setup_dispatcher() -> (TempDir, FuseDispatcher) {
1313 let temp = TempDir::new().expect("failed to create temp dir");
1314 let fs = Arc::new(PassthroughFs::new(temp.path()).expect("failed to create fs"));
1315 let dispatcher = FuseDispatcher::new(fs, DispatcherConfig::default());
1316 (temp, dispatcher)
1317 }
1318
1319 fn make_header(opcode: FuseOpcode, nodeid: u64, body_len: usize) -> Vec<u8> {
1320 let header = FuseInHeader {
1321 len: (FuseInHeader::SIZE + body_len) as u32,
1322 opcode: opcode as u32,
1323 unique: 1,
1324 nodeid,
1325 uid: 0,
1326 gid: 0,
1327 pid: 0,
1328 padding: 0,
1329 };
1330
1331 let header_bytes = unsafe {
1332 std::slice::from_raw_parts(
1333 &header as *const FuseInHeader as *const u8,
1334 FuseInHeader::SIZE,
1335 )
1336 };
1337 header_bytes.to_vec()
1338 }
1339
1340 fn parse_response_header(response: &[u8]) -> FuseOutHeader {
1341 assert!(response.len() >= FuseOutHeader::SIZE);
1342 unsafe { std::ptr::read_unaligned(response.as_ptr() as *const FuseOutHeader) }
1344 }
1345
1346 #[test]
1347 fn test_getattr_root() {
1348 let (_temp, dispatcher) = setup_dispatcher();
1349
1350 let request = make_header(FuseOpcode::Getattr, 1, 0);
1351 let response = dispatcher.dispatch(&request).unwrap();
1352 let header = parse_response_header(&response);
1353
1354 assert_eq!(header.error, 0);
1355 assert!(response.len() > FuseOutHeader::SIZE);
1356 }
1357
1358 #[test]
1359 fn test_lookup_nonexistent() {
1360 let (_temp, dispatcher) = setup_dispatcher();
1361
1362 let name = b"nonexistent\0";
1363 let mut request = make_header(FuseOpcode::Lookup, 1, name.len());
1364 request.extend_from_slice(name);
1365
1366 let response = dispatcher.dispatch(&request).unwrap();
1367 let header = parse_response_header(&response);
1368
1369 assert_eq!(header.error, -libc::ENOENT);
1370 }
1371
1372 #[test]
1373 fn test_lookup_existing() {
1374 let (temp, dispatcher) = setup_dispatcher();
1375
1376 std::fs::write(temp.path().join("test.txt"), "hello").unwrap();
1378
1379 let name = b"test.txt\0";
1380 let mut request = make_header(FuseOpcode::Lookup, 1, name.len());
1381 request.extend_from_slice(name);
1382
1383 let response = dispatcher.dispatch(&request).unwrap();
1384 let header = parse_response_header(&response);
1385
1386 assert_eq!(header.error, 0);
1387 }
1388
1389 #[test]
1390 fn test_mkdir_and_rmdir() {
1391 let (_temp, dispatcher) = setup_dispatcher();
1392
1393 let mkdir_in = FuseMkdirIn {
1395 mode: 0o755,
1396 umask: 0,
1397 };
1398 let name = b"testdir\0";
1399
1400 let mut request = make_header(FuseOpcode::Mkdir, 1, size_of::<FuseMkdirIn>() + name.len());
1401 let mkdir_bytes = unsafe {
1402 std::slice::from_raw_parts(
1403 &mkdir_in as *const FuseMkdirIn as *const u8,
1404 size_of::<FuseMkdirIn>(),
1405 )
1406 };
1407 request.extend_from_slice(mkdir_bytes);
1408 request.extend_from_slice(name);
1409
1410 let response = dispatcher.dispatch(&request).unwrap();
1411 let header = parse_response_header(&response);
1412 assert_eq!(header.error, 0);
1413
1414 let mut request = make_header(FuseOpcode::Rmdir, 1, name.len());
1416 request.extend_from_slice(name);
1417
1418 let response = dispatcher.dispatch(&request).unwrap();
1419 let header = parse_response_header(&response);
1420 assert_eq!(header.error, 0);
1421 }
1422
1423 #[test]
1424 fn test_open_read_write_release() {
1425 let (temp, dispatcher) = setup_dispatcher();
1426
1427 std::fs::write(temp.path().join("test.txt"), "initial").unwrap();
1429
1430 let name = b"test.txt\0";
1432 let mut request = make_header(FuseOpcode::Lookup, 1, name.len());
1433 request.extend_from_slice(name);
1434 let response = dispatcher.dispatch(&request).unwrap();
1435 let header = parse_response_header(&response);
1436 assert_eq!(header.error, 0);
1437
1438 let entry = unsafe {
1441 std::ptr::read_unaligned(
1442 (response.as_ptr() as *const u8).add(FuseOutHeader::SIZE) as *const FuseEntryOut
1443 )
1444 };
1445 let inode = entry.nodeid;
1446
1447 let open_in = FuseOpenIn {
1449 flags: libc::O_RDWR as u32,
1450 unused: 0,
1451 };
1452 let mut request = make_header(FuseOpcode::Open, inode, size_of::<FuseOpenIn>());
1453 let open_bytes = unsafe {
1454 std::slice::from_raw_parts(
1455 &open_in as *const FuseOpenIn as *const u8,
1456 size_of::<FuseOpenIn>(),
1457 )
1458 };
1459 request.extend_from_slice(open_bytes);
1460
1461 let response = dispatcher.dispatch(&request).unwrap();
1462 let header = parse_response_header(&response);
1463 assert_eq!(header.error, 0);
1464
1465 let open_out = unsafe {
1468 std::ptr::read_unaligned(
1469 (response.as_ptr() as *const u8).add(FuseOutHeader::SIZE) as *const FuseOpenOut
1470 )
1471 };
1472 let fh = open_out.fh;
1473
1474 let read_in = FuseReadIn {
1476 fh,
1477 offset: 0,
1478 size: 100,
1479 read_flags: 0,
1480 lock_owner: 0,
1481 flags: 0,
1482 padding: 0,
1483 };
1484 let mut request = make_header(FuseOpcode::Read, inode, size_of::<FuseReadIn>());
1485 let read_bytes = unsafe {
1486 std::slice::from_raw_parts(
1487 &read_in as *const FuseReadIn as *const u8,
1488 size_of::<FuseReadIn>(),
1489 )
1490 };
1491 request.extend_from_slice(read_bytes);
1492
1493 let response = dispatcher.dispatch(&request).unwrap();
1494 let header = parse_response_header(&response);
1495 assert_eq!(header.error, 0);
1496
1497 let data = &response[FuseOutHeader::SIZE..];
1498 assert_eq!(data, b"initial");
1499
1500 let release_in = FuseReleaseIn {
1502 fh,
1503 flags: 0,
1504 release_flags: 0,
1505 lock_owner: 0,
1506 };
1507 let mut request = make_header(FuseOpcode::Release, inode, size_of::<FuseReleaseIn>());
1508 let release_bytes = unsafe {
1509 std::slice::from_raw_parts(
1510 &release_in as *const FuseReleaseIn as *const u8,
1511 size_of::<FuseReleaseIn>(),
1512 )
1513 };
1514 request.extend_from_slice(release_bytes);
1515
1516 let response = dispatcher.dispatch(&request).unwrap();
1517 let header = parse_response_header(&response);
1518 assert_eq!(header.error, 0);
1519 }
1520
1521 #[test]
1522 fn test_statfs() {
1523 let (_temp, dispatcher) = setup_dispatcher();
1524
1525 let request = make_header(FuseOpcode::Statfs, 1, 0);
1526 let response = dispatcher.dispatch(&request).unwrap();
1527 let header = parse_response_header(&response);
1528
1529 assert_eq!(header.error, 0);
1530 assert!(response.len() >= FuseOutHeader::SIZE + size_of::<FuseStatfsOut>());
1531 }
1532
1533 #[test]
1534 fn test_unknown_opcode() {
1535 let (_temp, dispatcher) = setup_dispatcher();
1536
1537 let header = FuseInHeader {
1538 len: FuseInHeader::SIZE as u32,
1539 opcode: 9999, unique: 1,
1541 nodeid: 1,
1542 uid: 0,
1543 gid: 0,
1544 pid: 0,
1545 padding: 0,
1546 };
1547
1548 let request = unsafe {
1549 std::slice::from_raw_parts(
1550 &header as *const FuseInHeader as *const u8,
1551 FuseInHeader::SIZE,
1552 )
1553 };
1554
1555 let result = dispatcher.dispatch(request);
1556 assert!(result.is_err());
1557 }
1558
1559 #[test]
1560 fn test_unsupported_opcode() {
1561 let (_temp, dispatcher) = setup_dispatcher();
1562
1563 let request = make_header(FuseOpcode::Ioctl, 1, 0);
1565 let response = dispatcher.dispatch(&request).unwrap();
1566 let header = parse_response_header(&response);
1567
1568 assert_eq!(header.error, -libc::ENOSYS);
1569 }
1570
1571 #[test]
1572 fn test_opendir_readdir_releasedir() {
1573 let (temp, dispatcher) = setup_dispatcher();
1574
1575 std::fs::write(temp.path().join("file1.txt"), "").unwrap();
1577 std::fs::write(temp.path().join("file2.txt"), "").unwrap();
1578
1579 let open_in = FuseOpenIn {
1581 flags: 0,
1582 unused: 0,
1583 };
1584 let mut request = make_header(FuseOpcode::Opendir, 1, size_of::<FuseOpenIn>());
1585 let open_bytes = unsafe {
1586 std::slice::from_raw_parts(
1587 &open_in as *const FuseOpenIn as *const u8,
1588 size_of::<FuseOpenIn>(),
1589 )
1590 };
1591 request.extend_from_slice(open_bytes);
1592
1593 let response = dispatcher.dispatch(&request).unwrap();
1594 let header = parse_response_header(&response);
1595 assert_eq!(header.error, 0);
1596
1597 let open_out = unsafe {
1599 std::ptr::read_unaligned(
1600 (response.as_ptr() as *const u8).add(FuseOutHeader::SIZE) as *const FuseOpenOut
1601 )
1602 };
1603 let fh = open_out.fh;
1604
1605 let read_in = FuseReadIn {
1607 fh,
1608 offset: 0,
1609 size: 4096,
1610 read_flags: 0,
1611 lock_owner: 0,
1612 flags: 0,
1613 padding: 0,
1614 };
1615 let mut request = make_header(FuseOpcode::Readdir, 1, size_of::<FuseReadIn>());
1616 let read_bytes = unsafe {
1617 std::slice::from_raw_parts(
1618 &read_in as *const FuseReadIn as *const u8,
1619 size_of::<FuseReadIn>(),
1620 )
1621 };
1622 request.extend_from_slice(read_bytes);
1623
1624 let response = dispatcher.dispatch(&request).unwrap();
1625 let header = parse_response_header(&response);
1626 assert_eq!(header.error, 0);
1627
1628 {
1632 let body = &response[FuseOutHeader::SIZE..];
1633 let base_offset: u64 = read_in.offset + 1; let mut pos = 0usize;
1635 let mut i = 0usize;
1636 while pos + size_of::<FuseDirent>() <= body.len() {
1637 let dirent =
1639 unsafe { std::ptr::read_unaligned(body[pos..].as_ptr() as *const FuseDirent) };
1640 assert_eq!(
1641 dirent.off,
1642 base_offset + i as u64,
1643 "dirent[{i}].off should be base_offset({base_offset}) + {i}"
1644 );
1645 let entry_size = FuseDirent::size(dirent.namelen as usize);
1646 pos += entry_size;
1647 i += 1;
1648 }
1649 assert!(i > 0, "should have parsed at least one dirent entry");
1650 }
1651
1652 let release_in = FuseReleaseIn {
1654 fh,
1655 flags: 0,
1656 release_flags: 0,
1657 lock_owner: 0,
1658 };
1659 let mut request = make_header(FuseOpcode::Releasedir, 1, size_of::<FuseReleaseIn>());
1660 let release_bytes = unsafe {
1661 std::slice::from_raw_parts(
1662 &release_in as *const FuseReleaseIn as *const u8,
1663 size_of::<FuseReleaseIn>(),
1664 )
1665 };
1666 request.extend_from_slice(release_bytes);
1667
1668 let response = dispatcher.dispatch(&request).unwrap();
1669 let header = parse_response_header(&response);
1670 assert_eq!(header.error, 0);
1671 }
1672
1673 #[test]
1674 fn test_response_builder() {
1675 let mut builder = ResponseBuilder::new();
1676
1677 builder.write_error(123, libc::ENOENT);
1679 let response = builder.as_bytes();
1680 let header = parse_response_header(response);
1681 assert_eq!(header.unique, 123);
1682 assert_eq!(header.error, -libc::ENOENT);
1683 assert_eq!(header.len as usize, FuseOutHeader::SIZE);
1684
1685 builder.write_empty(456);
1687 let response = builder.as_bytes();
1688 let header = parse_response_header(response);
1689 assert_eq!(header.unique, 456);
1690 assert_eq!(header.error, 0);
1691 assert_eq!(header.len as usize, FuseOutHeader::SIZE);
1692
1693 builder.write_bytes(789, b"hello");
1695 let response = builder.as_bytes();
1696 let header = parse_response_header(response);
1697 assert_eq!(header.unique, 789);
1698 assert_eq!(header.error, 0);
1699 assert_eq!(header.len as usize, FuseOutHeader::SIZE + 5);
1700 assert_eq!(&response[FuseOutHeader::SIZE..], b"hello");
1701 }
1702
1703 #[test]
1704 fn test_readdirplus() {
1705 let (temp, dispatcher) = setup_dispatcher();
1706
1707 std::fs::write(temp.path().join("alpha.txt"), "aaa").unwrap();
1709 std::fs::write(temp.path().join("beta.txt"), "bb").unwrap();
1710
1711 let open_in = FuseOpenIn {
1713 flags: 0,
1714 unused: 0,
1715 };
1716 let mut request = make_header(FuseOpcode::Opendir, 1, size_of::<FuseOpenIn>());
1717 let open_bytes = unsafe {
1718 std::slice::from_raw_parts(
1719 &open_in as *const FuseOpenIn as *const u8,
1720 size_of::<FuseOpenIn>(),
1721 )
1722 };
1723 request.extend_from_slice(open_bytes);
1724
1725 let response = dispatcher.dispatch(&request).unwrap();
1726 let header = parse_response_header(&response);
1727 assert_eq!(header.error, 0);
1728
1729 let open_out = unsafe {
1730 std::ptr::read_unaligned(
1731 (response.as_ptr() as *const u8).add(FuseOutHeader::SIZE) as *const FuseOpenOut
1732 )
1733 };
1734 let fh = open_out.fh;
1735
1736 let read_in = FuseReadIn {
1738 fh,
1739 offset: 0,
1740 size: 8192, read_flags: 0,
1742 lock_owner: 0,
1743 flags: 0,
1744 padding: 0,
1745 };
1746 let mut request = make_header(FuseOpcode::Readdirplus, 1, size_of::<FuseReadIn>());
1747 let read_bytes = unsafe {
1748 std::slice::from_raw_parts(
1749 &read_in as *const FuseReadIn as *const u8,
1750 size_of::<FuseReadIn>(),
1751 )
1752 };
1753 request.extend_from_slice(read_bytes);
1754
1755 let response = dispatcher.dispatch(&request).unwrap();
1756 let header = parse_response_header(&response);
1757 assert_eq!(header.error, 0, "READDIRPLUS should succeed");
1758
1759 let body_len = response.len() - FuseOutHeader::SIZE;
1762 assert!(
1763 body_len > 0,
1764 "READDIRPLUS response should contain directory entries"
1765 );
1766
1767 let body = &response[FuseOutHeader::SIZE..];
1770 assert!(
1771 body.len() >= size_of::<FuseEntryOut>() + size_of::<FuseDirent>(),
1772 "Response should contain at least one full READDIRPLUS entry"
1773 );
1774
1775 let first_entry = unsafe { std::ptr::read_unaligned(body.as_ptr() as *const FuseEntryOut) };
1777 assert!(
1778 first_entry.nodeid > 0,
1779 "First entry should have a valid node ID"
1780 );
1781 assert!(
1782 first_entry.entry_valid > 0 || first_entry.attr_valid > 0,
1783 "First entry should have cache timeouts set"
1784 );
1785
1786 let release_in = FuseReleaseIn {
1788 fh,
1789 flags: 0,
1790 release_flags: 0,
1791 lock_owner: 0,
1792 };
1793 let mut request = make_header(FuseOpcode::Releasedir, 1, size_of::<FuseReleaseIn>());
1794 let release_bytes = unsafe {
1795 std::slice::from_raw_parts(
1796 &release_in as *const FuseReleaseIn as *const u8,
1797 size_of::<FuseReleaseIn>(),
1798 )
1799 };
1800 request.extend_from_slice(release_bytes);
1801
1802 let response = dispatcher.dispatch(&request).unwrap();
1803 let header = parse_response_header(&response);
1804 assert_eq!(header.error, 0);
1805 }
1806
1807 #[test]
1812 fn test_setup_mapping_sentinel_fh() {
1813 use crate::fuse::{FUSE_NO_FH, FuseSetupMappingIn};
1814 use std::sync::atomic::{AtomicBool, Ordering};
1815
1816 struct RecordingMapper {
1818 called: AtomicBool,
1819 }
1820 impl crate::DaxMapper for RecordingMapper {
1821 fn setup_mapping(
1822 &self,
1823 _host_fd: i32,
1824 _file_offset: u64,
1825 _window_offset: u64,
1826 _length: u64,
1827 _writable: bool,
1828 ) -> std::result::Result<(), i32> {
1829 self.called.store(true, Ordering::SeqCst);
1830 Ok(())
1831 }
1832 fn remove_mapping(
1833 &self,
1834 _window_offset: u64,
1835 _length: u64,
1836 ) -> std::result::Result<(), i32> {
1837 Ok(())
1838 }
1839 }
1840
1841 let temp = tempfile::TempDir::new().unwrap();
1842
1843 std::fs::write(temp.path().join("exec_bin"), b"ELF_PAYLOAD").unwrap();
1845
1846 let fs = Arc::new(PassthroughFs::new(temp.path()).unwrap());
1847
1848 let mapper = Arc::new(RecordingMapper {
1850 called: AtomicBool::new(false),
1851 });
1852 let mut dispatcher = FuseDispatcher::new(Arc::clone(&fs), DispatcherConfig::default());
1853 dispatcher.set_dax_mapper(Arc::clone(&mapper) as Arc<dyn crate::DaxMapper>);
1854
1855 let name = b"exec_bin\0";
1856 let mut req = make_header(FuseOpcode::Lookup, 1, name.len());
1857 req.extend_from_slice(name);
1858 let resp = dispatcher.dispatch(&req).unwrap();
1859 let resp_hdr = parse_response_header(&resp);
1860 assert_eq!(resp_hdr.error, 0, "lookup must succeed");
1861
1862 let entry_out = unsafe {
1864 std::ptr::read_unaligned(
1865 (resp.as_ptr() as *const u8).add(FuseOutHeader::SIZE) as *const FuseEntryOut
1866 )
1867 };
1868 let inode = entry_out.nodeid;
1869
1870 let mapping_in = FuseSetupMappingIn {
1872 fh: FUSE_NO_FH,
1873 foffset: 0,
1874 len: 4096,
1875 flags: 0, moffset: 0,
1877 };
1878 let mut req = make_header(
1879 FuseOpcode::SetupMapping,
1880 inode,
1881 size_of::<FuseSetupMappingIn>(),
1882 );
1883 let mapping_bytes = unsafe {
1884 std::slice::from_raw_parts(
1885 &mapping_in as *const FuseSetupMappingIn as *const u8,
1886 size_of::<FuseSetupMappingIn>(),
1887 )
1888 };
1889 req.extend_from_slice(mapping_bytes);
1890
1891 let resp = dispatcher.dispatch(&req).unwrap();
1892 let resp_hdr = parse_response_header(&resp);
1893
1894 assert_eq!(
1896 resp_hdr.error, 0,
1897 "SETUPMAPPING with FUSE_NO_FH sentinel should succeed (got errno {})",
1898 -resp_hdr.error
1899 );
1900
1901 assert!(
1903 mapper.called.load(Ordering::SeqCst),
1904 "DaxMapper::setup_mapping should have been called via the sentinel-fh path"
1905 );
1906 }
1907
1908 #[test]
1913 fn test_dax_fs_ext_open_inode_for_dax_reads_content() {
1914 use crate::dispatcher::DaxFsExt;
1915 use std::io::Read;
1916
1917 let temp = tempfile::TempDir::new().unwrap();
1918 std::fs::write(temp.path().join("data.bin"), b"hello dax").unwrap();
1919
1920 let fs = PassthroughFs::new(temp.path()).unwrap();
1921
1922 let name = std::ffi::OsStr::new("data.bin");
1924 let (inode, _attr) = fs.lookup(1, name).unwrap();
1925
1926 let mut file = DaxFsExt::open_inode_for_dax(&fs, inode, false).unwrap();
1928 let mut buf = Vec::new();
1929 file.read_to_end(&mut buf).unwrap();
1930 assert_eq!(
1931 buf, b"hello dax",
1932 "open_inode_for_dax should expose file content"
1933 );
1934 }
1935
1936 #[test]
1937 fn test_dax_fs_ext_open_inode_for_dax_toctou_rename_detected() {
1938 use crate::dispatcher::DaxFsExt;
1939
1940 let temp = tempfile::TempDir::new().unwrap();
1941 std::fs::write(temp.path().join("original.bin"), b"orig").unwrap();
1942 std::fs::write(temp.path().join("replacement.bin"), b"evil").unwrap();
1943
1944 let fs = PassthroughFs::new(temp.path()).unwrap();
1945
1946 let name = std::ffi::OsStr::new("original.bin");
1948 let (inode, _attr) = fs.lookup(1, name).unwrap();
1949
1950 std::fs::rename(
1953 temp.path().join("replacement.bin"),
1954 temp.path().join("original.bin"),
1955 )
1956 .unwrap();
1957
1958 let result = DaxFsExt::open_inode_for_dax(&fs, inode, false);
1960 let err = result.expect_err("should fail with EIO after TOCTOU swap");
1961 assert_eq!(
1962 err.raw_os_error(),
1963 Some(libc::EIO),
1964 "expected EIO for TOCTOU-detected rename, got: {err}"
1965 );
1966 }
1967}