Skip to content

Wait for the session to end when dropping BackgroundSession#709

Open
cberner wants to merge 1 commit into
masterfrom
claude/fix-411-background-session-drop
Open

Wait for the session to end when dropping BackgroundSession#709
cberner wants to merge 1 commit into
masterfrom
claude/fix-411-background-session-drop

Conversation

@cberner

@cberner cberner commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Fixes #411
Fixes #239

Problem

Since 0.16, dropping a BackgroundSession unmounts the filesystem but leaves the fuser-bg thread detached: drop returns before the session has ended, so Filesystem::destroy runs much later - or never, when the process exits first. #411 reports exactly this regression from the pre-0.16 blocking drop (worker threads that rely on destroy for cleanup hang), and it is why the destroy-exactly-once guarantee did not effectively extend to BackgroundSession (#239): the session structure that runs destroy on its way out was detached from the caller's lifetime.

Fix

BackgroundSession now has a Drop impl that unmounts and then joins the session thread. run() already calls destroy() after all event loop threads have joined (and FilesystemHolder backstops early-exit paths), so the join is sufficient to guarantee destroy has run when drop returns - restoring the pre-0.16 behavior, on the thread that was already responsible for it.

Two deliberate exceptions avoid blocking forever, both documented on the struct:

  • a session created via Session::from_fd has no mount to remove, so nothing in drop can end the session; its thread stays detached (this is the _mount: None case identified in the BackgroundSession::drop() no longer waits for filesystem destroy, causing cleanup issues #411 discussion), and callers should end the session themselves and use join();
  • if the unmount itself fails, the session is still running, so the thread is detached exactly as before rather than joined (umount_and_join() keeps the same early-return contract).

API notes: join() and umount_and_join() keep their signatures; the guard field is now private (required for a Drop impl to coexist with the consuming methods), which is a breaking change for code that accessed it directly. Dropping from within a filesystem callback would self-deadlock and is called out in the docs - the same was already true of calling umount_and_join there.

Testing

New regression test drop_waits_for_destroy: spawns a background session whose destroy sleeps 200ms before setting a flag, drops the BackgroundSession, and asserts the flag is set when drop returns. The sleep makes the race deterministic in both directions: without the fix drop consistently returns first and the test fails; with it, the join waits through destroy. Verified fail-before/pass-after on all three mount backends.

cargo test --lib, cargo fmt --check, and cargo clippy --all-targets with RUSTFLAGS=--deny warnings pass for default features, libfuse2, and libfuse3; cargo check --target x86_64-apple-darwin --features=macos-no-mount --all-targets passes for the macOS configuration.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TfPeUUeQKYjo26Y9mQG9sA


Generated by Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 97200974c0

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/session.rs
warn!("Failed to unmount filesystem during drop: {err}");
return;
}
if let Err(err) = self.join_impl() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid joining after an unverified helper unmount

When an unprivileged pure-Rust mount must fall back to fusermount, MountImpl::umount_impl reports success even if the helper cannot run or exits nonzero: fuse_unmount_pure ignores both the command error and output.status. In that failure case the FUSE connection remains live, so this unconditional join_impl() waits forever during an ordinary BackgroundSession drop. Propagate the helper failure or verify that the connection ended before joining.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch - fixed in the latest push by propagating the failure at its source. fuse_unmount_pure now returns io::Result: the fusermount invocation checks both the spawn result and the exit status (fusermount_unmount, with a unit test covering nonzero-exit, unspawnable-binary, and success), and umount_impl propagates it. A failed helper therefore surfaces as Err from Mount::umount(), and the existing error path in Drop/umount_and_join detaches instead of joining - no indefinite wait.

Two boundary notes:

  • A successful fusermount -z on a busy filesystem is not a failure: the session ends when the last user releases it, and drop intentionally blocks until then (the pre-0.16 semantics these issues ask to restore).
  • The libfuse2/libfuse3 EPERM fallbacks go through libfuse's fuse_unmount_compat22/fuse_session_unmount, which are void and cannot report helper failures. That exposure is unchanged and predates this PR - umount_and_join() has joined after those unverified paths since 0.17. Making those backends verifiable would require fuser to bypass libfuse's own unmount, which is out of scope here.

Generated by Claude Code

