You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@opendal.apache.org by xu...@apache.org on 2026/10/26 08:57:09 UTC

(opendal-yinyang) 02/02: fix: bound FUSE teardown and abort the connection on expiry

This is an automated email from the ASF dual-hosted git repository.

Xuanwo pushed a commit to branch xuanwo/linux-fuse-foundation
in repository https://gitbox.apache.org/repos/asf/opendal-yinyang.git

commit 95a8c69d21917a79c56e0871584ccd3652763569
Author: Xuanwo <gi...@xuanwo.io>
AuthorDate: Mon Oct 26 16:56:40 2026 +0800

    fix: bound FUSE teardown and abort the connection on expiry
    
    A lazy unmount keeps the kernel connection alive while clients hold open
    references or in-flight requests, so a signal-initiated unmount could
    block forever in BackgroundSession::umount_and_join and pin both the
    daemon and the blocked clients. The signal path now waits a five-second
    grace period after the lazy unmount and exits the process when the
    session has not ended; closing /dev/fuse makes the kernel abort every
    pending request (fs/fuse fuse_dev_release -> fuse_abort_conn,
    -ECONNABORTED) instead of leaving clients wedged.
    
    The cancellation section now states the kernel-verified wait boundary
    instead of claiming late replies are discarded: requests already
    delivered to the daemon are waited out by the kernel until a reply or a
    connection abort, so the daemon must reply exactly once and its exit is
    the abort guarantee.
    
    Co-authored-by: Chojan Shang <ps...@apache.org>
---
 Cargo.toml          |  2 +-
 specs/linux-fuse.md | 39 +++++++++++++++++++++++++--------------
 src/fuse/mod.rs     | 21 ++++++++++++++++++---
 3 files changed, 44 insertions(+), 18 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml
index 3c864f2..067270f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -43,7 +43,7 @@ rusqlite = { version = "0.40.2", default-features = false, features = ["bundled"
 serde = { version = "1", features = ["derive"] }
 serde_json = "1"
 tempfile = "3"
-tokio = { workspace = true, features = ["fs", "macros", "rt-multi-thread", "signal", "sync"] }
+tokio = { workspace = true, features = ["fs", "macros", "rt-multi-thread", "signal", "sync", "time"] }
 uuid.workspace = true
 yinyang-core = { path = "crates/core" }
 
diff --git a/specs/linux-fuse.md b/specs/linux-fuse.md
index adf2a20..b251d4e 100644
--- a/specs/linux-fuse.md
+++ b/specs/linux-fuse.md
@@ -115,23 +115,34 @@ map to the narrow errno intended for each operation; unknown identity is
 
 ## Cancellation and exit
 
-The binding answers `FUSE_INTERRUPT` with `ENOSYS`; the kernel ignores that
-reply, so an interrupted syscall does not cancel in-flight remote work. Each
-operation still completes and replies exactly once. Interrupted callers may
-observe `EINTR` while the underlying read or scan runs to completion.
+The binding answers `FUSE_INTERRUPT` with `ENOSYS` (fuser 0.17
+`src/request.rs`), so an interrupted syscall does not cancel in-flight remote
+work. The Linux kernel waits out every request already delivered to the daemon
+(`fs/fuse/dev.c` `request_wait_answer`): a client terminated by a fatal signal
+while its request is in userspace stays in an uninterruptible wait until the
+reply arrives or the connection aborts, and only a request that was never
+delivered can be dropped with `-EINTR`. The daemon therefore replies to every
+request exactly once, and daemon termination is the abort guarantee: closing
+`/dev/fuse` makes the kernel end pending requests with `-ECONNABORTED`
+(`fuse_dev_release` -> `fuse_abort_conn`).
 
 The session runs in the foreground. Teardown waits on both the session end and
 SIGINT/SIGTERM: an external `fusermount3 -u` ends the session loop and the
-process exits on its own, while a signal unmounts and joins the session before
-exiting. `Session::spawn` moves the mount object into `BackgroundSession`, so a
-`SessionUnmounter` obtained before spawning would find an empty slot; instead
-one blocking owner holds the background session, polls its public thread guard
-for a natural end, and receives the local teardown request. Exactly one `join`
-or `umount_and_join` runs. Returning from `run` ends `main`, so the Tokio
-runtime shuts down; request tasks are detached and owned by that runtime, their
-unanswered replies are dropped on the closing channel, and the staging lease is
-released at process exit. In-flight tasks hold their `Arc<Mount>` for that
-window; there is no mid-session cancellation signal.
+process exits on its own, while a signal detaches the mount and waits up to a
+five-second grace period. `Session::spawn` moves the mount object into
+`BackgroundSession`, so a `SessionUnmounter` obtained before spawning would
+find an empty slot; one blocking owner holds the background session, polls its
+public thread guard for a natural end, and receives the local teardown
+request. Exactly one `join` or `umount_and_join` runs. A lazy unmount does not
+end the kernel connection while clients still hold open references or in-flight
+requests, so when the grace period expires the process exits: the closed
+`/dev/fuse` aborts the connection and every blocked client syscall returns an
+error instead of hanging. Returning from `run` ends `main`, so the Tokio
+runtime shuts down; request tasks are detached and owned by that runtime, and
+their unanswered replies are dropped on the closing channel. The staging lease
+is released at process exit (or immediately on OS file-descriptor close).
+In-flight tasks hold their `Arc<Mount>` for that window; there is no
+mid-session cancellation signal.
 
 ## Boundaries and known limits
 
diff --git a/src/fuse/mod.rs b/src/fuse/mod.rs
index b0362d3..c89d80e 100644
--- a/src/fuse/mod.rs
+++ b/src/fuse/mod.rs
@@ -30,8 +30,10 @@
 //! disposable and are reclaimed by the existing Mount recovery.
 //!
 //! The session mounts as `fuse.yinyang` and runs in the foreground. SIGINT and
-//! SIGTERM unmount the session and finish the process; dropping the session
-//! otherwise unmounts as well.
+//! SIGTERM detach the mount and wait briefly for the session. Lazy unmounting
+//! does not end the kernel connection while clients still hold open
+//! references, so a bounded wait is followed by process exit, which closes
+//! `/dev/fuse` and forces the kernel to abort every pending request.
 //!
 //! See `specs/linux-fuse.md` for the full read-only contract.
 
@@ -116,7 +118,20 @@ pub async fn run(
         }
         _ = shutdown_signal() => {
             let _ = teardown.send(());
-            join.await.map_err(blocking_error)??;
+            match tokio::time::timeout(TEARDOWN_GRACE, &mut join).await {
+                Ok(result) => result.map_err(blocking_error)??,
+                Err(_) => {
+                    // A lazy unmount keeps the session alive while clients hold
+                    // the mount; exiting closes /dev/fuse and the kernel aborts
+                    // every pending request (fs/fuse: fuse_dev_release ->
+                    // fuse_abort_conn), so clients cannot stay wedged forever.
+                    eprintln!(
+                        "yy: FUSE session did not end within {}s (clients may hold open references); exiting to abort the connection",
+                        TEARDOWN_GRACE.as_secs()
+                    );
+                    std::process::exit(1);
+                }
+            }
         }
     }
     println!("yy: unmounted {}", mountpoint.display());