@@ -345,7 +345,7 @@ impl<FS: Filesystem> Session<FS> {
345345 // Read the init request from the kernel
346346 let size = match self . ch . receive_retrying ( buf) {
347347 Ok ( size) => size,
348- Err ( nix:: errno:: Errno :: ENODEV ) => {
348+ Err ( nix:: errno:: Errno :: ENODEV | nix :: errno :: Errno :: ECONNABORTED ) => {
349349 return Err ( io:: Error :: new (
350350 io:: ErrorKind :: NotConnected ,
351351 "FUSE device disconnected during handshake" ,
@@ -553,7 +553,11 @@ impl<FS: Filesystem> SessionEventLoop<FS> {
553553 ) ) ;
554554 }
555555 } ,
556- Err ( nix:: errno:: Errno :: ENODEV ) => return Ok ( ( ) ) ,
556+ // The kernel returns ENODEV when the filesystem was unmounted, or
557+ // ECONNABORTED when the connection was aborted with FUSE_ABORT_ERROR
558+ // negotiated. Either way the connection is gone: a normal end of the
559+ // session, not an operation failure (issue #212)
560+ Err ( nix:: errno:: Errno :: ENODEV | nix:: errno:: Errno :: ECONNABORTED ) => return Ok ( ( ) ) ,
557561 Err ( err) => return Err ( err. into ( ) ) ,
558562 }
559563 }
@@ -597,3 +601,102 @@ impl BackgroundSession {
597601 } ) ?
598602 }
599603}
604+
605+ // The abort test uses fusectl, which only exists on Linux; on other targets the
606+ // whole module would be dead code
607+ #[ cfg( all( test, target_os = "linux" ) ) ]
608+ mod test {
609+ use std:: io:: Write ;
610+ use std:: mem:: ManuallyDrop ;
611+
612+ use super :: * ;
613+ use crate :: Config ;
614+ use crate :: InitFlags ;
615+ use crate :: KernelConfig ;
616+ use crate :: Request ;
617+
618+ /// A filesystem that requests FUSE_ABORT_ERROR during init, so that after an
619+ /// abort the FUSE device fails reads with ECONNABORTED instead of ENODEV.
620+ struct AbortErrorFs ;
621+
622+ impl Filesystem for AbortErrorFs {
623+ fn init ( & mut self , _req : & Request , config : & mut KernelConfig ) -> io:: Result < ( ) > {
624+ // Ignore the error: on kernels without FUSE_ABORT_ERROR an abort
625+ // yields ENODEV, which ends the session cleanly anyway
626+ let _ = config. add_capabilities ( InitFlags :: FUSE_ABORT_ERROR ) ;
627+ Ok ( ( ) )
628+ }
629+ }
630+
631+ /// Aborting the connection (via fusectl) with FUSE_ABORT_ERROR negotiated makes
632+ /// the FUSE device return ECONNABORTED. The session loop must treat that as a
633+ /// normal end of the session, so that umount_and_join() reports success rather
634+ /// than an error for an administratively aborted filesystem (issue #212).
635+ #[ test]
636+ fn session_ends_cleanly_after_abort ( ) {
637+ // Leak the directory on failure: it may still be a (dead) mountpoint
638+ let tmp = ManuallyDrop :: new ( tempfile:: tempdir ( ) . unwrap ( ) ) ;
639+ // The mount table lists the canonical path; resolve while it is a plain dir
640+ let mountpoint = tmp. path ( ) . canonicalize ( ) . unwrap ( ) ;
641+
642+ let session = Session :: new ( AbortErrorFs , & mountpoint, & Config :: default ( ) ) . unwrap ( ) ;
643+ let bg = session. spawn ( ) . unwrap ( ) ;
644+
645+ // Wait until the handshake completed: the kernel forwards filesystem
646+ // requests only after the init reply was processed, so a completed
647+ // operation (the errno does not matter) proves the loop is past it
648+ let _ = std:: fs:: metadata ( & mountpoint) ;
649+
650+ let Some ( abort_path) = fusectl_abort_path ( & mountpoint) else {
651+ eprintln ! ( "skipping session_ends_cleanly_after_abort: fusectl not available" ) ;
652+ bg. umount_and_join ( ) . unwrap ( ) ;
653+ ManuallyDrop :: into_inner ( tmp) ;
654+ return ;
655+ } ;
656+ std:: fs:: OpenOptions :: new ( )
657+ . write ( true )
658+ . open ( abort_path)
659+ . unwrap ( )
660+ . write_all ( b"1" )
661+ . unwrap ( ) ;
662+
663+ bg. umount_and_join ( )
664+ . expect ( "session must end cleanly after the connection was aborted" ) ;
665+
666+ // Teardown intentionally leaves the dead mount in the mount table (the
667+ // same as libfuse); detach it so the tempdir can be removed
668+ let _ = nix:: mount:: umount2 ( & mountpoint, nix:: mount:: MntFlags :: MNT_DETACH ) ;
669+ for fusermount in [ "fusermount3" , "fusermount" ] {
670+ let _ = std:: process:: Command :: new ( fusermount)
671+ . args ( [ "-u" , "-q" , "-z" , "--" ] )
672+ . arg ( & mountpoint)
673+ . status ( ) ;
674+ }
675+ ManuallyDrop :: into_inner ( tmp) ;
676+ }
677+
678+ /// The fusectl abort file for the FUSE mount at `mountpoint`: the connection
679+ /// directory is named after the mount's anonymous device number.
680+ fn fusectl_abort_path ( mountpoint : & Path ) -> Option < std:: path:: PathBuf > {
681+ let mountinfo = std:: fs:: read_to_string ( "/proc/self/mountinfo" ) . ok ( ) ?;
682+ let mut device = None ;
683+ for line in mountinfo. lines ( ) {
684+ let mut fields = line. split ( ' ' ) ;
685+ let ( Some ( dev) , Some ( path) ) = ( fields. nth ( 2 ) , fields. nth ( 1 ) ) else {
686+ continue ;
687+ } ;
688+ // mountinfo octal-escapes special characters; the tempdir path has none.
689+ // Later entries are mounted on top of earlier ones, so the last match wins
690+ if Path :: new ( path) == mountpoint {
691+ let ( major, minor) = dev. split_once ( ':' ) ?;
692+ let ( major, minor) : ( u64 , u64 ) = ( major. parse ( ) . ok ( ) ?, minor. parse ( ) . ok ( ) ?) ;
693+ // fusectl names the directory with the raw kernel-internal
694+ // device number, (major << 20) | minor: fuse_ctl_add_conn()
695+ // prints fc->dev (= sb->s_dev) without re-encoding it
696+ device = Some ( ( major << 20 ) | minor) ;
697+ }
698+ }
699+ let path = std:: path:: PathBuf :: from ( format ! ( "/sys/fs/fuse/connections/{}/abort" , device?) ) ;
700+ path. exists ( ) . then_some ( path)
701+ }
702+ }
0 commit comments