@cberner
cberner force-pushed the claude/fix-411-background-session-drop branch from 9720097 to 154d2df Compare July 25, 2026 20:03

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 154d2df6be

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/session.rs
warn!("Failed to unmount filesystem during drop: {err}");
return;
}
if let Err(err) = self.join_impl() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Skip joining after unverified libfuse2 unmounts

When built with libfuse2 and run unprivileged, this join can block forever if the compatibility unmount fails. The pure-Rust fix addresses the prior case, but fresh evidence remains in src/mnt/fuse2.rs: lines 47-50 explicitly state that fuse_unmount_compat22 can leave the mount in place without reporting status, while lines 64-66 nevertheless return Ok(()); that success reaches this unconditional join while the session is still serving. Verify that the connection ended, or leave the thread detached for this backend, before joining.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in the latest push, and by upgrading rather than detaching: the libfuse2 EPERM path no longer uses fuse_unmount_compat22 at all. It now invokes fusermount directly through the same status-checked helper the pure-Rust backend uses (fusermount_unmount, shared in mnt), so a helper failure surfaces as Err and teardown detaches instead of joining. This is strictly better than skipping the join for that backend - the destroy-on-drop guarantee is kept whenever the unmount actually succeeded - and it drops compat22's other known hazard, the realpath that stats the mountpoint through the filesystem (the comment in this file already avoided fuse_unmount_compat22 for the primary path for exactly that reason). The now-unused extern declaration is removed.

libfuse3 remains the one backend whose EPERM fallback cannot report helper failures: its unmount is coupled to the fuse_session (which owns the fd and gets destroyed in the same call sequence), so bypassing fuse_session_unmount is not a safe local change. Its fuse_kern_unmount does at least skip dead connections itself, and the exposure is unchanged from what umount_and_join() has had since 0.17.


Generated by Claude Code

@cberner
cberner force-pushed the claude/fix-411-background-session-drop branch 2 times, most recently from bddca28 to a2ec561 Compare July 25, 2026 20:11

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a2ec561ed0

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/session.rs
warn!("Failed to unmount filesystem during drop: {err}");
return;
}
if let Err(err) = self.join_impl() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Verify libfuse3 unmount before joining

When the libfuse3 backend runs unprivileged and its fusermount helper cannot unmount, this join can still block forever. Fresh evidence beyond the fixed libfuse2 path remains in src/mnt/fuse3.rs: MountImpl::umount_impl calls the void fuse_session_unmount, destroys the libfuse session, and returns Ok(()) without knowing whether the mount was removed; the duplicated FUSE descriptor used by the background loop remains live if removal failed. Propagate or independently verify the libfuse3 unmount result before joining.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in the latest push, together with the auto-unmount case, by a single mechanism instead of more per-backend surgery: after a successful unmount request, Drop now waits (bounded, 5s) for the kernel connection to actually end - polling the FUSE device fd the session already holds - and joins only then; if the connection is still alive, it warns and detaches. A silently-failed fusermount3 leaves the connection alive, so this covers the libfuse3 fallback (and any other unmount path that cannot report failure) without bypassing fuse_session_unmount, whose coupling to the session made per-backend verification unsafe. The wait also tolerates asynchronous teardown (auto_unmount, kernel-side delays), and a slow Filesystem::destroy is unaffected: the bound applies only to the connection ending, while the join itself still waits for destroy to complete. umount_and_join() keeps its explicit, documented blocking semantics.


Generated by Claude Code

Comment thread src/session.rs
warn!("Failed to unmount filesystem during drop: {err}");
return;
}
if let Err(err) = self.join_impl() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Verify auto-unmount completion before joining

