Skip to content

macos: defer FD_CLOEXEC fcntl until after fuse_daemonize()#380

Open
Yike-Ye wants to merge 1 commit into
libfuse:masterfrom
Yike-Ye:fix/macfuse-5.3.3-fcntl
Open

macos: defer FD_CLOEXEC fcntl until after fuse_daemonize()#380
Yike-Ye wants to merge 1 commit into
libfuse:masterfrom
Yike-Ye:fix/macfuse-5.3.3-fcntl

Conversation

@Yike-Ye

@Yike-Ye Yike-Ye commented Jul 8, 2026

Copy link
Copy Markdown

Problem

On macOS with macFUSE 5.3.3, sshfs can no longer daemonize into the background. Mounting without -f prints

fuse: daemonize requested after mount started; continuing in foreground

and the process stays attached to the terminal, even though sshfs's own code hasn't changed. @dmik independently hit this in #371 and suggested that "sshfs daemonization should be changed so that it happens before triggering MFMount use"; this PR does exactly that.

Root cause

macFUSE 5.3.3 mounts lazily: fuse_session_mount() (called by fuse_mount()) only records the request, and the real mount — including its XPC channel — is created on first use, after the normal fuse_main() daemonization point. Once that mount has started, fuse_daemonize() deliberately refuses to fork, because the XPC connection cannot survive a fork.

The catch is that fuse_session_fd() is one of those "first uses". Since macFUSE commit 867b5cdb ("Defer mount process until first use on macOS") it went from a plain getter to routing through fuse_session_mfch(), which triggers the delayed mount:

fuse_session_fd(se)
  └─ fuse_session_mfch(se)            // state == FUSE_DARWIN_MOUNT_DELAYED
       ├─ fuse_darwin_set_mount_started()   // sticky "mount started" flag
       └─ fuse_session_mount_real(se)       // the real mount + XPC channel

sshfs calls fuse_session_fd() between fuse_mount() and fuse_daemonize() purely to set FD_CLOEXEC on the fuse device:

res = fcntl(fuse_session_fd(se), F_SETFD, FD_CLOEXEC);

On 5.3.3 that call now materializes the mount before daemonizing, so by the time fuse_daemonize() checks fuse_darwin_mount_started() it is already set, and sshfs stays in the foreground.

Fix

Defer the FD_CLOEXEC fcntl on macOS until after fuse_daemonize(). The upstream mount → connect → daemonize order is otherwise unchanged.

This is safe:

  • The fuse fd does not exist yet while the first ssh child is spawned in ssh_connect() (the delayed mount hasn't been triggered), so there is nothing to leak in that window.
  • After daemonizing, the fcntl triggers the mount in the daemon child — where the XPC connection survives — and still sets FD_CLOEXEC before any ssh child is re-spawned on -o reconnect.

Non-macOS platforms keep the fcntl in its original place (before ssh_connect()), where it must stay because the fuse fd exists from fuse_mount() onward. This makes sshfs on macOS behave like fuse_main(), which never touches the session fd before daemonizing.

Testing

macOS 27 + macFUSE 5.3.3, key auth, no -f: the process now daemonizes to the background (PPID == 1), the mount completes in the daemon child, the SSH connection survives, and directory enumeration works. The foreground fallback no longer triggers.

Refs: #338, #371

On macOS with macFUSE 5.3.3+, fuse_mount() only records the mount
request; the real mount and its XPC channel are created lazily on first
use. fuse_session_fd() is one such first use: since macFUSE commit
867b5cdb ("Defer mount process until first use on macOS") it routes
through fuse_session_mfch(), which triggers the delayed mount and marks
the mount as started. sshfs calls fuse_session_fd() between fuse_mount()
and fuse_daemonize() only to set FD_CLOEXEC on the fuse device, so on
5.3.3 that call now materializes the mount before daemonizing, and
fuse_daemonize() then refuses to fork ("daemonize requested after mount
started; continuing in foreground"). sshfs can no longer background
itself on macOS even though its own code is unchanged.

Defer the FD_CLOEXEC fcntl on macOS until after fuse_daemonize(). The
fuse fd does not exist yet while the first ssh child is spawned in
ssh_connect(), so nothing leaks in that window; the fcntl then runs in
the daemon child, triggering the mount there (where its XPC connection
survives) and still setting FD_CLOEXEC before any ssh child is
re-spawned on reconnect. The upstream mount -> connect -> daemonize
order is otherwise unchanged, and non-macOS platforms keep the fcntl in
its original place (before ssh_connect), where it must stay because the
fuse fd exists from fuse_mount() onward.

Refs: libfuse#338

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant