Skip to main content

arcbox_fs/
dispatcher.rs

1//! FUSE request dispatcher.
2//!
3//! This module handles parsing FUSE requests and dispatching them to the
4//! appropriate filesystem operations. It acts as the bridge between the
5//! raw FUSE protocol and the [`PassthroughFs`] implementation.
6
7// Allow casts and pointer operations for FUSE protocol binary compatibility.
8// FUSE structs arriving from VirtIO queue memory may be unaligned, so all
9// deserialization uses `std::ptr::read_unaligned` instead of reference casts.
10#![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    // Test helpers serialize FUSE structs to byte slices using the same
17    // `&x as *const T as *const u8` and `.add(N) as *const T` patterns as
18    // the production code. The suppression is intentional for binary compat.
19    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
42/// Extension trait for `PassthroughFs` that adds DAX-specific operations.
43///
44/// Kept separate from `PassthroughFs` so that the general-purpose filesystem
45/// abstraction stays free of DAX protocol concerns. Only `FuseDispatcher`
46/// (and tests) need to call this method.
47pub trait DaxFsExt {
48    /// Opens a short-lived `File` for the given inode, for use in
49    /// `FUSE_SETUPMAPPING` requests that carry the [`FUSE_NO_FH`] sentinel.
50    ///
51    /// When `writable` is `true` the file is opened O_RDWR so that a subsequent
52    /// `mmap(MAP_SHARED | PROT_WRITE)` does not get EACCES.  When `writable` is
53    /// `false` the file is opened O_RDONLY.
54    ///
55    /// After opening, performs a TOCTOU check by comparing the `st_ino` of the
56    /// opened fd against the `st_ino` of the path still in the filesystem.
57    /// Returns `io::Error` with `EIO` if they differ (file was swapped between
58    /// the path resolution and the open call).
59    ///
60    /// The caller is expected to pass the returned fd to `mmap` / `hv_vm_map`
61    /// and then drop the `File`. The kernel mapping survives fd close.
62    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        // Retrieve the kernel st_ino that was recorded at lookup/create time.
70        // This is the ground truth: it reflects the file the guest believes it
71        // has a reference to. If the directory entry is later renamed so that
72        // the path now resolves to a different kernel inode, the comparison
73        // below will catch the swap even when open() itself did not race.
74        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        // Open with the mode the caller requested.  A writable DAX mapping
86        // requires O_RDWR: mmap(MAP_SHARED | PROT_WRITE) on an O_RDONLY fd
87        // returns EACCES even if the host file itself is writable.
88        let file = std::fs::OpenOptions::new()
89            .read(true)
90            .write(writable)
91            .open(&path)?;
92
93        // TOCTOU guard: compare the fd's st_ino against the value that was
94        // stored at inode-registration time. A rename that happened at any
95        // point after registration — including before this function was called
96        // — will cause the fd to have a different kernel inode number, which we
97        // detect here and reject with EIO.
98        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// ============================================================================
115// Dispatcher Configuration
116// ============================================================================
117
118/// Configuration for the FUSE dispatcher.
119#[derive(Debug, Clone)]
120pub struct DispatcherConfig {
121    /// Entry cache timeout in seconds.
122    pub entry_timeout: u64,
123    /// Attribute cache timeout in seconds.
124    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// ============================================================================
137// Request Context
138// ============================================================================
139
140/// Context for a FUSE request.
141#[derive(Debug, Clone, Copy)]
142pub struct RequestContext {
143    /// Unique request ID.
144    pub unique: u64,
145    /// Node ID (inode).
146    pub nodeid: u64,
147    /// User ID.
148    pub uid: u32,
149    /// Group ID.
150    pub gid: u32,
151    /// Process ID.
152    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
167// ============================================================================
168// Response Builder
169// ============================================================================
170
171/// Helper for building FUSE responses.
172pub struct ResponseBuilder {
173    buffer: Vec<u8>,
174}
175
176impl ResponseBuilder {
177    /// Creates a new response builder.
178    #[must_use]
179    pub fn new() -> Self {
180        Self {
181            buffer: Vec::with_capacity(4096),
182        }
183    }
184
185    /// Writes an error response.
186    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    /// Writes a success response with only a header.
196    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    /// Writes a success response with data.
203    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    /// Writes a success response with raw bytes.
212    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    /// Returns the built response.
221    #[must_use]
222    pub fn finish(self) -> Vec<u8> {
223        self.buffer
224    }
225
226    /// Returns a reference to the buffer.
227    #[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
246// ============================================================================
247// FUSE Dispatcher
248// ============================================================================
249
250/// FUSE request dispatcher.
251///
252/// Parses incoming FUSE requests and routes them to the appropriate
253/// filesystem operations on [`PassthroughFs`].
254///
255/// # Example
256///
257/// ```no_run
258/// use arcbox_fs::dispatcher::{FuseDispatcher, DispatcherConfig};
259/// use arcbox_fs::passthrough::PassthroughFs;
260/// use std::sync::Arc;
261///
262/// let fs = Arc::new(PassthroughFs::new("/tmp").unwrap());
263/// let dispatcher = FuseDispatcher::new(fs, DispatcherConfig::default());
264///
265/// // Process a FUSE request
266/// let request = vec![0u8; 64]; // Example request bytes
267/// let response = dispatcher.dispatch(&request);
268/// ```
269pub struct FuseDispatcher {
270    /// The underlying filesystem.
271    fs: Arc<PassthroughFs>,
272    /// Dispatcher configuration.
273    config: DispatcherConfig,
274    /// Optional DAX mapper for direct host page mapping.
275    dax_mapper: Option<Arc<dyn crate::DaxMapper>>,
276}
277
278impl FuseDispatcher {
279    /// Creates a new dispatcher.
280    #[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    /// Sets the DAX mapper for direct host page mapping.
290    pub fn set_dax_mapper(&mut self, mapper: Arc<dyn crate::DaxMapper>) {
291        self.dax_mapper = Some(mapper);
292    }
293
294    /// Dispatches a FUSE request and returns the response.
295    ///
296    /// # Errors
297    ///
298    /// Returns an error if the request cannot be parsed.
299    pub fn dispatch(&self, request: &[u8]) -> Result<Vec<u8>> {
300        // Parse header
301        if request.len() < FuseInHeader::SIZE {
302            return Err(FsError::Fuse("request too small".to_string()));
303        }
304
305        // SAFETY: FuseInHeader is a packed FUSE protocol struct that may arrive at
306        // an unaligned offset from VirtIO queue memory; read_unaligned avoids UB.
307        let header = unsafe { std::ptr::read_unaligned(request.as_ptr() as *const FuseInHeader) };
308        let body = &request[FuseInHeader::SIZE..];
309
310        // Parse opcode
311        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        // Dispatch to handler
325        match opcode {
326            // FUSE_INIT is intercepted by FuseSession::handle_init in arcbox-virtio-fs
327            // before any FuseRequestHandler is called.  If we somehow receive it here
328            // the session has already sent the handshake; responding ENOSYS is safe.
329            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    // ========================================================================
378    // Destroy
379    // ========================================================================
380
381    fn handle_destroy(&self, ctx: &RequestContext, response: &mut ResponseBuilder) {
382        response.write_empty(ctx.unique);
383    }
384
385    // ========================================================================
386    // Lookup / Forget
387    // ========================================================================
388
389    fn handle_lookup(&self, ctx: &RequestContext, body: &[u8], response: &mut ResponseBuilder) {
390        // Body is null-terminated name
391        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            // SAFETY: FuseForgetIn may sit at an unaligned offset in the VirtIO buffer.
405            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        // FORGET has no response
410        let _ = response;
411    }
412
413    // ========================================================================
414    // Attributes
415    // ========================================================================
416
417    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        // SAFETY: FuseSetattrIn may sit at an unaligned offset in the VirtIO buffer.
439        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    // ========================================================================
504    // File Creation
505    // ========================================================================
506
507    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        // SAFETY: FuseMknodIn may sit at an unaligned offset in the VirtIO buffer.
514        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        // SAFETY: FuseMkdirIn may sit at an unaligned offset in the VirtIO buffer.
536        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        // Body: name\0linkname\0
550        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        // SAFETY: FuseLinkIn may sit at an unaligned offset in the VirtIO buffer.
577        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        // SAFETY: FuseCreateIn may sit at an unaligned offset in the VirtIO buffer.
596        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                // Write entry followed by open response
612                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    // ========================================================================
626    // File Deletion
627    // ========================================================================
628
629    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        // SAFETY: FuseRenameIn may sit at an unaligned offset in the VirtIO buffer.
654        let rename_in = unsafe { std::ptr::read_unaligned(body.as_ptr() as *const FuseRenameIn) };
655        let names = &body[size_of::<FuseRenameIn>()..];
656
657        // Parse old_name\0new_name\0
658        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    // ========================================================================
677    // File Operations
678    // ========================================================================
679
680    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        // SAFETY: FuseOpenIn may sit at an unaligned offset in the VirtIO buffer.
687        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        // SAFETY: FuseReadIn may sit at an unaligned offset in the VirtIO buffer.
709        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        // SAFETY: FuseWriteIn may sit at an unaligned offset in the VirtIO buffer.
724        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        // SAFETY: FuseReleaseIn may sit at an unaligned offset in the VirtIO buffer.
749        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        // SAFETY: FuseFlushIn may sit at an unaligned offset in the VirtIO buffer.
764        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        // SAFETY: FuseFsyncIn may sit at an unaligned offset in the VirtIO buffer.
779        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        // SAFETY: FuseLseekIn may sit at an unaligned offset in the VirtIO buffer.
795        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        // SAFETY: FuseFallocateIn may sit at an unaligned offset in the VirtIO buffer.
816        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    // ========================================================================
831    // DAX Mapping Operations
832    // ========================================================================
833
834    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        // The Linux virtiofs DAX client sends `fh = FUSE_NO_FH` (u64::MAX) when
857        // it wants to map a file for which it has no open file handle — typically
858        // the on-demand mapping path used during `execve` and read-ahead.
859        // For that case we fall back to opening a fresh host fd via
860        // `DaxFsExt::open_inode_for_dax`, which also performs a TOCTOU check.
861        // The fd is opened writable when the guest requests a writable mapping so
862        // that mmap(MAP_SHARED | PROT_WRITE) does not fail with EACCES.
863        // `mmap(2)` keeps the mapping alive independent of the fd's lifetime,
864        // so the temporary `File` can be dropped as soon as `setup_mapping` returns.
865        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                    // `file` drops here — closes the fd. The mapping
881                    // installed by `mmap` stays alive regardless.
882                }
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    // ========================================================================
978    // Directory Operations
979    // ========================================================================
980
981    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        // SAFETY: FuseReadIn may sit at an unaligned offset in the VirtIO buffer.
1007        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                    // Check if we have room in the buffer
1019                    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                    // Write dirent header
1031                    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                    // Write name
1040                    dirent_buf.extend_from_slice(name_bytes);
1041
1042                    // Pad to 8-byte boundary
1043                    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    /// Handles FUSE_READDIRPLUS: like readdir but each entry includes a full
1054    /// `FuseEntryOut` with inode attributes and cache timeouts. This allows
1055    /// the guest kernel to populate its dcache and icache in a single round
1056    /// trip, eliminating separate LOOKUP calls after directory listing.
1057    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        // SAFETY: FuseReadIn may sit at an unaligned offset in the VirtIO buffer.
1069        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                    // READDIRPLUS entry: FuseEntryOut + FuseDirent + name + padding
1079                    let dirent_size = FuseDirent::size(name_bytes.len());
1080                    let entry_size = size_of::<FuseEntryOut>() + dirent_size;
1081
1082                    // Check if we have room in the buffer
1083                    if buf.len() + entry_size > read_in.size as usize {
1084                        break;
1085                    }
1086
1087                    // Try to get attributes for this entry via getattr.
1088                    // If getattr fails (e.g. inode disappeared between opendir
1089                    // and readdirplus) we skip the entry rather than failing
1090                    // the entire request.
1091                    let attr = match self.fs.getattr(entry.ino) {
1092                        Ok(attr) => attr,
1093                        Err(_) => continue,
1094                    };
1095
1096                    // Build the entry_out with cache timeouts
1097                    let entry_out = self.make_entry_out(entry.ino, &attr);
1098
1099                    // Write FuseEntryOut
1100                    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                    // Write FuseDirent header
1109                    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                    // Write name
1124                    buf.extend_from_slice(name_bytes);
1125
1126                    // Pad to 8-byte boundary
1127                    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        // SAFETY: FuseReleaseIn may sit at an unaligned offset in the VirtIO buffer.
1146        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        // SAFETY: FuseFsyncIn may sit at an unaligned offset in the VirtIO buffer.
1161        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    // ========================================================================
1171    // Extended Attributes
1172    // ========================================================================
1173
1174    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        // SAFETY: FuseGetxattrIn may sit at an unaligned offset in the VirtIO buffer.
1181        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                    // Return size only
1189                    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        // SAFETY: FuseSetxattrIn may sit at an unaligned offset in the VirtIO buffer.
1209        let setxattr_in =
1210            unsafe { std::ptr::read_unaligned(body.as_ptr() as *const FuseSetxattrIn) };
1211        let rest = &body[size_of::<FuseSetxattrIn>()..];
1212
1213        // Parse name\0value
1214        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    // ========================================================================
1242    // Other Operations
1243    // ========================================================================
1244
1245    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        // SAFETY: FuseAccessIn may sit at an unaligned offset in the VirtIO buffer.
1262        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    // ========================================================================
1271    // Helper Methods
1272    // ========================================================================
1273
1274    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// The `fs` field is intentionally excluded from Debug output — it contains
1293// internal locks and file descriptors that are not meaningful to log.
1294#[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// ============================================================================
1304// Tests
1305// ============================================================================
1306
1307#[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        // SAFETY: FuseOutHeader may sit at an unaligned offset in the response buffer.
1343        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        // Create a file
1377        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        // Mkdir
1394        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        // Rmdir
1415        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        // Create file
1428        std::fs::write(temp.path().join("test.txt"), "initial").unwrap();
1429
1430        // Lookup to get inode
1431        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        // Extract inode from entry response
1439        // SAFETY: FuseEntryOut may sit at an unaligned offset in the response buffer.
1440        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        // Open
1448        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        // Extract file handle
1466        // SAFETY: FuseOpenOut may sit at an unaligned offset in the response buffer.
1467        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        // Read
1475        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        // Release
1501        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, // Unknown opcode
1540            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        // IOCTL is unsupported
1564        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        // Create some files
1576        std::fs::write(temp.path().join("file1.txt"), "").unwrap();
1577        std::fs::write(temp.path().join("file2.txt"), "").unwrap();
1578
1579        // Opendir
1580        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        // SAFETY: FuseOpenOut may sit at an unaligned offset in the response buffer.
1598        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        // Readdir
1606        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        // Parse dirent entries and verify each entry's `off` field equals
1629        // `base_offset + i` where base_offset = read_in.offset + 1 = 1.
1630        // This pins the `base_offset + i` formula introduced in ABX-366.
1631        {
1632            let body = &response[FuseOutHeader::SIZE..];
1633            let base_offset: u64 = read_in.offset + 1; // matches dispatcher formula
1634            let mut pos = 0usize;
1635            let mut i = 0usize;
1636            while pos + size_of::<FuseDirent>() <= body.len() {
1637                // SAFETY: FuseDirent may sit at any alignment in the packed response body.
1638                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        // Releasedir
1653        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        // Test error response
1678        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        // Test empty response
1686        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        // Test bytes response
1694        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        // Create files so the directory has known entries
1708        std::fs::write(temp.path().join("alpha.txt"), "aaa").unwrap();
1709        std::fs::write(temp.path().join("beta.txt"), "bb").unwrap();
1710
1711        // Opendir on root inode
1712        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        // Send READDIRPLUS request
1737        let read_in = FuseReadIn {
1738            fh,
1739            offset: 0,
1740            size: 8192, // Generous buffer
1741            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        // The response body should be non-empty (it contains entries for
1760        // at least ".", "..", "alpha.txt", "beta.txt").
1761        let body_len = response.len() - FuseOutHeader::SIZE;
1762        assert!(
1763            body_len > 0,
1764            "READDIRPLUS response should contain directory entries"
1765        );
1766
1767        // Each READDIRPLUS entry starts with a FuseEntryOut followed by a
1768        // FuseDirent. Verify we can parse the first entry.
1769        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        // Parse the first FuseEntryOut to verify it has valid timeouts
1776        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        // Releasedir
1787        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    // -----------------------------------------------------------------------
1808    // #14 — sentinel-fh (FUSE_NO_FH) path in handle_setup_mapping
1809    // -----------------------------------------------------------------------
1810
1811    #[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        // A mock DaxMapper that records whether setup_mapping was called.
1817        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        // Write a real file so the inode exists and open_inode_for_dax succeeds.
1844        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        // Perform a LOOKUP so the dispatcher has the inode registered.
1849        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        // SAFETY: FuseEntryOut may sit at an unaligned offset in the response buffer.
1863        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        // Build FUSE_SETUPMAPPING with fh = FUSE_NO_FH (sentinel — no open fd).
1871        let mapping_in = FuseSetupMappingIn {
1872            fh: FUSE_NO_FH,
1873            foffset: 0,
1874            len: 4096,
1875            flags: 0, // read-only
1876            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        // 1. Response error must be 0 — not ENOSYS or EBADF.
1895        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        // 2. The mock setup_mapping must have been called exactly once.
1902        assert!(
1903            mapper.called.load(Ordering::SeqCst),
1904            "DaxMapper::setup_mapping should have been called via the sentinel-fh path"
1905        );
1906    }
1907
1908    // -----------------------------------------------------------------------
1909    // #15 — DaxFsExt::open_inode_for_dax (positive + TOCTOU negative)
1910    // -----------------------------------------------------------------------
1911
1912    #[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        // Register the inode via lookup.
1923        let name = std::ffi::OsStr::new("data.bin");
1924        let (inode, _attr) = fs.lookup(1, name).unwrap();
1925
1926        // (a) The returned file must be readable and contain the written content.
1927        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        // Register original.bin so the inode table maps inode → "original.bin".
1947        let name = std::ffi::OsStr::new("original.bin");
1948        let (inode, _attr) = fs.lookup(1, name).unwrap();
1949
1950        // Simulate a TOCTOU swap: rename replacement.bin → original.bin so
1951        // the path now points to a different file (different kernel st_ino).
1952        std::fs::rename(
1953            temp.path().join("replacement.bin"),
1954            temp.path().join("original.bin"),
1955        )
1956        .unwrap();
1957
1958        // open_inode_for_dax must detect the st_ino mismatch and return EIO.
1959        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}