@@ -227,17 +227,21 @@ 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 the session is waited for, so that
231+ /// `Filesystem::destroy` has run when drop returns - except in the cases documented
232+ /// on [`BackgroundSession`], where the session thread is detached instead.
231233 pub fn spawn ( self ) -> io:: Result < BackgroundSession > {
232234 let sender = self . ch . sender ( ) ;
235+ let fuse_device = self . ch . device ( ) ;
233236 // Take the fuse_session, so that we can unmount it
234237 let mount = std:: mem:: take ( & mut * self . mount . mount . lock ( ) ) ;
235238 let guard = thread:: Builder :: new ( )
236239 . name ( "fuser-bg" . to_string ( ) )
237240 . spawn ( move || self . run ( ) ) ?;
238241 Ok ( BackgroundSession {
239- guard,
242+ guard : Some ( guard ) ,
240243 sender,
244+ fuse_device,
241245 mount,
242246 } )
243247 }
@@ -565,33 +569,69 @@ impl<FS: Filesystem> SessionEventLoop<FS> {
565569}
566570
567571/// The background session data structure
572+ ///
573+ /// Dropping this unmounts the filesystem and blocks until the session has ended,
574+ /// which guarantees that `Filesystem::destroy` has run when drop returns. Because
575+ /// of that, it must not be dropped from within a filesystem callback, which runs
576+ /// on the session thread being waited for. For a session created via
577+ /// `Session::from_fd` there is no mount to remove, so dropping cannot end the
578+ /// session and leaves its thread detached; end the session externally and use
579+ /// `join` to wait for it instead. The thread is likewise left detached, with a
580+ /// warning, rather than waiting for a session that may never end when the
581+ /// unmount fails, or when the kernel connection is still alive a few seconds
582+ /// after a successful unmount request (e.g. a lazily unmounted filesystem that
583+ /// is still in use, or an unmount helper that failed without reporting it).
568584#[ derive( Debug ) ]
569585pub struct BackgroundSession {
570- /// Thread guard of the background session
571- pub guard : JoinHandle < io:: Result < ( ) > > ,
586+ /// Thread guard of the background session. None once joined.
587+ guard : Option < JoinHandle < io:: Result < ( ) > > > ,
572588 /// Object for creating Notifiers for client use
573589 sender : ChannelSender ,
590+ /// The FUSE device fd of the session's kernel connection
591+ fuse_device : Arc < DevFuse > ,
574592 /// Ensures the filesystem is unmounted when the session ends
575593 mount : Option < Mount > ,
576594}
577595
596+ /// How long teardown waits for the kernel connection to end after a successful
597+ /// unmount request, before giving up on joining the session thread. The
598+ /// connection can end asynchronously (auto_unmount, kernel teardown), but a few
599+ /// seconds only pass without it ending when the session cannot be waited for,
600+ /// e.g. a lazily unmounted filesystem still in use
601+ const UNMOUNT_WAIT : std:: time:: Duration = std:: time:: Duration :: from_secs ( 5 ) ;
602+
578603impl BackgroundSession {
579- /// Unmount the filesystem and join the background thread.
604+ /// Unmount the filesystem and join the background thread, returning the
605+ /// session result. `Filesystem::destroy` has run when this returns.
580606 pub fn umount_and_join ( mut self ) -> io:: Result < ( ) > {
581607 if let Some ( mount) = self . mount . take ( ) {
582- mount. umount ( ) ?;
608+ if let Err ( err) = mount. umount ( ) {
609+ // The filesystem is still mounted and the session still running,
610+ // so joining would block indefinitely; leave the thread detached,
611+ // as before
612+ self . guard . take ( ) ;
613+ return Err ( err) ;
614+ }
583615 }
584- self . join ( )
616+ self . join_impl ( )
585617 }
586618
587619 /// Returns an object that can be used to send notifications to the kernel
588620 pub fn notifier ( & self ) -> Notifier {
589621 Notifier :: new ( self . sender . clone ( ) )
590622 }
591623
592- /// Join the filesystem thread.
593- pub fn join ( self ) -> io:: Result < ( ) > {
594- self . guard
624+ /// Join the filesystem thread without unmounting first: blocks until
625+ /// something else ends the session, e.g. an external unmount.
626+ pub fn join ( mut self ) -> io:: Result < ( ) > {
627+ self . join_impl ( )
628+ }
629+
630+ fn join_impl ( & mut self ) -> io:: Result < ( ) > {
631+ let Some ( guard) = self . guard . take ( ) else {
632+ return Ok ( ( ) ) ;
633+ } ;
634+ guard
595635 . join ( )
596636 . map_err ( |_panic : Box < dyn std:: any:: Any + Send > | {
597637 io:: Error :: new (
@@ -602,6 +642,36 @@ impl BackgroundSession {
602642 }
603643}
604644
645+ impl Drop for BackgroundSession {
646+ fn drop ( & mut self ) {
647+ // Unmount and wait for the session to end, so that Filesystem::destroy
648+ // has run by the time drop returns (issues #239 and #411). Without the
649+ // join, the process could exit before the detached session thread got
650+ // around to calling destroy
651+ let Some ( mount) = self . mount . take ( ) else {
652+ // No mount to remove (Session::from_fd): nothing here can end the
653+ // session, so joining could block forever; leave the thread detached
654+ return ;
655+ } ;
656+ if let Err ( err) = mount. umount ( ) {
657+ // Still mounted, so the session is still running and joining would
658+ // block indefinitely
659+ warn ! ( "Failed to unmount filesystem during drop: {err}" ) ;
660+ return ;
661+ }
662+ // The connection can end asynchronously (e.g. auto_unmount), and some
663+ // unmount helpers cannot report failures. Wait boundedly, and if the
664+ // session lives on, detach rather than block indefinitely
665+ if !crate :: mnt:: connection_ended ( & self . fuse_device , UNMOUNT_WAIT ) {
666+ warn ! ( "FUSE connection still alive after unmount; detaching session thread" ) ;
667+ return ;
668+ }
669+ if let Err ( err) = self . join_impl ( ) {
670+ warn ! ( "Session ended with an error during drop: {err}" ) ;
671+ }
672+ }
673+ }
674+
605675// The abort test uses fusectl, which only exists on Linux; on other targets the
606676// whole module would be dead code
607677#[ cfg( all( test, target_os = "linux" ) ) ]
@@ -675,6 +745,40 @@ mod test {
675745 ManuallyDrop :: into_inner ( tmp) ;
676746 }
677747
748+ /// Dropping a BackgroundSession must unmount the filesystem and wait for the
749+ /// session to end, so that Filesystem::destroy has run when drop returns.
750+ #[ test]
751+ fn drop_waits_for_destroy ( ) {
752+ struct DestroyFs {
753+ destroyed : Arc < std:: sync:: atomic:: AtomicBool > ,
754+ }
755+ impl Filesystem for DestroyFs {
756+ fn destroy ( & mut self ) {
757+ // Give drop a chance to return early: without the join, drop
758+ // consistently wins this race and the assertion below fails
759+ std:: thread:: sleep ( std:: time:: Duration :: from_millis ( 200 ) ) ;
760+ self . destroyed
761+ . store ( true , std:: sync:: atomic:: Ordering :: SeqCst ) ;
762+ }
763+ }
764+
765+ let tmp = ManuallyDrop :: new ( tempfile:: tempdir ( ) . unwrap ( ) ) ;
766+ let destroyed = Arc :: new ( std:: sync:: atomic:: AtomicBool :: new ( false ) ) ;
767+ let fs = DestroyFs {
768+ destroyed : destroyed. clone ( ) ,
769+ } ;
770+ let bg = Session :: new ( fs, tmp. path ( ) , & Config :: default ( ) )
771+ . unwrap ( )
772+ . spawn ( )
773+ . unwrap ( ) ;
774+ drop ( bg) ;
775+ assert ! (
776+ destroyed. load( std:: sync:: atomic:: Ordering :: SeqCst ) ,
777+ "destroy() must have been called by the time drop returns"
778+ ) ;
779+ ManuallyDrop :: into_inner ( tmp) ;
780+ }
781+
678782 /// The fusectl abort file for the FUSE mount at `mountpoint`: the connection
679783 /// directory is named after the mount's anonymous device number.
680784 fn fusectl_abort_path ( mountpoint : & Path ) -> Option < std:: path:: PathBuf > {
0 commit comments