@@ -227,17 +227,20 @@ impl<FS: Filesystem> Session<FS> {
227227 }
228228
229229 /// Run the session loop in a background thread. If the returned handle is dropped,
230- /// the filesystem is unmounted and the given session ends.
230+ /// the filesystem is unmounted, and this blocks until the session has ended and
231+ /// `Filesystem::destroy` has run.
231232 pub fn spawn ( self ) -> io:: Result < BackgroundSession > {
232233 let sender = self . ch . sender ( ) ;
234+ let fuse_device = self . ch . device ( ) ;
233235 // Take the fuse_session, so that we can unmount it
234236 let mount = std:: mem:: take ( & mut * self . mount . mount . lock ( ) ) ;
235237 let guard = thread:: Builder :: new ( )
236238 . name ( "fuser-bg" . to_string ( ) )
237239 . spawn ( move || self . run ( ) ) ?;
238240 Ok ( BackgroundSession {
239- guard,
241+ guard : Some ( guard ) ,
240242 sender,
243+ fuse_device,
241244 mount,
242245 } )
243246 }
@@ -565,33 +568,69 @@ impl<FS: Filesystem> SessionEventLoop<FS> {
565568}
566569
567570/// The background session data structure
571+ ///
572+ /// Dropping this unmounts the filesystem and blocks until the session has ended,
573+ /// which guarantees that `Filesystem::destroy` has run when drop returns. Because
574+ /// of that, it must not be dropped from within a filesystem callback, which runs
575+ /// on the session thread being waited for. For a session created via
576+ /// `Session::from_fd` there is no mount to remove, so dropping cannot end the
577+ /// session and leaves its thread detached; end the session externally and use
578+ /// `join` to wait for it instead. The thread is likewise left detached, with a
579+ /// warning, rather than waiting for a session that may never end when the
580+ /// unmount fails, or when the kernel connection is still alive a few seconds
581+ /// after a successful unmount request (e.g. a lazily unmounted filesystem that
582+ /// is still in use, or an unmount helper that failed without reporting it).
568583#[ derive( Debug ) ]
569584pub struct BackgroundSession {
570- /// Thread guard of the background session
571- pub guard : JoinHandle < io:: Result < ( ) > > ,
585+ /// Thread guard of the background session. None once joined.
586+ guard : Option < JoinHandle < io:: Result < ( ) > > > ,
572587 /// Object for creating Notifiers for client use
573588 sender : ChannelSender ,
589+ /// The FUSE device fd of the session's kernel connection
590+ fuse_device : Arc < DevFuse > ,
574591 /// Ensures the filesystem is unmounted when the session ends
575592 mount : Option < Mount > ,
576593}
577594
595+ /// How long teardown waits for the kernel connection to end after a successful
596+ /// unmount request, before giving up on joining the session thread. The
597+ /// connection can end asynchronously (auto_unmount, kernel teardown), but a few
598+ /// seconds only pass without it ending when the session cannot be waited for,
599+ /// e.g. a lazily unmounted filesystem still in use
600+ const UNMOUNT_WAIT : std:: time:: Duration = std:: time:: Duration :: from_secs ( 5 ) ;
601+
578602impl BackgroundSession {
579- /// Unmount the filesystem and join the background thread.
603+ /// Unmount the filesystem and join the background thread, returning the
604+ /// session result. `Filesystem::destroy` has run when this returns.
580605 pub fn umount_and_join ( mut self ) -> io:: Result < ( ) > {
581606 if let Some ( mount) = self . mount . take ( ) {
582- mount. umount ( ) ?;
607+ if let Err ( err) = mount. umount ( ) {
608+ // The filesystem is still mounted and the session still running,
609+ // so joining would block indefinitely; leave the thread detached,
610+ // as before
611+ self . guard . take ( ) ;
612+ return Err ( err) ;
613+ }
583614 }
584- self . join ( )
615+ self . join_impl ( )
585616 }
586617
587618 /// Returns an object that can be used to send notifications to the kernel
588619 pub fn notifier ( & self ) -> Notifier {
589620 Notifier :: new ( self . sender . clone ( ) )
590621 }
591622
592- /// Join the filesystem thread.
593- pub fn join ( self ) -> io:: Result < ( ) > {
594- self . guard
623+ /// Join the filesystem thread without unmounting first: blocks until
624+ /// something else ends the session, e.g. an external unmount.
625+ pub fn join ( mut self ) -> io:: Result < ( ) > {
626+ self . join_impl ( )
627+ }
628+
629+ fn join_impl ( & mut self ) -> io:: Result < ( ) > {
630+ let Some ( guard) = self . guard . take ( ) else {
631+ return Ok ( ( ) ) ;
632+ } ;
633+ guard
595634 . join ( )
596635 . map_err ( |_panic : Box < dyn std:: any:: Any + Send > | {
597636 io:: Error :: new (
@@ -602,6 +641,36 @@ impl BackgroundSession {
602641 }
603642}
604643
644+ impl Drop for BackgroundSession {
645+ fn drop ( & mut self ) {
646+ // Unmount and wait for the session to end, so that Filesystem::destroy
647+ // has run by the time drop returns (issues #239 and #411). Without the
648+ // join, the process could exit before the detached session thread got
649+ // around to calling destroy
650+ let Some ( mount) = self . mount . take ( ) else {
651+ // No mount to remove (Session::from_fd): nothing here can end the
652+ // session, so joining could block forever; leave the thread detached
653+ return ;
654+ } ;
655+ if let Err ( err) = mount. umount ( ) {
656+ // Still mounted, so the session is still running and joining would
657+ // block indefinitely
658+ warn ! ( "Failed to unmount filesystem during drop: {err}" ) ;
659+ return ;
660+ }
661+ // The connection can end asynchronously (e.g. auto_unmount), and some
662+ // unmount helpers cannot report failures. Wait boundedly, and if the
663+ // session lives on, detach rather than block indefinitely
664+ if !crate :: mnt:: connection_ended ( & self . fuse_device , UNMOUNT_WAIT ) {
665+ warn ! ( "FUSE connection still alive after unmount; detaching session thread" ) ;
666+ return ;
667+ }
668+ if let Err ( err) = self . join_impl ( ) {
669+ warn ! ( "Session ended with an error during drop: {err}" ) ;
670+ }
671+ }
672+ }
673+
605674// The abort test uses fusectl, which only exists on Linux; on other targets the
606675// whole module would be dead code
607676#[ cfg( all( test, target_os = "linux" ) ) ]
@@ -675,6 +744,42 @@ mod test {
675744 ManuallyDrop :: into_inner ( tmp) ;
676745 }
677746
747+ /// Dropping a BackgroundSession must unmount the filesystem and wait for the
748+ /// session to end, so that Filesystem::destroy has run when drop returns
749+ /// (issues #239 and #411). Previously the session thread was detached, and
750+ /// destroy raced (or lost to) process exit.
751+ #[ test]
752+ fn drop_waits_for_destroy ( ) {
753+ struct DestroyFs {
754+ destroyed : Arc < std:: sync:: atomic:: AtomicBool > ,
755+ }
756+ impl Filesystem for DestroyFs {
757+ fn destroy ( & mut self ) {
758+ // Give drop a chance to return early: without the join, drop
759+ // consistently wins this race and the assertion below fails
760+ std:: thread:: sleep ( std:: time:: Duration :: from_millis ( 200 ) ) ;
761+ self . destroyed
762+ . store ( true , std:: sync:: atomic:: Ordering :: SeqCst ) ;
763+ }
764+ }
765+
766+ let tmp = ManuallyDrop :: new ( tempfile:: tempdir ( ) . unwrap ( ) ) ;
767+ let destroyed = Arc :: new ( std:: sync:: atomic:: AtomicBool :: new ( false ) ) ;
768+ let fs = DestroyFs {
769+ destroyed : destroyed. clone ( ) ,
770+ } ;
771+ let bg = Session :: new ( fs, tmp. path ( ) , & Config :: default ( ) )
772+ . unwrap ( )
773+ . spawn ( )
774+ . unwrap ( ) ;
775+ drop ( bg) ;
776+ assert ! (
777+ destroyed. load( std:: sync:: atomic:: Ordering :: SeqCst ) ,
778+ "destroy() must have been called by the time drop returns"
779+ ) ;
780+ ManuallyDrop :: into_inner ( tmp) ;
781+ }
782+
678783 /// The fusectl abort file for the FUSE mount at `mountpoint`: the connection
679784 /// directory is named after the mount's anonymous device number.
680785 fn fusectl_abort_path ( mountpoint : & Path ) -> Option < std:: path:: PathBuf > {
0 commit comments