fuse/
session.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4use super::Fuse;
5use super::Mapper;
6use super::protocol::*;
7use super::reply::ReplySender;
8use super::request::FuseOperation;
9use super::request::Request;
10use super::request::RequestReader;
11use parking_lot::RwLock;
12use std::io;
13use std::sync::atomic;
14use thiserror::Error;
15use zerocopy::FromZeros;
16use zerocopy::Immutable;
17use zerocopy::KnownLayout;
18
19// These are the flags that libfuse enables by default when calling init.
20const DEFAULT_FLAGS: u32 = FUSE_ASYNC_READ
21    | FUSE_PARALLEL_DIROPS
22    | FUSE_AUTO_INVAL_DATA
23    | FUSE_HANDLE_KILLPRIV
24    | FUSE_ASYNC_DIO
25    | FUSE_ATOMIC_O_TRUNC
26    | FUSE_BIG_WRITES;
27
28const DEFAULT_MAX_PAGES: u32 = 256;
29
30// Page size is currently hardcoded. While it could be determined from the OS, in the case of
31// virtio-fs it's not clear whether the host's or guest's page size should be used, if there's
32// a difference.
33const PAGE_SIZE: u32 = 4096;
34
35/// A FUSE session for a file system.
36///
37/// Handles negotiation and dispatching requests to the file system.
38pub struct Session {
39    fs: Box<dyn Fuse + Send + Sync>,
40    // Initialized provides a quick way to check if FUSE_INIT is expected without having to take
41    // a lock, since operations mostly don't need to access the SessionInfo.
42    initialized: atomic::AtomicBool,
43    info: RwLock<SessionInfo>,
44}
45
46impl Session {
47    /// Create a new `Session`.
48    pub fn new<T>(fs: T) -> Self
49    where
50        T: 'static + Fuse + Send + Sync,
51    {
52        Self {
53            fs: Box::new(fs),
54            initialized: atomic::AtomicBool::new(false),
55            info: RwLock::new(SessionInfo::default()),
56        }
57    }
58
59    /// Indicates whether the session has received an init request.
60    ///
61    /// Also returns `false` after the session received a destroy request.
62    pub fn is_initialized(&self) -> bool {
63        self.initialized.load(atomic::Ordering::Acquire)
64    }
65
66    /// Dispatch a FUSE request to the file system.
67    pub fn dispatch(
68        &self,
69        request: Request,
70        sender: &mut impl ReplySender,
71        mapper: Option<&dyn Mapper>,
72    ) {
73        let unique = request.unique();
74        let result = if self.is_initialized() {
75            self.dispatch_helper(request, sender, mapper)
76        } else {
77            self.dispatch_init(request, sender)
78        };
79
80        match result {
81            Err(OperationError::FsError(e)) => {
82                if let Err(e) = sender.send_error(unique, e.value()) {
83                    tracing::error!(
84                        unique,
85                        error = &e as &dyn std::error::Error,
86                        "Failed to send reply",
87                    );
88                }
89            }
90            Err(OperationError::SendError(e)) => {
91                if e.kind() == io::ErrorKind::NotFound {
92                    tracing::trace!(unique, "Request was interrupted.");
93                } else {
94                    tracing::error!(
95                        unique,
96                        error = &e as &dyn std::error::Error,
97                        "Failed to send reply",
98                    );
99                }
100            }
101            Ok(_) => (),
102        }
103    }
104
105    /// End the session.
106    ///
107    /// This puts the session in a state where it can accept another FUSE_INIT message. This allows
108    /// a virtiofs file system to be remounted after unmount.
109    ///
110    /// This invokes the file system's destroy callback if it hadn't been called already.
111    pub fn destroy(&self) {
112        if self.initialized.swap(false, atomic::Ordering::AcqRel) {
113            self.fs.destroy();
114        }
115    }
116
117    /// Perform the actual dispatch. This allows the caller to send an error reply if any operation
118    /// encounters an error.
119    fn dispatch_helper(
120        &self,
121        request: Request,
122        sender: &mut impl ReplySender,
123        mapper: Option<&dyn Mapper>,
124    ) -> Result<(), OperationError> {
125        request.log();
126
127        match request.operation() {
128            FuseOperation::Invalid => {
129                // This indicates the header could be parsed but the rest of the request could not,
130                // so send an error reply.
131                return Err(lx::Error::EIO.into());
132            }
133            FuseOperation::Error(e) => {
134                // This indicates the request was parsed but contained invalid data (e.g., a name
135                // that was too long). Return the specific error.
136                return Err((*e).into());
137            }
138            FuseOperation::Lookup { name } => {
139                let out = self.fs.lookup(&request, name)?;
140                sender.send_arg(request.unique(), out)?;
141            }
142            FuseOperation::Forget { arg } => {
143                self.fs.forget(request.node_id(), arg.nlookup);
144            }
145            FuseOperation::GetAttr { arg } => {
146                let out = self.fs.get_attr(&request, arg.getattr_flags, arg.fh)?;
147                sender.send_arg(request.unique(), out)?;
148            }
149            FuseOperation::SetAttr { arg } => {
150                let out = self.fs.set_attr(&request, arg)?;
151                sender.send_arg(request.unique(), out)?;
152            }
153            FuseOperation::ReadLink {} => {
154                let out = self.fs.read_link(&request)?;
155                sender.send_string(request.unique(), out)?;
156            }
157            FuseOperation::Symlink { name, target } => {
158                let out = self.fs.symlink(&request, name, target)?;
159                sender.send_arg(request.unique(), out)?;
160            }
161            FuseOperation::MkNod { arg, name } => {
162                let out = self.fs.mknod(&request, name, arg)?;
163                sender.send_arg(request.unique(), out)?;
164            }
165            FuseOperation::MkDir { arg, name } => {
166                let out = self.fs.mkdir(&request, name, arg)?;
167                sender.send_arg(request.unique(), out)?;
168            }
169            FuseOperation::Unlink { name } => {
170                self.fs.unlink(&request, name)?;
171                sender.send_empty(request.unique())?;
172            }
173            FuseOperation::RmDir { name } => {
174                self.fs.rmdir(&request, name)?;
175                sender.send_empty(request.unique())?;
176            }
177            FuseOperation::Rename {
178                arg,
179                name,
180                new_name,
181            } => {
182                self.fs.rename(&request, name, arg.newdir, new_name, 0)?;
183
184                sender.send_empty(request.unique())?;
185            }
186            FuseOperation::Link { arg, name } => {
187                let out = self.fs.link(&request, name, arg.oldnodeid)?;
188                sender.send_arg(request.unique(), out)?;
189            }
190            FuseOperation::Open { arg } => {
191                let out = self.fs.open(&request, arg.flags)?;
192                self.send_release_if_interrupted(&request, sender, out.fh, arg.flags, out, false)?;
193            }
194            FuseOperation::Read { arg } => {
195                let out = self.fs.read(&request, arg)?;
196                Self::send_max_size(sender, request.unique(), &out, arg.size)?;
197            }
198            FuseOperation::Write { arg, data } => {
199                let out = self.fs.write(&request, arg, data)?;
200                sender.send_arg(
201                    request.unique(),
202                    fuse_write_out {
203                        size: out.try_into().unwrap(),
204                        padding: 0,
205                    },
206                )?;
207            }
208            FuseOperation::StatFs {} => {
209                let out = self.fs.statfs(&request)?;
210                sender.send_arg(request.unique(), fuse_statfs_out { st: out })?;
211            }
212            FuseOperation::Release { arg } => {
213                self.fs.release(&request, arg)?;
214                sender.send_empty(request.unique())?;
215            }
216            FuseOperation::FSync { arg } => {
217                self.fs.fsync(&request, arg.fh, arg.fsync_flags)?;
218                sender.send_empty(request.unique())?;
219            }
220            FuseOperation::SetXAttr { arg, name, value } => {
221                self.fs.set_xattr(&request, name, value, arg.flags)?;
222                sender.send_empty(request.unique())?;
223            }
224            FuseOperation::GetXAttr { arg, name } => {
225                if arg.size == 0 {
226                    let out = self.fs.get_xattr_size(&request, name)?;
227                    sender.send_arg(
228                        request.unique(),
229                        fuse_getxattr_out {
230                            size: out,
231                            padding: 0,
232                        },
233                    )?;
234                } else {
235                    let out = self.fs.get_xattr(&request, name, arg.size)?;
236                    Self::send_max_size(sender, request.unique(), &out, arg.size)?;
237                }
238            }
239            FuseOperation::ListXAttr { arg } => {
240                if arg.size == 0 {
241                    let out = self.fs.list_xattr_size(&request)?;
242                    sender.send_arg(
243                        request.unique(),
244                        fuse_getxattr_out {
245                            size: out,
246                            padding: 0,
247                        },
248                    )?;
249                } else {
250                    let out = self.fs.list_xattr(&request, arg.size)?;
251                    Self::send_max_size(sender, request.unique(), &out, arg.size)?;
252                }
253            }
254            FuseOperation::RemoveXAttr { name } => {
255                self.fs.remove_xattr(&request, name)?;
256                sender.send_empty(request.unique())?;
257            }
258            FuseOperation::Flush { arg } => {
259                self.fs.flush(&request, arg)?;
260                sender.send_empty(request.unique())?;
261            }
262            FuseOperation::Init { arg: _ } => {
263                tracing::warn!("Duplicate init message.");
264                return Err(lx::Error::EIO.into());
265            }
266            FuseOperation::OpenDir { arg } => {
267                let out = self.fs.open_dir(&request, arg.flags)?;
268                self.send_release_if_interrupted(&request, sender, out.fh, arg.flags, out, true)?;
269            }
270            FuseOperation::ReadDir { arg } => {
271                let out = self.fs.read_dir(&request, arg)?;
272                Self::send_max_size(sender, request.unique(), &out, arg.size)?;
273            }
274            FuseOperation::ReleaseDir { arg } => {
275                self.fs.release_dir(&request, arg)?;
276                sender.send_empty(request.unique())?;
277            }
278            FuseOperation::FSyncDir { arg } => {
279                self.fs.fsync_dir(&request, arg.fh, arg.fsync_flags)?;
280                sender.send_empty(request.unique())?;
281            }
282            FuseOperation::GetLock { arg } => {
283                let out = self.fs.get_lock(&request, arg)?;
284                sender.send_arg(request.unique(), out)?;
285            }
286            FuseOperation::SetLock { arg } => {
287                self.fs.set_lock(&request, arg, false)?;
288                sender.send_empty(request.unique())?;
289            }
290            FuseOperation::SetLockSleep { arg } => {
291                self.fs.set_lock(&request, arg, true)?;
292                sender.send_empty(request.unique())?;
293            }
294            FuseOperation::Access { arg } => {
295                self.fs.access(&request, arg.mask)?;
296                sender.send_empty(request.unique())?;
297            }
298            FuseOperation::Create { arg, name } => {
299                let out = self.fs.create(&request, name, arg)?;
300                self.send_release_if_interrupted(
301                    &request,
302                    sender,
303                    out.open.fh,
304                    arg.flags,
305                    out,
306                    false,
307                )?;
308            }
309            FuseOperation::Interrupt { arg: _ } => {
310                // Interrupt is potentially complicated, and none of the sample file systems seem
311                // to use it, so it's left as TODO for now.
312                tracing::warn!("FUSE_INTERRUPT not supported.");
313                return Err(lx::Error::ENOSYS.into());
314            }
315            FuseOperation::BMap { arg } => {
316                let out = self.fs.block_map(&request, arg.block, arg.blocksize)?;
317                sender.send_arg(request.unique(), fuse_bmap_out { block: out })?;
318            }
319            FuseOperation::Destroy {} => {
320                if let Some(mapper) = mapper {
321                    mapper.clear();
322                }
323                self.destroy();
324            }
325            FuseOperation::Ioctl { arg, data } => {
326                let out = self.fs.ioctl(&request, arg, data)?;
327                if out.1.len() > arg.out_size as usize {
328                    return Err(lx::Error::EINVAL.into());
329                }
330
331                // As far as I can tell, the fields other than result are only used for CUSE.
332                sender.send_arg_data(
333                    request.unique(),
334                    fuse_ioctl_out {
335                        result: out.0,
336                        flags: 0,
337                        in_iovs: 0,
338                        out_iovs: 0,
339                    },
340                    data,
341                )?;
342            }
343            FuseOperation::Poll { arg: _ } => {
344                // Poll is not currently needed, and complicated to support. It appears to have some
345                // way of registering for later notifications, but I can't figure out how that
346                // works without libfuse source.
347                tracing::warn!("FUSE_POLL not supported.");
348                return Err(lx::Error::ENOSYS.into());
349            }
350            FuseOperation::NotifyReply { arg: _, data: _ } => {
351                // Not sure what this is. It has something to do with poll, I think.
352                tracing::warn!("FUSE_NOTIFY_REPLY not supported.");
353                return Err(lx::Error::ENOSYS.into());
354            }
355            FuseOperation::BatchForget { arg, nodes } => {
356                self.batch_forget(arg.count, nodes);
357            }
358            FuseOperation::FAllocate { arg } => {
359                self.fs.fallocate(&request, arg)?;
360                sender.send_empty(request.unique())?;
361            }
362            FuseOperation::ReadDirPlus { arg } => {
363                let out = self.fs.read_dir_plus(&request, arg)?;
364                Self::send_max_size(sender, request.unique(), &out, arg.size)?;
365            }
366            FuseOperation::Rename2 {
367                arg,
368                name,
369                new_name,
370            } => {
371                self.fs
372                    .rename(&request, name, arg.newdir, new_name, arg.flags)?;
373
374                sender.send_empty(request.unique())?;
375            }
376            FuseOperation::LSeek { arg } => {
377                let out = self.fs.lseek(&request, arg.fh, arg.offset, arg.whence)?;
378                sender.send_arg(request.unique(), fuse_lseek_out { offset: out })?;
379            }
380            FuseOperation::CopyFileRange { arg } => {
381                let out = self.fs.copy_file_range(&request, arg)?;
382                sender.send_arg(
383                    request.unique(),
384                    fuse_write_out {
385                        size: out.try_into().unwrap(),
386                        padding: 0,
387                    },
388                )?;
389            }
390            FuseOperation::SetupMapping { arg } => {
391                if let Some(mapper) = mapper {
392                    self.fs.setup_mapping(&request, mapper, arg)?;
393                    sender.send_empty(request.unique())?;
394                } else {
395                    return Err(lx::Error::ENOSYS.into());
396                }
397            }
398            FuseOperation::RemoveMapping { arg, mappings } => {
399                if let Some(mapper) = mapper {
400                    self.remove_mapping(&request, mapper, arg.count, mappings)?;
401                    sender.send_empty(request.unique())?;
402                } else {
403                    return Err(lx::Error::ENOSYS.into());
404                }
405            }
406            FuseOperation::SyncFs { _arg } => {
407                // Rely on host file system to sync data
408                sender.send_empty(request.unique())?;
409            }
410            FuseOperation::StatX { arg } => {
411                let out = self.fs.get_statx(
412                    &request,
413                    arg.fh,
414                    arg.getattr_flags,
415                    arg.flags,
416                    arg.mask.into(),
417                )?;
418                sender.send_arg(request.unique(), out)?;
419            }
420            FuseOperation::CanonicalPath {} => {
421                // Android-specific opcode used to return a guest accessible
422                // path to the file location being proxied by the fuse
423                // implementation.
424                tracing::trace!("Unsupported opcode FUSE_CANONICAL_PATH");
425                sender.send_error(request.unique(), lx::Error::ENOSYS.value())?;
426            }
427        }
428
429        Ok(())
430    }
431
432    /// Dispatch the init message.
433    fn dispatch_init(
434        &self,
435        request: Request,
436        sender: &mut impl ReplySender,
437    ) -> Result<(), OperationError> {
438        request.log();
439        let init: &fuse_init_in = if let FuseOperation::Init { arg } = request.operation() {
440            arg
441        } else {
442            tracing::error!(opcode = request.opcode(), "Expected FUSE_INIT");
443            return Err(lx::Error::EIO.into());
444        };
445
446        let mut info = self.info.write();
447        if self.is_initialized() {
448            tracing::error!("Racy FUSE_INIT requests.");
449            return Err(lx::Error::EIO.into());
450        }
451
452        let mut out = fuse_init_out::new_zeroed();
453        out.major = FUSE_KERNEL_VERSION;
454        out.minor = FUSE_KERNEL_MINOR_VERSION;
455
456        // According to the docs, if the kernel reports a higher version, the response should have
457        // only the desired version set and the kernel will resend FUSE_INIT with that version.
458        if init.major > FUSE_KERNEL_VERSION {
459            sender.send_arg(request.unique(), out)?;
460            return Ok(());
461        }
462
463        // Don't bother supporting old versions. Version 7.27 is what kernel 4.19 uses, and can
464        // be supported without needing to change the daemon's behavior for compatibility.
465        if init.major < FUSE_KERNEL_VERSION || init.minor < 27 {
466            tracing::error!(
467                major = init.major,
468                minor = init.minor,
469                "Got unsupported kernel version",
470            );
471            return Err(lx::Error::EIO.into());
472        }
473
474        // Prepare the session info and call the file system to negotiate.
475        info.major = init.major;
476        info.minor = init.minor;
477        info.max_readahead = init.max_readahead;
478        info.capable = init.flags;
479        info.want = DEFAULT_FLAGS & init.flags;
480        info.time_gran = 1;
481        info.max_write = DEFAULT_MAX_PAGES * PAGE_SIZE;
482        self.fs.init(&mut info);
483
484        assert!(info.want & !info.capable == 0);
485
486        // Report the negotiated values back to the client.
487        // TODO: Set map_alignment for DAX.
488        out.max_readahead = info.max_readahead;
489        out.flags = info.want;
490        out.max_background = info.max_background;
491        out.congestion_threshold = info.congestion_threshold;
492        out.max_write = info.max_write;
493        out.time_gran = info.time_gran;
494        out.max_pages = ((info.max_write - 1) / PAGE_SIZE - 1).try_into().unwrap();
495
496        sender.send_arg(request.unique(), out)?;
497
498        // Indicate other requests can be received now.
499        self.initialized.store(true, atomic::Ordering::Release);
500        Ok(())
501    }
502
503    /// Send a reply and call the release method if the reply was interrupted.
504    fn send_release_if_interrupted<
505        TArg: zerocopy::IntoBytes + std::fmt::Debug + Immutable + KnownLayout,
506    >(
507        &self,
508        request: &Request,
509        sender: &mut impl ReplySender,
510        fh: u64,
511        flags: u32,
512        arg: TArg,
513        dir: bool,
514    ) -> lx::Result<()> {
515        if let Err(e) = sender.send_arg(request.unique(), arg) {
516            // ENOENT means the request was interrupted, and the kernel will not call
517            // release, so do it now.
518            if e.kind() == io::ErrorKind::NotFound {
519                let arg = fuse_release_in {
520                    fh,
521                    flags,
522                    release_flags: 0,
523                    lock_owner: 0,
524                };
525
526                if dir {
527                    self.fs.release_dir(request, &arg)?;
528                } else {
529                    self.fs.release(request, &arg)?;
530                }
531            } else {
532                return Err(e.into());
533            }
534        }
535
536        Ok(())
537    }
538
539    /// Send a reply, validating it doesn't exceed the requested size.
540    ///
541    /// If it exceeds the maximum size, this causes a panic because that's a bug in the file system.
542    fn send_max_size(
543        sender: &mut impl ReplySender,
544        unique: u64,
545        data: &[u8],
546        max_size: u32,
547    ) -> Result<(), OperationError> {
548        assert!(data.len() <= max_size as usize);
549        sender.send_data(unique, data)?;
550        Ok(())
551    }
552
553    /// Process `FUSE_BATCH_FORGET` by repeatedly calling `forget`.
554    fn batch_forget(&self, count: u32, mut nodes: &[u8]) {
555        for _ in 0..count {
556            let forget: fuse_forget_one = match nodes.read_type() {
557                Ok(f) => f,
558                Err(_) => break,
559            };
560
561            self.fs.forget(forget.nodeid, forget.nlookup);
562        }
563    }
564
565    /// Remove multiple DAX mappings.
566    fn remove_mapping(
567        &self,
568        request: &Request,
569        mapper: &dyn Mapper,
570        count: u32,
571        mut mappings: &[u8],
572    ) -> lx::Result<()> {
573        for _ in 0..count {
574            let mapping: fuse_removemapping_one = mappings.read_type()?;
575            self.fs
576                .remove_mapping(request, mapper, mapping.moffset, mapping.len)?;
577        }
578
579        Ok(())
580    }
581}
582
583/// Provides information about a session. Public fields may be modified during `init`.
584#[derive(Default)]
585pub struct SessionInfo {
586    major: u32,
587    minor: u32,
588    pub max_readahead: u32,
589    capable: u32,
590    pub want: u32,
591    pub max_background: u16,
592    pub congestion_threshold: u16,
593    pub max_write: u32,
594    pub time_gran: u32,
595}
596
597impl SessionInfo {
598    pub fn major(&self) -> u32 {
599        self.major
600    }
601
602    pub fn minor(&self) -> u32 {
603        self.minor
604    }
605
606    pub fn capable(&self) -> u32 {
607        self.capable
608    }
609}
610
611#[derive(Debug, Error)]
612enum OperationError {
613    #[error("File system error")]
614    FsError(#[from] lx::Error),
615    #[error("Send error")]
616    SendError(#[from] io::Error),
617}
618
619#[cfg(test)]
620mod tests {
621    use super::*;
622    use crate::request::tests::*;
623    use parking_lot::Mutex;
624    use std::sync::Arc;
625
626    #[test]
627    fn dispatch_error_name_too_long() {
628        let fs = TestFs::default();
629        let session = Session::new(fs);
630
631        // Initialize the session first
632        let mut init_sender = MockSender::default();
633        session.dispatch(
634            Request::new(FUSE_INIT_REQUEST).unwrap(),
635            &mut init_sender,
636            None,
637        );
638        assert!(session.is_initialized());
639
640        // Create a LOOKUP request with a name that's too long (256 bytes, exceeds NAME_MAX of 255)
641        let mut error_sender = ErrorCheckingSender::default();
642        let lookup_data = make_lookup_name_too_long();
643        let request = Request::new(lookup_data.as_slice()).unwrap();
644
645        // Verify the operation is Error(ENAMETOOLONG)
646        assert!(
647            matches!(request.operation(), FuseOperation::Error(e) if *e == lx::Error::ENAMETOOLONG)
648        );
649
650        session.dispatch(request, &mut error_sender, None);
651
652        // Verify that an error reply was sent with ENAMETOOLONG (36)
653        assert_eq!(
654            error_sender.last_error,
655            Some(lx::Error::ENAMETOOLONG.value())
656        );
657    }
658
659    #[test]
660    fn dispatch() {
661        let mut sender = MockSender::default();
662        let fs = TestFs::default();
663        let state = fs.state.clone();
664        let session = Session::new(fs);
665        assert!(!session.is_initialized());
666        let request = Request::new(FUSE_INIT_REQUEST).unwrap();
667        session.dispatch(request, &mut sender, None);
668        assert_eq!(state.lock().called, INIT_CALLED);
669        assert!(session.is_initialized());
670        session.dispatch(
671            Request::new(FUSE_GETATTR_REQUEST).unwrap(),
672            &mut sender,
673            None,
674        );
675        assert_eq!(state.lock().called, INIT_CALLED | GETATTR_CALLED);
676
677        session.dispatch(
678            Request::new(FUSE_LOOKUP_REQUEST).unwrap(),
679            &mut sender,
680            None,
681        );
682        assert_eq!(
683            state.lock().called,
684            INIT_CALLED | GETATTR_CALLED | LOOKUP_CALLED
685        );
686    }
687
688    #[derive(Default)]
689    struct State {
690        called: u32,
691    }
692
693    #[derive(Default)]
694    struct TestFs {
695        state: Arc<Mutex<State>>,
696    }
697
698    impl Fuse for TestFs {
699        fn init(&self, info: &mut SessionInfo) {
700            assert_eq!(self.state.lock().called & INIT_CALLED, 0);
701            assert_eq!(info.major(), 7);
702            assert_eq!(info.minor(), 27);
703            assert_eq!(info.capable(), 0x3FFFFB);
704            assert_eq!(info.want, 0xC9029);
705            assert_eq!(info.max_readahead, 131072);
706            assert_eq!(info.max_background, 0);
707            assert_eq!(info.max_write, 1048576);
708            assert_eq!(info.congestion_threshold, 0);
709            assert_eq!(info.time_gran, 1);
710            self.state.lock().called |= INIT_CALLED;
711        }
712
713        fn get_attr(&self, request: &Request, flags: u32, fh: u64) -> lx::Result<fuse_attr_out> {
714            assert_eq!(self.state.lock().called & GETATTR_CALLED, 0);
715            assert_eq!(request.node_id(), 1);
716            assert_eq!(flags, 0);
717            assert_eq!(fh, 0);
718            let mut attr = fuse_attr_out::new_zeroed();
719            attr.attr.ino = 1;
720            attr.attr.mode = lx::S_IFDIR | 0o755;
721            attr.attr.nlink = 2;
722            attr.attr_valid = 1;
723            self.state.lock().called |= GETATTR_CALLED;
724            Ok(attr)
725        }
726
727        fn lookup(&self, request: &Request, name: &lx::LxStr) -> lx::Result<fuse_entry_out> {
728            assert_eq!(self.state.lock().called & LOOKUP_CALLED, 0);
729            assert_eq!(request.node_id(), 1);
730            assert_eq!(name, "hello");
731            self.state.lock().called |= LOOKUP_CALLED;
732            let mut attr = fuse_attr::new_zeroed();
733            attr.ino = 2;
734            attr.mode = lx::S_IFREG | 0o644;
735            attr.nlink = 1;
736            attr.size = 13;
737            Ok(fuse_entry_out {
738                nodeid: 2,
739                generation: 0,
740                entry_valid: 1,
741                entry_valid_nsec: 0,
742                attr_valid: 1,
743                attr_valid_nsec: 0,
744                attr,
745            })
746        }
747    }
748
749    #[derive(Default)]
750    struct MockSender {
751        state: u32,
752    }
753
754    impl ReplySender for MockSender {
755        fn send(&mut self, bufs: &[io::IoSlice<'_>]) -> io::Result<()> {
756            let flat: Vec<u8> = bufs.iter().flat_map(|s| s.iter()).copied().collect();
757            match self.state {
758                0 => assert_eq!(flat, INIT_REPLY),
759                1 => assert_eq!(flat, GETATTR_REPLY),
760                2 => assert_eq!(flat, LOOKUP_REPLY),
761                _ => panic!("Unexpected send."),
762            }
763
764            self.state += 1;
765            Ok(())
766        }
767    }
768
769    const INIT_CALLED: u32 = 0x1;
770    const GETATTR_CALLED: u32 = 0x2;
771    const LOOKUP_CALLED: u32 = 0x4;
772
773    const INIT_REPLY: &[u8] = &[
774        80, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 31, 0, 0, 0, 0, 0, 2, 0, 41,
775        144, 12, 0, 0, 0, 0, 0, 0, 0, 16, 0, 1, 0, 0, 0, 254, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
776        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
777    ];
778
779    const GETATTR_REPLY: &[u8] = &[
780        120, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
781        0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
782        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
783        0, 0, 237, 65, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
784        0,
785    ];
786
787    const LOOKUP_REPLY: &[u8] = &[
788        144, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
789        0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0,
790        0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
791        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 164, 129, 0,
792        0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
793    ];
794
795    /// A ReplySender that tracks error responses for testing
796    #[derive(Default)]
797    struct ErrorCheckingSender {
798        last_error: Option<i32>,
799    }
800
801    impl ReplySender for ErrorCheckingSender {
802        fn send(&mut self, bufs: &[io::IoSlice<'_>]) -> io::Result<()> {
803            // Parse the fuse_out_header to check for errors
804            let flat: Vec<u8> = bufs.iter().flat_map(|s| s.iter()).copied().collect();
805            if flat.len() >= 16 {
806                // fuse_out_header: len (4), error (4), unique (8)
807                let error = i32::from_ne_bytes([flat[4], flat[5], flat[6], flat[7]]);
808                if error != 0 {
809                    self.last_error = Some(-error); // Error is stored as negative in header
810                }
811            }
812            Ok(())
813        }
814    }
815
816    /// Creates a FUSE_LOOKUP request with a name that's too long (256 bytes, exceeds NAME_MAX of 255)
817    fn make_lookup_name_too_long() -> Vec<u8> {
818        let mut data = vec![0u8; 297]; // 40 byte header + 256 byte name + 1 null terminator
819
820        // fuse_in_header (40 bytes):
821        // len: u32 = 297 (0x129)
822        data[0] = 0x29;
823        data[1] = 0x01;
824        data[2] = 0x00;
825        data[3] = 0x00;
826
827        // opcode: u32 = 1 (FUSE_LOOKUP)
828        data[4] = 0x01;
829        data[5] = 0x00;
830        data[6] = 0x00;
831        data[7] = 0x00;
832
833        // unique: u64 = 99
834        data[8] = 99;
835        data[9] = 0x00;
836        data[10] = 0x00;
837        data[11] = 0x00;
838        data[12] = 0x00;
839        data[13] = 0x00;
840        data[14] = 0x00;
841        data[15] = 0x00;
842
843        // nodeid: u64 = 1
844        data[16] = 0x01;
845        data[17] = 0x00;
846        data[18] = 0x00;
847        data[19] = 0x00;
848        data[20] = 0x00;
849        data[21] = 0x00;
850        data[22] = 0x00;
851        data[23] = 0x00;
852
853        // uid: u32 = 0
854        // gid: u32 = 0
855        // pid: u32 = 971 (0x3CB)
856        data[32] = 0xCB;
857        data[33] = 0x03;
858        data[34] = 0x00;
859        data[35] = 0x00;
860
861        // padding: u32 = 0
862
863        // Name: 256 'a' characters (0x61) starting at byte 40
864        for item in data.iter_mut().take(296).skip(40) {
865            *item = 0x61; // 'a'
866        }
867        // Null terminator at byte 296
868        data[296] = 0x00;
869
870        data
871    }
872}