With the pure-Rust AutoUnmount path, this join can still wait forever if the long-lived fusermount helper dies after mounting or fails during its eventual cleanup. Fresh evidence beyond the regular-helper fix is src/mnt/fuse_pure.rs:78-82, where teardown merely closes auto_unmount_socket and returns Ok(()) without retaining the child handle, checking its status, or verifying that the connection ended. Do not join until that asynchronous unmount is confirmed, or detach the thread when it cannot be confirmed.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in the latest push by the same bounded verification as the libfuse3 thread: after a successful unmount request, Drop waits up to 5s for the kernel connection to end (polling the session's own FUSE device fd) and joins only then; otherwise it warns and detaches. For the AutoUnmount path this handles both directions: the normal case, where fusermount unmounts asynchronously moments after the socket closes (the wait absorbs the latency and the join then proceeds), and the pathological one, where the fusermount process died without unmounting and the connection never ends (the wait expires and the thread is detached instead of blocking forever).


Generated by Claude Code

Comment thread CHANGELOG.md Outdated
Comment on lines +11 to +12
teardown wait forever. The `libfuse2` backend no longer uses `fuse_unmount_compat22`,
which also used to stat the mountpoint through the filesystem

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove implementation details from the release note

The sentence naming fuse_unmount_compat22 and describing its internal mountpoint stat is an implementation detail rather than user-facing impact. Keep the preceding failure-reporting behavior in the changelog and move or remove this backend-specific mechanism; the repository's AGENTS.md explicitly requires release notes to be brief and focused on user impact.

AGENTS.md reference: AGENTS.md:L15-L18

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in the latest push: the release note keeps only the user-facing behavior (fusermount unmount failures are now reported) and the fuse_unmount_compat22 mechanism sentence is removed. The drop bullet was also reworded to describe when drop waits and when it detaches, without backend internals.


Generated by Claude Code

@cberner
cberner force-pushed the claude/fix-411-background-session-drop branch 2 times, most recently from 345a5e1 to ee67787 Compare July 25, 2026 20:18

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ee67787626

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/session.rs Outdated
Comment on lines +229 to +231
/// Run the session loop in a background thread. If the returned handle is dropped,
/// the filesystem is unmounted and the given session ends.
/// the filesystem is unmounted, and this blocks until the session has ended and
/// `Filesystem::destroy` has run.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Qualify the blocking-drop guarantee

Document the exceptions here rather than promising that every returned handle blocks until destroy runs. For Session::from_fd, BackgroundSession::drop returns immediately because mount is None, and after an unmount failure it also returns with the session thread detached; callers relying on this method-level contract can therefore free resources before Filesystem::destroy has run.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in the latest push: spawn()'s doc no longer promises unconditional blocking - it now says the session is waited for "except in the cases documented on [BackgroundSession], where the session thread is detached instead", linking to the struct doc that enumerates all three exceptions (from_fd sessions, unmount failure, and a connection still alive after the bounded wait). Verified with cargo doc under --deny warnings for the intra-doc link.


Generated by Claude Code

@cberner
cberner force-pushed the claude/fix-411-background-session-drop branch from ee67787 to 5ce0330 Compare July 25, 2026 20:22

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5ce0330825

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/session.rs Outdated
Comment on lines +750 to +751
/// (issues #239 and #411). Previously the session thread was detached, and
/// destroy raced (or lost to) process exit.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove patch-history details from the test comment

The references to issue numbers and the behavior before this patch describe change history rather than the lasting invariant being tested. Keep the preceding statement that drop waits for Filesystem::destroy, but remove these commit-specific details to comply with the repository's comment style guide.

AGENTS.md reference: AGENTS.md:L6-L9

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in the latest push: the test doc keeps only the invariant (drop unmounts and waits so that Filesystem::destroy has run when it returns); the issue references and the pre-change behavior sentence are removed.


Generated by Claude Code

Since 0.16, dropping a BackgroundSession unmounted the filesystem but
left the session thread detached, so drop returned before the session
had ended and Filesystem::destroy could run much later - or never, when
the process exited first (#411). That also meant the destroy-exactly-
once guarantee did not effectively extend to BackgroundSession (#239).

Add a Drop impl that unmounts and then joins the session thread,
restoring the pre-0.16 blocking behavior: destroy has run by the time
drop returns. Two deliberate exceptions avoid blocking forever: a
session created via Session::from_fd has no mount to remove, so nothing
in drop can end it and its thread stays detached; and if the unmount
itself fails, the session is still running, so the thread is detached
as before instead of joined.

The guard field of BackgroundSession is now private; use join() or
umount_and_join(), which keep their existing signatures.

Fixes #411
Fixes #239

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TfPeUUeQKYjo26Y9mQG9sA
@cberner
cberner force-pushed the claude/fix-411-background-session-drop branch from 5ce0330 to e91c9e6 Compare July 25, 2026 20:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants