@@ -227,7 +227,8 @@ 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 ( ) ;
233234 // Take the fuse_session, so that we can unmount it
@@ -236,7 +237,7 @@ impl<FS: Filesystem> Session<FS> {
236237 . name ( "fuser-bg" . to_string ( ) )
237238 . spawn ( move || self . run ( ) ) ?;
238239 Ok ( BackgroundSession {
239- guard,
240+ guard : Some ( guard ) ,
240241 sender,
241242 mount,
242243 } )
@@ -565,33 +566,57 @@ impl<FS: Filesystem> SessionEventLoop<FS> {
565566}
566567
567568/// The background session data structure
569+ ///
570+ /// Dropping this unmounts the filesystem and blocks until the session has ended,
571+ /// which guarantees that `Filesystem::destroy` has run when drop returns. Because
572+ /// of that, it must not be dropped from within a filesystem callback, which runs
573+ /// on the session thread being waited for. For a session created via
574+ /// `Session::from_fd` there is no mount to remove, so dropping cannot end the
575+ /// session and leaves its thread detached; end the session externally and use
576+ /// `join` to wait for it instead. If unmounting fails, the thread is likewise
577+ /// left detached rather than waiting for a session that cannot end.
568578#[ derive( Debug ) ]
569579pub struct BackgroundSession {
570- /// Thread guard of the background session
571- pub guard : JoinHandle < io:: Result < ( ) > > ,
580+ /// Thread guard of the background session. None once joined.
581+ guard : Option < JoinHandle < io:: Result < ( ) > > > ,
572582 /// Object for creating Notifiers for client use
573583 sender : ChannelSender ,
574584 /// Ensures the filesystem is unmounted when the session ends
575585 mount : Option < Mount > ,
576586}
577587
578588impl BackgroundSession {
579- /// Unmount the filesystem and join the background thread.
589+ /// Unmount the filesystem and join the background thread, returning the
590+ /// session result. `Filesystem::destroy` has run when this returns.
580591 pub fn umount_and_join ( mut self ) -> io:: Result < ( ) > {
581592 if let Some ( mount) = self . mount . take ( ) {
582- mount. umount ( ) ?;
593+ if let Err ( err) = mount. umount ( ) {
594+ // The filesystem is still mounted and the session still running,
595+ // so joining would block indefinitely; leave the thread detached,
596+ // as before
597+ self . guard . take ( ) ;
598+ return Err ( err) ;
599+ }
583600 }
584- self . join ( )
601+ self . join_impl ( )
585602 }
586603
587604 /// Returns an object that can be used to send notifications to the kernel
588605 pub fn notifier ( & self ) -> Notifier {
589606 Notifier :: new ( self . sender . clone ( ) )
590607 }
591608
592- /// Join the filesystem thread.
593- pub fn join ( self ) -> io:: Result < ( ) > {
594- self . guard
609+ /// Join the filesystem thread without unmounting first: blocks until
610+ /// something else ends the session, e.g. an external unmount.
611+ pub fn join ( mut self ) -> io:: Result < ( ) > {
612+ self . join_impl ( )
613+ }
614+
615+ fn join_impl ( & mut self ) -> io:: Result < ( ) > {
616+ let Some ( guard) = self . guard . take ( ) else {
617+ return Ok ( ( ) ) ;
618+ } ;
619+ guard
595620 . join ( )
596621 . map_err ( |_panic : Box < dyn std:: any:: Any + Send > | {
597622 io:: Error :: new (
@@ -602,6 +627,29 @@ impl BackgroundSession {
602627 }
603628}
604629
630+ impl Drop for BackgroundSession {
631+ fn drop ( & mut self ) {
632+ // Unmount and wait for the session to end, so that Filesystem::destroy
633+ // has run by the time drop returns (issues #239 and #411). Without the
634+ // join, the process could exit before the detached session thread got
635+ // around to calling destroy
636+ let Some ( mount) = self . mount . take ( ) else {
637+ // No mount to remove (Session::from_fd): nothing here can end the
638+ // session, so joining could block forever; leave the thread detached
639+ return ;
640+ } ;
641+ if let Err ( err) = mount. umount ( ) {
642+ // Still mounted, so the session is still running and joining would
643+ // block indefinitely
644+ warn ! ( "Failed to unmount filesystem during drop: {err}" ) ;
645+ return ;
646+ }
647+ if let Err ( err) = self . join_impl ( ) {
648+ warn ! ( "Session ended with an error during drop: {err}" ) ;
649+ }
650+ }
651+ }
652+
605653// The abort test uses fusectl, which only exists on Linux; on other targets the
606654// whole module would be dead code
607655#[ cfg( all( test, target_os = "linux" ) ) ]
@@ -675,6 +723,42 @@ mod test {
675723 ManuallyDrop :: into_inner ( tmp) ;
676724 }
677725
726+ /// Dropping a BackgroundSession must unmount the filesystem and wait for the
727+ /// session to end, so that Filesystem::destroy has run when drop returns
728+ /// (issues #239 and #411). Previously the session thread was detached, and
729+ /// destroy raced (or lost to) process exit.
730+ #[ test]
731+ fn drop_waits_for_destroy ( ) {
732+ struct DestroyFs {
733+ destroyed : Arc < std:: sync:: atomic:: AtomicBool > ,
734+ }
735+ impl Filesystem for DestroyFs {
736+ fn destroy ( & mut self ) {
737+ // Give drop a chance to return early: without the join, drop
738+ // consistently wins this race and the assertion below fails
739+ std:: thread:: sleep ( std:: time:: Duration :: from_millis ( 200 ) ) ;
740+ self . destroyed
741+ . store ( true , std:: sync:: atomic:: Ordering :: SeqCst ) ;
742+ }
743+ }
744+
745+ let tmp = ManuallyDrop :: new ( tempfile:: tempdir ( ) . unwrap ( ) ) ;
746+ let destroyed = Arc :: new ( std:: sync:: atomic:: AtomicBool :: new ( false ) ) ;
747+ let fs = DestroyFs {
748+ destroyed : destroyed. clone ( ) ,
749+ } ;
750+ let bg = Session :: new ( fs, tmp. path ( ) , & Config :: default ( ) )
751+ . unwrap ( )
752+ . spawn ( )
753+ . unwrap ( ) ;
754+ drop ( bg) ;
755+ assert ! (
756+ destroyed. load( std:: sync:: atomic:: Ordering :: SeqCst ) ,
757+ "destroy() must have been called by the time drop returns"
758+ ) ;
759+ ManuallyDrop :: into_inner ( tmp) ;
760+ }
761+
678762 /// The fusectl abort file for the FUSE mount at `mountpoint`: the connection
679763 /// directory is named after the mount's anonymous device number.
680764 fn fusectl_abort_path ( mountpoint : & Path ) -> Option < std:: path:: PathBuf > {
0 commit comments