* [PATCH 00/19] libfuse: Add support for synchronous init
@ 2026-03-23 17:44 Bernd Schubert
2026-03-23 17:44 ` [PATCH 01/19] ci-build: Add environment logging Bernd Schubert
` (19 more replies)
0 siblings, 20 replies; 59+ messages in thread
From: Bernd Schubert @ 2026-03-23 17:44 UTC (permalink / raw)
To: linux-fsdevel
Cc: Miklos Szeredi, Joanne Koong, Darrick J. Wong, Bernd Schubert,
Bernd Schubert
I'm taking Darricks example here and posting API changing libfuse
changes to linux-fsdevel. We should consider to create a fuse
specific list if that is too much. The existing
fuse-devel@lists.sourceforge.net is rather hopeless due to lack
of spam filtering.
The first few patches in this series are just preparation, after that
follow the important parts:
1) New daemonize API, see that commit for details. In short, the existing
fuse_daemonize() was not sufficient for complex daemons and is impossible
to use with sync-init and the current way to start fuse-io-uring ring
threads.
2) Support for privileged daemons, still rather straight forward, but
requires the startup of a worker thread that handles requests until
mount is complete.
3) Privileged daemons - requirement is to update the API with fusermount,
because fuse_session_mount_new_api() needs to obtain the /dev/fuse file
descriptor, then start the worker thread with that fd and then continue
the actual mount through fusermount.
To: linux-fsdevel@vger.kernel.org
Cc: Miklos Szeredi <miklos@szeredi.hu>
Cc: Joanne Koong <joannelkoong@gmail.com>
Cc: Darrick J. Wong <djwong@kernel.org>
Joanne Koong <joannelkoong@gmail.com>
Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
---
Bernd Schubert (19):
ci-build: Add environment logging
Add 'STRCPY' to the checkpatch ignore option
checkpatch.pl: Add _Atomic to $Attribute patttern
Add a new daemonize API
Sync fuse_kernel.h with linux-6.18
mount.c: Split fuse_mount_sys to prepare privileged sync FUSE_INIT
Add FUSE_MOUNT_FALLBACK_NEEDED define for -2 mount errors
Refactor mount code / move common functions to mount_util.c
Move mount flags to mount_i.h
conftest.py: Add more valgrind filter patterns
Add support for the new linux mount API
fuse mount: Support synchronous FUSE_INIT (privileged daemon)
Add fuse_session_set_debug() to enable debug output without foreground
Move more generic mount code to mount_util.{c,h}
Split the fusermount do_mount function
fusermount: Refactor extract_x_options
Make fusermount work bidirectional for sync init
New mount API: Filter out "user="
Add support for sync-init of unprivileged daemons
.github/workflows/checkpatch.yml | 2 +-
checkpatch.pl | 3 +-
doc/README.daemonize | 186 +++++++++++
doc/README.fusermount | 359 +++++++++++++++++++++
doc/README.mount | 86 +++++
doc/README.sync-init | 184 +++++++++++
example/passthrough_hp.cc | 18 +-
include/fuse_daemonize.h | 71 +++++
include/fuse_kernel.h | 1 +
include/fuse_lowlevel.h | 24 ++
include/meson.build | 3 +-
lib/fuse_daemonize.c | 292 +++++++++++++++++
lib/fuse_i.h | 27 +-
lib/fuse_lowlevel.c | 342 +++++++++++++++++++-
lib/fuse_versionscript | 4 +
lib/helper.c | 13 +-
lib/meson.build | 6 +-
lib/mount.c | 380 +++++++++++++++-------
lib/mount_bsd.c | 1 +
lib/mount_common_i.h | 29 ++
lib/mount_fsmount.c | 417 ++++++++++++++++++++++++
lib/mount_i_linux.h | 87 +++++
lib/mount_util.c | 34 ++
lib/mount_util.h | 11 +
meson.build | 19 +-
test/ci-build.sh | 16 +
test/conftest.py | 7 +-
test/test_want_conversion.c | 1 +
util/fusermount.c | 671 +++++++++++++++++++++++++++++++--------
util/meson.build | 2 +-
30 files changed, 3024 insertions(+), 272 deletions(-)
---
base-commit: 9eba0f3c9e8b5af7b252093bb6f81f086bb35563
change-id: 20260323-fuse-init-before-mount-8f5b09a1acf1
Best regards,
--
Bernd Schubert <bernd@bsbernd.com>
^ permalink raw reply [flat|nested] 59+ messages in thread
* [PATCH 01/19] ci-build: Add environment logging
2026-03-23 17:44 [PATCH 00/19] libfuse: Add support for synchronous init Bernd Schubert
@ 2026-03-23 17:44 ` Bernd Schubert
2026-03-23 17:44 ` [PATCH 02/19] Add 'STRCPY' to the checkpatch ignore option Bernd Schubert
` (18 subsequent siblings)
19 siblings, 0 replies; 59+ messages in thread
From: Bernd Schubert @ 2026-03-23 17:44 UTC (permalink / raw)
To: linux-fsdevel
Cc: Miklos Szeredi, Joanne Koong, Darrick J. Wong, Bernd Schubert,
Bernd Schubert
From: Bernd Schubert <bschubert@ddn.com>
It is very hard to see what exactly fails, as everything
is run from the same script.
Signed-off-by: Bernd Schubert <bschubert@ddn.com>
---
test/ci-build.sh | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
diff --git a/test/ci-build.sh b/test/ci-build.sh
index 8b019a0b5e52c1af0eee510ede21785d584e8414..799f13b2911fac47b0bbb2d445ec22a6f40f065d 100755
--- a/test/ci-build.sh
+++ b/test/ci-build.sh
@@ -30,6 +30,20 @@ export LSAN_OPTIONS="suppressions=$(pwd)/lsan_suppress.txt"
export ASAN_OPTIONS="detect_leaks=1"
export CC
+log_env()
+{
+ echo "=== Environment ==="
+ echo "CC: ${CC}"
+ echo "CXX: ${CXX}"
+ echo "LSAN_OPTIONS: ${LSAN_OPTIONS}"
+ echo "ASAN_OPTIONS: ${ASAN_OPTIONS}"
+ echo "UBSAN_OPTIONS: ${UBSAN_OPTIONS}"
+ echo "FUSE_URING_ENABLE: ${FUSE_URING_ENABLE}"
+ echo "FUSE_URING_QUEUE_DEPTH: ${FUSE_URING_QUEUE_DEPTH}"
+ echo "Valgrind: ${TEST_WITH_VALGRIND}"
+ echo "==================="
+}
+
non_sanitized_build()
(
echo "Standard build (without sanitizers)"
@@ -54,6 +68,7 @@ non_sanitized_build()
build_opts=''
fi
+ log_env
meson setup -Dprefix=${PREFIX_DIR} -D werror=true ${build_opts} "${SOURCE_DIR}" || (cat meson-logs/meson-log.txt; false)
ninja
sudo env PATH=$PATH ninja install
@@ -79,6 +94,7 @@ sanitized_build()
mkdir build-san; pushd build-san
+ log_env
meson setup -Dprefix=${PREFIX_DIR} -D werror=true\
"${SOURCE_DIR}" \
|| (cat meson-logs/meson-log.txt; false)
--
2.43.0
^ permalink raw reply related [flat|nested] 59+ messages in thread
* [PATCH 02/19] Add 'STRCPY' to the checkpatch ignore option
2026-03-23 17:44 [PATCH 00/19] libfuse: Add support for synchronous init Bernd Schubert
2026-03-23 17:44 ` [PATCH 01/19] ci-build: Add environment logging Bernd Schubert
@ 2026-03-23 17:44 ` Bernd Schubert
2026-03-23 21:03 ` Darrick J. Wong
2026-03-23 17:44 ` [PATCH 03/19] checkpatch.pl: Add _Atomic to $Attribute patttern Bernd Schubert
` (17 subsequent siblings)
19 siblings, 1 reply; 59+ messages in thread
From: Bernd Schubert @ 2026-03-23 17:44 UTC (permalink / raw)
To: linux-fsdevel
Cc: Miklos Szeredi, Joanne Koong, Darrick J. Wong, Bernd Schubert,
Bernd Schubert
From: Bernd Schubert <bschubert@ddn.com>
checkpatch.pl is a kernel copy and the strcpy warning is not valid
for libfuse.
Signed-off-by: Bernd Schubert <bschubert@ddn.com>
---
.github/workflows/checkpatch.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/checkpatch.yml b/.github/workflows/checkpatch.yml
index 4a98c91cddeab686c782a1d34cd5d15f86f0b42d..8dead95b065bc81449a98fa7ea94fac7463d2334 100644
--- a/.github/workflows/checkpatch.yml
+++ b/.github/workflows/checkpatch.yml
@@ -35,7 +35,7 @@ jobs:
fi
subject=$(git log -1 --format=%s $commit)
echo "Checking commit: $commit - $subject"
- if ! ./checkpatch.pl --max-line-length=100 --no-tree --ignore MAINTAINERS,SPDX_LICENSE_TAG,COMMIT_MESSAGE,FILE_PATH_CHANGES,EMAIL_SUBJECT,AVOID_EXTERNS,GIT_COMMIT_ID,ENOSYS_SYSCALL,ENOSYS,FROM_SIGN_OFF_MISMATCH,QUOTED_COMMIT_ID,PREFER_ATTRIBUTE_ALWAYS_UNUSED,PREFER_DEFINED_ATTRIBUTE_MACRO -g $commit; then
+ if ! ./checkpatch.pl --max-line-length=100 --no-tree --ignore MAINTAINERS,SPDX_LICENSE_TAG,COMMIT_MESSAGE,FILE_PATH_CHANGES,EMAIL_SUBJECT,AVOID_EXTERNS,GIT_COMMIT_ID,ENOSYS_SYSCALL,ENOSYS,FROM_SIGN_OFF_MISMATCH,QUOTED_COMMIT_ID,PREFER_ATTRIBUTE_ALWAYS_UNUSED,PREFER_DEFINED_ATTRIBUTE_MACRO,STRCPY -g $commit; then
echo "checkpatch.pl found issues in commit $commit - $subject"
exit 1
fi
--
2.43.0
^ permalink raw reply related [flat|nested] 59+ messages in thread
* [PATCH 03/19] checkpatch.pl: Add _Atomic to $Attribute patttern
2026-03-23 17:44 [PATCH 00/19] libfuse: Add support for synchronous init Bernd Schubert
2026-03-23 17:44 ` [PATCH 01/19] ci-build: Add environment logging Bernd Schubert
2026-03-23 17:44 ` [PATCH 02/19] Add 'STRCPY' to the checkpatch ignore option Bernd Schubert
@ 2026-03-23 17:44 ` Bernd Schubert
2026-03-23 21:09 ` Darrick J. Wong
2026-03-23 17:44 ` [PATCH 04/19] Add a new daemonize API Bernd Schubert
` (16 subsequent siblings)
19 siblings, 1 reply; 59+ messages in thread
From: Bernd Schubert @ 2026-03-23 17:44 UTC (permalink / raw)
To: linux-fsdevel
Cc: Miklos Szeredi, Joanne Koong, Darrick J. Wong, Bernd Schubert,
Bernd Schubert
From: Bernd Schubert <bschubert@ddn.com>
WARNING: Missing a blank line after declarations
+ int init_error;
+ _Atomic bool terminate_mount_worker;
This change should go to linux upstream.
Signed-off-by: Bernd Schubert <bschubert@ddn.com>
---
checkpatch.pl | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/checkpatch.pl b/checkpatch.pl
index 9eed3683ad76caffbbb2418e5dbea7551d374406..2d44e0e6edac303731c7637de00ed35d7b1dfcb7 100755
--- a/checkpatch.pl
+++ b/checkpatch.pl
@@ -520,7 +520,8 @@ our $Attribute = qr{
____cacheline_aligned_in_smp|
____cacheline_internodealigned_in_smp|
__weak|
- __alloc_size\s*\(\s*\d+\s*(?:,\s*\d+\s*)?\)
+ __alloc_size\s*\(\s*\d+\s*(?:,\s*\d+\s*)?\)|
+ _Atomic
}x;
our $Modifier;
our $Inline = qr{inline|__always_inline|noinline|__inline|__inline__};
--
2.43.0
^ permalink raw reply related [flat|nested] 59+ messages in thread
* [PATCH 04/19] Add a new daemonize API
2026-03-23 17:44 [PATCH 00/19] libfuse: Add support for synchronous init Bernd Schubert
` (2 preceding siblings ...)
2026-03-23 17:44 ` [PATCH 03/19] checkpatch.pl: Add _Atomic to $Attribute patttern Bernd Schubert
@ 2026-03-23 17:44 ` Bernd Schubert
2026-03-23 22:28 ` Darrick J. Wong
2026-03-23 17:45 ` [PATCH 05/19] Sync fuse_kernel.h with linux-6.18 Bernd Schubert
` (15 subsequent siblings)
19 siblings, 1 reply; 59+ messages in thread
From: Bernd Schubert @ 2026-03-23 17:44 UTC (permalink / raw)
To: linux-fsdevel
Cc: Miklos Szeredi, Joanne Koong, Darrick J. Wong, Bernd Schubert
In complex fuse file systems one often wants
a) fork
b) start extra threads and system initialization (like network
connection and RDMA memory registration).
c) Start the fuse session
With fuse_daemonize() there is no way to return a notification
if step b) or c) failed. Therefore exising examples do
the fuse_daemonize() after fuse_session_mount().
Step, i.e. starting extra threads and possible failures are
do not exist in these examples - unhandled use case.
With the FUSE_SYNC_INIT feature, which does FUSE_INIT at mount time
and not after the mount anymore, it becomes even more complex
as FUSE_INIT triggers startup of fuse_io_uring threads. That
means for FUSE_SYNC_INIT forking/daemonization has to be done
_before_ the fuse_session_mount().
A new API is introduced to overcome the limitations of
fuse_daemonize()
fuse_daemonize_start() - fork, but foreground process does not
terminate yet and watches the background.
fuse_daemonize_signal() - background daemon signals to
the foreground process success or failure.
fuse_daemonize_active() - helper function for the high level
interface, which needs to handle both APIs. fuse_daemonize()
is called within fuse_main(), which now needs to know if the caller
actually already used the new API itself.
The object 'struct fuse_daemonize *' is allocated dynamically
and stored a global variable in fuse_daemonize.c, because
- high level fuse_main_real_versioned() needs to know
if already daemonized
- high level daemons do not have access to struct fuse_session
- FUSE_SYNC_INIT in later commits can only be done if the new
API (or a file system internal) daemonization is used.
Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
---
doc/README.daemonize | 186 +++++++++++++++++++++++++++++
example/passthrough_hp.cc | 18 ++-
include/fuse_daemonize.h | 71 +++++++++++
include/meson.build | 3 +-
lib/fuse_daemonize.c | 284 ++++++++++++++++++++++++++++++++++++++++++++
lib/fuse_i.h | 4 +-
lib/fuse_lowlevel.c | 1 +
lib/fuse_versionscript | 3 +
lib/helper.c | 13 +-
lib/meson.build | 3 +-
test/test_want_conversion.c | 1 +
11 files changed, 576 insertions(+), 11 deletions(-)
diff --git a/doc/README.daemonize b/doc/README.daemonize
new file mode 100644
index 0000000000000000000000000000000000000000..f7473361b3a39369291e9401dfc5f7928ff4160b
--- /dev/null
+++ b/doc/README.daemonize
@@ -0,0 +1,186 @@
+FUSE Daemonization API
+======================
+
+This document describes the FUSE daemonization APIs, including the legacy
+fuse_daemonize() function and the new fuse_daemonize_start()/signal() API
+introduced in libfuse 3.19.
+
+
+Overview
+--------
+
+FUSE filesystems often need to run as background daemons. Daemonization
+involves forking the process, creating a new session, and redirecting
+standard file descriptors. The challenge is properly reporting initialization
+failures to the parent process.
+
+
+Old API: fuse_daemonize()
+--------------------------
+
+Function signature:
+ int fuse_daemonize(int foreground);
+
+Location: lib/helper.c
+
+This is the legacy daemonization API, primarily used with the high-level
+fuse_main() interface.
+
+Behavior:
+- If foreground=0: forks the process, creates new session, redirects stdio
+- If foreground=1: only changes directory to "/"
+- Parent waits for a single byte on a pipe before exiting
+- Child writes completion byte immediately after redirecting stdio
+- Always changes directory to "/"
+
+Limitations:
+1. No failure reporting: The parent receives notification immediately after
+ fork/setsid, before any meaningful initialization (like mounting the
+ filesystem or starting threads).
+
+2. Timing constraint: Must be called AFTER fuse_session_mount() in existing
+ examples, because there's no way to report mount failures to the parent.
+
+3. Thread initialization: Cannot report failures from complex initialization
+ steps like:
+ - Starting worker threads
+ - Network connection setup
+ - RDMA memory registration
+ - Resource allocation
+
+4. FUSE_SYNC_INIT incompatibility: With the FUSE_SYNC_INIT feature, FUSE_INIT
+ happens at mount time and may start io_uring threads. This requires
+ daemonization BEFORE mount, which the old API cannot handle properly.
+
+Example usage (old API):
+ fuse = fuse_new(...);
+ fuse_mount(fuse, mountpoint);
+ fuse_daemonize(opts.foreground); // After mount, can't report mount failure
+ fuse_set_signal_handlers(se);
+ fuse_session_loop(se);
+
+
+New API: fuse_daemonize_start() / fuse_daemonize_signal()
+----------------------------------------------------------
+
+Functions:
+ int fuse_daemonize_start(unsigned int flags);
+ void fuse_daemonize_signal(int status);
+ bool fuse_daemonize_active(void);
+
+Location: lib/fuse_daemonize.c, include/fuse_daemonize.h
+Available since: libfuse 3.19
+
+This new API solves the limitations of fuse_daemonize() by splitting
+daemonization into two phases:
+
+1. fuse_daemonize_start() - Fork and setup, but parent waits
+2. fuse_daemonize_signal() - Signal success/failure to parent
+
+
+fuse_daemonize_start()
+----------------------
+
+Flags:
+- FUSE_DAEMONIZE_NO_CHDIR: Don't change directory to "/"
+- FUSE_DAEMONIZE_NO_BACKGROUND: Don't fork (foreground mode)
+
+Behavior:
+- Unless NO_BACKGROUND: forks the process
+- Parent waits for status signal from child
+- Child creates new session and continues
+- Unless NO_CHDIR: changes directory to "/"
+- Closes stdin immediately in child
+- Starts a watcher thread to detect parent death
+- Returns 0 in child on success, negative errno on error
+
+Parent death detection:
+- Uses a "death pipe" - parent keeps write end open
+- Child's watcher thread polls the read end
+- When parent dies, pipe gets POLLHUP and child exits
+- Prevents orphaned daemons if parent is killed
+
+
+fuse_daemonize_signal()
+-----------------------
+
+Status values:
+- FUSE_DAEMONIZE_SUCCESS (0): Initialization succeeded
+- FUSE_DAEMONIZE_FAILURE (1): Initialization failed
+
+Behavior:
+- Signals the parent process with success or failure status
+- Parent exits with EXIT_SUCCESS or EXIT_FAILURE accordingly
+- On success: redirects stdout/stderr to /dev/null
+- Stops the parent watcher thread
+- Cleans up pipes and internal state
+- Safe to call multiple times
+- Safe to call even if fuse_daemonize_start() failed
+
+
+fuse_daemonize_active()
+-----------------------
+
+Returns true if daemonization is active and waiting for signal.
+
+Used by the high-level fuse_main() to detect if the application already
+called the new API, avoiding double-daemonization.
+
+
+Example usage (new API):
+-------------------------
+
+ // Start daemonization BEFORE mount
+ unsigned int daemon_flags = 0;
+ if (foreground)
+ daemon_flags |= FUSE_DAEMONIZE_NO_BACKGROUND;
+
+ if (fuse_daemonize_start(daemon_flags) != 0)
+ goto error;
+
+ // Mount can now fail and be reported to parent
+ if (fuse_session_mount(se, mountpoint) != 0)
+ goto error_signal;
+
+ // Complex initialization can fail and be reported
+ if (setup_threads() != 0)
+ goto error_signal;
+
+ if (setup_network() != 0)
+ goto error_signal;
+
+ // Signal success - parent exits with EXIT_SUCCESS
+ // This is typically done in the init() callback after FUSE_INIT
+ fuse_daemonize_signal(FUSE_DAEMONIZE_SUCCESS);
+
+ // Run main loop
+ fuse_session_loop(se);
+
+ return 0;
+
+error_signal:
+ // Signal failure - parent exits with EXIT_FAILURE
+ fuse_daemonize_signal(FUSE_DAEMONIZE_FAILURE);
+error:
+ return 1;
+
+
+When to signal success
+----------------------
+
+The success signal should be sent after all critical initialization is
+complete. For FUSE filesystems, this is typically in the init() callback,
+after FUSE_INIT has been processed successfully.
+
+Example (from passthrough_hp.cc):
+ static void sfs_init(void *userdata, fuse_conn_info *conn) {
+ // ... initialization ...
+ fuse_daemonize_signal(FUSE_DAEMONIZE_SUCCESS);
+ }
+
+This ensures the parent only exits after:
+- Mount succeeded
+- FUSE_INIT completed
+- All threads started
+- Filesystem is ready to serve requests
+
diff --git a/example/passthrough_hp.cc b/example/passthrough_hp.cc
index 9f795c5546ee8312e1393c5b8fcebfd77724fb49..a0da5c7e6ab07f921df65db9c700e7de77fe1598 100644
--- a/example/passthrough_hp.cc
+++ b/example/passthrough_hp.cc
@@ -55,6 +55,7 @@
#include <errno.h>
#include <ftw.h>
#include <fuse_lowlevel.h>
+#include <fuse_daemonize.h>
#include <inttypes.h>
#include <string.h>
#include <sys/file.h>
@@ -243,6 +244,9 @@ static void sfs_init(void *userdata, fuse_conn_info *conn)
/* Try a large IO by default */
conn->max_write = 4 * 1024 * 1024;
+
+ /* Signal successful init to parent */
+ fuse_daemonize_signal(FUSE_DAEMONIZE_SUCCESS);
}
static void sfs_getattr(fuse_req_t req, fuse_ino_t ino, fuse_file_info *fi)
@@ -1580,6 +1584,7 @@ int main(int argc, char *argv[])
{
struct fuse_loop_config *loop_config = NULL;
void *teardown_watchog = NULL;
+ unsigned int daemon_flags = 0;
// Parse command line options
auto options{ parse_options(argc, argv) };
@@ -1638,10 +1643,14 @@ int main(int argc, char *argv[])
fuse_loop_cfg_set_clone_fd(loop_config, fs.clone_fd);
- if (fuse_session_mount(se, argv[2]) != 0)
+ /* Start daemonization before mount so parent can report mount failure */
+ if (fs.foreground)
+ daemon_flags |= FUSE_DAEMONIZE_NO_BACKGROUND;
+ if (fuse_daemonize_start(daemon_flags) != 0)
goto err_out3;
- fuse_daemonize(fs.foreground);
+ if (fuse_session_mount(se, argv[2]) != 0)
+ goto err_out4;
if (!fs.foreground)
fuse_log_enable_syslog("passthrough-hp", LOG_PID | LOG_CONS,
@@ -1650,7 +1659,7 @@ int main(int argc, char *argv[])
teardown_watchog = fuse_session_start_teardown_watchdog(
se, fs.root.stop_timeout_secs, NULL, NULL);
if (teardown_watchog == NULL)
- goto err_out3;
+ goto err_out4;
if (options.count("single"))
ret = fuse_session_loop(se);
@@ -1659,6 +1668,9 @@ int main(int argc, char *argv[])
fuse_session_unmount(se);
+err_out4:
+ if (fuse_daemonize_active())
+ fuse_daemonize_signal(FUSE_DAEMONIZE_FAILURE);
err_out3:
fuse_remove_signal_handlers(se);
err_out2:
diff --git a/include/fuse_daemonize.h b/include/fuse_daemonize.h
new file mode 100644
index 0000000000000000000000000000000000000000..f49f6aa580a93547198e7dabe65498708e65d024
--- /dev/null
+++ b/include/fuse_daemonize.h
@@ -0,0 +1,71 @@
+/*
+ * FUSE: Filesystem in Userspace
+ * Copyright (C) 2026 Bernd Schubert <bsbernd.com>
+ *
+ * This program can be distributed under the terms of the GNU LGPLv2.
+ * See the file COPYING.LIB.
+ *
+ */
+
+#ifndef FUSE_DAEMONIZE_H_
+#define FUSE_DAEMONIZE_H_
+
+#include <stdint.h>
+#include <stdbool.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * Flags for fuse_daemonize_start()
+ */
+#define FUSE_DAEMONIZE_NO_CHDIR (1 << 0)
+#define FUSE_DAEMONIZE_NO_BACKGROUND (1 << 1)
+
+/**
+ * Status values for fuse_daemonize_signal()
+ */
+#define FUSE_DAEMONIZE_SUCCESS 0
+#define FUSE_DAEMONIZE_FAILURE 1
+
+/**
+ * Start daemonization process.
+ *
+ * Unless FUSE_DAEMONIZE_NO_BACKGROUND is set, this forks the process.
+ * The parent waits for a signal from the child via fuse_daemonize_signal().
+ * The child returns from this call and continues setup.
+ *
+ * Unless FUSE_DAEMONIZE_NO_CHDIR is set, changes directory to "/".
+ *
+ * Must be called before fuse_session_mount().
+ *
+ * @param flags combination of FUSE_DAEMONIZE_* flags
+ * @return 0 on success, negative errno on error
+ */
+int fuse_daemonize_start(unsigned int flags);
+
+/**
+ * Signal daemonization status to parent and cleanup.
+ *
+ * The child calls this after setup is complete (or failed).
+ * The parent receives the status and exits with it.
+ * Safe to call multiple times or if start failed.
+ *
+ * @param status FUSE_DAEMONIZE_SUCCESS or FUSE_DAEMONIZE_FAILURE
+ */
+void fuse_daemonize_signal(int status);
+
+/**
+ * Check if daemonization is active and waiting for signal.
+ *
+ * @return true if active, false otherwise
+ */
+bool fuse_daemonize_active(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* FUSE_DAEMONIZE_H_ */
+
diff --git a/include/meson.build b/include/meson.build
index bf671977a5a6a9142bd67aceabd8a919e3d968d0..cfbaf52ac5d84369e92948c631e2fcfdd04ac2eb 100644
--- a/include/meson.build
+++ b/include/meson.build
@@ -1,4 +1,5 @@
libfuse_headers = [ 'fuse.h', 'fuse_common.h', 'fuse_lowlevel.h',
- 'fuse_opt.h', 'cuse_lowlevel.h', 'fuse_log.h' ]
+ 'fuse_opt.h', 'cuse_lowlevel.h', 'fuse_log.h',
+ 'fuse_daemonize.h' ]
install_headers(libfuse_headers, subdir: 'fuse3')
diff --git a/lib/fuse_daemonize.c b/lib/fuse_daemonize.c
new file mode 100644
index 0000000000000000000000000000000000000000..5d191e7d737d04876d02ef6bd526061c003d2ab7
--- /dev/null
+++ b/lib/fuse_daemonize.c
@@ -0,0 +1,284 @@
+/*
+ * FUSE: Filesystem in Userspace
+ * Copyright (C) 2026 Bernd Schubert <bsbernd.com>
+ *
+ * This program can be distributed under the terms of the GNU LGPLv2.
+ * See the file COPYING.LIB.
+ */
+
+#define _GNU_SOURCE
+
+#include "fuse_daemonize.h"
+
+#include <fcntl.h>
+#include <poll.h>
+#include <pthread.h>
+#include <stdatomic.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <stdbool.h>
+#include <errno.h>
+
+/* Private/internal data */
+struct fuse_daemonize {
+ unsigned int flags;
+ int signal_pipe_wr; /* write end for signaling parent */
+ int death_pipe_rd; /* read end, POLLHUP when parent dies */
+ int stop_pipe_rd; /* read end for stop signal */
+ int stop_pipe_wr; /* write end for stop signal */
+ pthread_t watcher;
+ int watcher_started;
+
+ _Atomic bool active;
+
+ _Atomic bool daemonized;
+};
+
+/* Global daemonization object pointer */
+static _Atomic(struct fuse_daemonize *) daemonize;
+
+/* Watcher thread: polls for parent death or stop signal */
+static void *parent_watcher_thread(void *arg)
+{
+ struct fuse_daemonize *di = arg;
+ struct pollfd pfd[2];
+
+ pfd[0].fd = di->death_pipe_rd;
+ pfd[0].events = POLLIN;
+ pfd[1].fd = di->stop_pipe_rd;
+ pfd[1].events = POLLIN;
+
+ while (1) {
+ int rc = poll(pfd, 2, -1);
+
+ if (rc < 0)
+ continue;
+
+ /* Parent died - death pipe write end closed */
+ if (pfd[0].revents & (POLLHUP | POLLERR))
+ _exit(EXIT_FAILURE);
+
+ /* Stop signal received */
+ if (pfd[1].revents & POLLIN)
+ break;
+ }
+ return NULL;
+}
+
+static int start_parent_watcher(struct fuse_daemonize *daemonize)
+{
+ int rc;
+
+ rc = pthread_create(&daemonize->watcher, NULL, parent_watcher_thread,
+ daemonize);
+ if (rc != 0) {
+ perror("fuse_daemonize: pthread_create");
+ return -1;
+ }
+ daemonize->watcher_started = 1;
+ return 0;
+}
+
+static void stop_parent_watcher(struct fuse_daemonize *daemonize)
+{
+ char byte = 0;
+
+ if (daemonize && daemonize->watcher_started) {
+ /* Signal watcher to stop */
+ if (write(daemonize->stop_pipe_wr, &byte, 1) != 1)
+ perror("fuse_daemonize: stop write");
+ pthread_join(daemonize->watcher, NULL);
+ daemonize->watcher_started = 0;
+ }
+}
+
+static int daemonize_child(struct fuse_daemonize *daemonize)
+{
+ int stop_pipe[2];
+
+ if (pipe(stop_pipe) == -1) {
+ perror("fuse_daemonize_start: stop pipe");
+ return -1;
+ }
+ daemonize->stop_pipe_rd = stop_pipe[0];
+ daemonize->stop_pipe_wr = stop_pipe[1];
+
+ if (setsid() == -1) {
+ perror("fuse_daemonize_start: setsid");
+ goto err_close_stop;
+ }
+
+ /* Close stdin immediately */
+ int nullfd = open("/dev/null", O_RDWR, 0);
+
+ if (nullfd != -1) {
+ (void)dup2(nullfd, 0);
+ if (nullfd > 0)
+ close(nullfd);
+ }
+
+ /* Start watcher thread to detect parent death */
+ if (start_parent_watcher(daemonize) != 0)
+ goto err_close_stop;
+
+ daemonize->daemonized = true;
+ return 0;
+
+err_close_stop:
+ close(daemonize->stop_pipe_rd);
+ close(daemonize->stop_pipe_wr);
+ return -1;
+}
+
+/* Fork and daemonize. Returns 0 in child, never returns in parent. */
+static int do_daemonize(struct fuse_daemonize *daemonize)
+{
+ int signal_pipe[2];
+ int death_pipe[2];
+
+ if (pipe(signal_pipe) == -1) {
+ perror("fuse_daemonize_start: signal pipe");
+ return -1;
+ }
+
+ if (pipe(death_pipe) == -1) {
+ perror("fuse_daemonize_start: death pipe");
+ close(signal_pipe[0]);
+ close(signal_pipe[1]);
+ return -1;
+ }
+
+ switch (fork()) {
+ case -1:
+ perror("fuse_daemonize_start: fork");
+ close(signal_pipe[0]);
+ close(signal_pipe[1]);
+ close(death_pipe[0]);
+ close(death_pipe[1]);
+ return -1;
+
+ case 0:
+ /* Child: signal write end, death read end */
+ close(signal_pipe[0]);
+ close(death_pipe[1]);
+ daemonize->signal_pipe_wr = signal_pipe[1];
+ daemonize->death_pipe_rd = death_pipe[0];
+ return daemonize_child(daemonize);
+
+ default: {
+ /* Parent: signal read end, death write end (kept open) */
+ unsigned char status;
+ ssize_t res;
+
+ close(signal_pipe[1]);
+ close(death_pipe[0]);
+
+ res = read(signal_pipe[0], &status, sizeof(status));
+ close(signal_pipe[0]);
+ close(death_pipe[1]);
+
+ if (res != sizeof(status))
+ _exit(EXIT_FAILURE);
+ _exit(status);
+ }
+ }
+}
+
+int fuse_daemonize_start(unsigned int flags)
+{
+ struct fuse_daemonize *dm;
+ struct fuse_daemonize *expected = NULL;
+
+ dm = calloc(1, sizeof(*dm));
+ if (dm == NULL) {
+ fprintf(stderr, "%s: calloc failed\n", __func__);
+ return -ENOMEM;
+ }
+
+ dm->flags = flags;
+ dm->signal_pipe_wr = -1;
+ dm->death_pipe_rd = -1;
+ dm->stop_pipe_rd = -1;
+ dm->stop_pipe_wr = -1;
+ dm->active = true;
+
+ if (!(flags & FUSE_DAEMONIZE_NO_CHDIR))
+ (void)chdir("/");
+
+ if (!(flags & FUSE_DAEMONIZE_NO_BACKGROUND)) {
+ if (do_daemonize(dm) != 0) {
+ free(dm);
+ return -errno;
+ }
+ }
+
+ /* Set global pointer using CAS - fail if already set */
+ if (!atomic_compare_exchange_strong(&daemonize, &expected, dm)) {
+ fprintf(stderr, "%s: already active\n", __func__);
+ free(dm);
+ return -EEXIST;
+ }
+
+ return 0;
+}
+
+static void close_if_valid(int *fd)
+{
+ if (*fd != -1) {
+ close(*fd);
+ *fd = -1;
+ }
+}
+
+void fuse_daemonize_signal(int status)
+{
+ struct fuse_daemonize *dm;
+ unsigned char st;
+
+ dm = atomic_load(&daemonize);
+ if (dm == NULL || !dm->active)
+ return;
+
+ dm->active = false;
+
+ /* Stop watcher before signaling - parent will exit after this */
+ stop_parent_watcher(dm);
+
+ /* Signal status to parent */
+ if (dm->signal_pipe_wr != -1) {
+ st = (status != 0) ? EXIT_FAILURE : EXIT_SUCCESS;
+ if (write(dm->signal_pipe_wr, &st, sizeof(st)) != sizeof(st))
+ fprintf(stderr, "%s: write failed\n", __func__);
+ }
+
+ /* Redirect stdout/stderr to /dev/null on success */
+ if (status == 0 && dm->daemonized) {
+ int nullfd = open("/dev/null", O_RDWR, 0);
+
+ if (nullfd != -1) {
+ (void)dup2(nullfd, 1);
+ (void)dup2(nullfd, 2);
+ if (nullfd > 2)
+ close(nullfd);
+ }
+ }
+
+ close_if_valid(&dm->signal_pipe_wr);
+ close_if_valid(&dm->death_pipe_rd);
+ close_if_valid(&dm->stop_pipe_rd);
+ close_if_valid(&dm->stop_pipe_wr);
+
+ /* Clear global pointer using CAS and free */
+ if (atomic_compare_exchange_strong(&daemonize, &dm, NULL))
+ free(dm);
+}
+
+bool fuse_daemonize_active(void)
+{
+ struct fuse_daemonize *dm = atomic_load(&daemonize);
+
+ return dm != NULL && (dm->daemonized || dm->active);
+}
diff --git a/lib/fuse_i.h b/lib/fuse_i.h
index 65d2f68f7f30918a3c3ee4d473796cb013428a8f..9e3c5dc5021e210a2778e975a37ab609af324010 100644
--- a/lib/fuse_i.h
+++ b/lib/fuse_i.h
@@ -17,7 +17,6 @@
#include <semaphore.h>
#include <stdint.h>
#include <stdbool.h>
-#include <errno.h>
#include <stdatomic.h>
#define MIN(a, b) \
@@ -110,6 +109,9 @@ struct fuse_session {
/* true if reading requests from /dev/fuse are handled internally */
bool buf_reallocable;
+ /* synchronous FUSE_INIT support */
+ bool want_sync_init;
+
/* io_uring */
struct fuse_session_uring uring;
diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c
index 3234f0ce3b246a4c2c40dc0757177de91b6608b2..4a1c7c4c02a05706e077a7ea89fab3c81bfe22cd 100644
--- a/lib/fuse_lowlevel.c
+++ b/lib/fuse_lowlevel.c
@@ -19,6 +19,7 @@
#include "mount_util.h"
#include "util.h"
#include "fuse_uring_i.h"
+#include "fuse_daemonize.h"
#include <pthread.h>
#include <stdatomic.h>
diff --git a/lib/fuse_versionscript b/lib/fuse_versionscript
index cce09610316f4b0b1d6836dd0e63686342b70037..f1765d39e13bc9b1f53e625b9a091c5fa53f5afd 100644
--- a/lib/fuse_versionscript
+++ b/lib/fuse_versionscript
@@ -227,6 +227,9 @@ FUSE_3.19 {
fuse_session_start_teardown_watchdog;
fuse_session_stop_teardown_watchdog;
fuse_lowlevel_notify_prune;
+ fuse_daemonize_start;
+ fuse_daemonize_signal;
+ fuse_daemonize_active;
} FUSE_3.18;
# Local Variables:
diff --git a/lib/helper.c b/lib/helper.c
index 5c13b93a473181f027eba01e0bfefd78875ede3e..e6ec74364d000a6e091e0596fc74954b11cc51ab 100644
--- a/lib/helper.c
+++ b/lib/helper.c
@@ -15,6 +15,7 @@
#include "fuse_misc.h"
#include "fuse_opt.h"
#include "fuse_lowlevel.h"
+#include "fuse_daemonize.h"
#include "mount_util.h"
#include <stdio.h>
@@ -352,17 +353,19 @@ int fuse_main_real_versioned(int argc, char *argv[],
goto out1;
}
+ struct fuse_session *se = fuse_get_session(fuse);
if (fuse_mount(fuse,opts.mountpoint) != 0) {
res = 4;
goto out2;
}
- if (fuse_daemonize(opts.foreground) != 0) {
- res = 5;
- goto out3;
+ if (!fuse_daemonize_active()) {
+ /* Avoid daemonizing if we are already daemonized by the newer API */
+ if (fuse_daemonize(opts.foreground) != 0) {
+ res = 5;
+ goto out3;
+ }
}
-
- struct fuse_session *se = fuse_get_session(fuse);
if (fuse_set_signal_handlers(se) != 0) {
res = 6;
goto out3;
diff --git a/lib/meson.build b/lib/meson.build
index fcd95741c9d3748fa01d9ec52b417aca66745f26..5bd449ebffe7c9229df904d647d990c6c47f80b5 100644
--- a/lib/meson.build
+++ b/lib/meson.build
@@ -2,7 +2,8 @@ libfuse_sources = ['fuse.c', 'fuse_i.h', 'fuse_loop.c', 'fuse_loop_mt.c',
'fuse_lowlevel.c', 'fuse_misc.h', 'fuse_opt.c',
'fuse_signals.c', 'buffer.c', 'cuse_lowlevel.c',
'helper.c', 'modules/subdir.c', 'mount_util.c',
- 'fuse_log.c', 'compat.c', 'util.c', 'util.h' ]
+ 'fuse_log.c', 'compat.c', 'util.c', 'util.h',
+ 'fuse_daemonize.c' ]
if host_machine.system().startswith('linux')
libfuse_sources += [ 'mount.c' ]
diff --git a/test/test_want_conversion.c b/test/test_want_conversion.c
index db731edbfe1be8230ae16b422f798603b4a3bb82..48e6dd2dc6084425a0462bba000563c6083160be 100644
--- a/test/test_want_conversion.c
+++ b/test/test_want_conversion.c
@@ -8,6 +8,7 @@
#include <inttypes.h>
#include <stdbool.h>
#include <err.h>
+#include <errno.h>
static void print_conn_info(const char *prefix, struct fuse_conn_info *conn)
{
--
2.43.0
^ permalink raw reply related [flat|nested] 59+ messages in thread
* [PATCH 05/19] Sync fuse_kernel.h with linux-6.18
2026-03-23 17:44 [PATCH 00/19] libfuse: Add support for synchronous init Bernd Schubert
` (3 preceding siblings ...)
2026-03-23 17:44 ` [PATCH 04/19] Add a new daemonize API Bernd Schubert
@ 2026-03-23 17:45 ` Bernd Schubert
2026-03-23 21:16 ` Darrick J. Wong
2026-03-23 17:45 ` [PATCH 06/19] mount.c: Split fuse_mount_sys to prepare privileged sync FUSE_INIT Bernd Schubert
` (14 subsequent siblings)
19 siblings, 1 reply; 59+ messages in thread
From: Bernd Schubert @ 2026-03-23 17:45 UTC (permalink / raw)
To: linux-fsdevel
Cc: Miklos Szeredi, Joanne Koong, Darrick J. Wong, Bernd Schubert,
Bernd Schubert
From: Bernd Schubert <bschubert@ddn.com>
Signed-off-by: Bernd Schubert <bschubert@ddn.com>
---
include/fuse_kernel.h | 1 +
1 file changed, 1 insertion(+)
diff --git a/include/fuse_kernel.h b/include/fuse_kernel.h
index f0dee3d6cf51b0eb83104c1f30f70b8ba9d710cd..c13e1f9a2f12bd39f535188cb5466688eba42263 100644
--- a/include/fuse_kernel.h
+++ b/include/fuse_kernel.h
@@ -1138,6 +1138,7 @@ struct fuse_backing_map {
#define FUSE_DEV_IOC_BACKING_OPEN _IOW(FUSE_DEV_IOC_MAGIC, 1, \
struct fuse_backing_map)
#define FUSE_DEV_IOC_BACKING_CLOSE _IOW(FUSE_DEV_IOC_MAGIC, 2, uint32_t)
+#define FUSE_DEV_IOC_SYNC_INIT _IO(FUSE_DEV_IOC_MAGIC, 3)
struct fuse_lseek_in {
uint64_t fh;
--
2.43.0
^ permalink raw reply related [flat|nested] 59+ messages in thread
* [PATCH 06/19] mount.c: Split fuse_mount_sys to prepare privileged sync FUSE_INIT
2026-03-23 17:44 [PATCH 00/19] libfuse: Add support for synchronous init Bernd Schubert
` (4 preceding siblings ...)
2026-03-23 17:45 ` [PATCH 05/19] Sync fuse_kernel.h with linux-6.18 Bernd Schubert
@ 2026-03-23 17:45 ` Bernd Schubert
2026-03-23 22:34 ` Darrick J. Wong
2026-03-23 17:45 ` [PATCH 07/19] Add FUSE_MOUNT_FALLBACK_NEEDED define for -2 mount errors Bernd Schubert
` (13 subsequent siblings)
19 siblings, 1 reply; 59+ messages in thread
From: Bernd Schubert @ 2026-03-23 17:45 UTC (permalink / raw)
To: linux-fsdevel
Cc: Miklos Szeredi, Joanne Koong, Darrick J. Wong, Bernd Schubert,
Bernd Schubert
From: Bernd Schubert <bschubert@ddn.com>
Synchronous FUSE_INIT needs to spawn a worker thread to handle
FUSE_INIT before starting the mount. For that it needs to have
the device file descriptor - this is preparation work and
fuse_mount_sys() is split into fuse_kern_mount_prepare()
and fuse_kern_do_mount().
Signed-off-by: Bernd Schubert <bschubert@ddn.com>
---
lib/mount.c | 88 ++++++++++++++++++++++++++++++++++++++++++++++++-------------
1 file changed, 69 insertions(+), 19 deletions(-)
diff --git a/lib/mount.c b/lib/mount.c
index 398bf9e7a86743ac3c99f0cc2975e8db92346013..6e404451cc9edc8e35434cc31f25612cfc4edca1 100644
--- a/lib/mount.c
+++ b/lib/mount.c
@@ -506,13 +506,11 @@ static int fuse_mount_fusermount(const char *mountpoint, struct mount_opts *mo,
#define O_CLOEXEC 0
#endif
-static int fuse_mount_sys(const char *mnt, struct mount_opts *mo,
- const char *mnt_opts)
+static int fuse_kern_mount_prepare(const char *mnt,
+ struct mount_opts *mo)
{
char tmp[128];
const char *devname = getenv(FUSE_KERN_DEVICE_ENV) ?: "/dev/fuse";
- char *source = NULL;
- char *type = NULL;
struct stat stbuf;
int fd;
int res;
@@ -524,32 +522,58 @@ static int fuse_mount_sys(const char *mnt, struct mount_opts *mo,
res = stat(mnt, &stbuf);
if (res == -1) {
- fuse_log(FUSE_LOG_ERR, "fuse: failed to access mountpoint %s: %s\n",
- mnt, strerror(errno));
+ fuse_log(FUSE_LOG_ERR,
+ "fuse: failed to access mountpoint %s: %s\n", mnt,
+ strerror(errno));
return -1;
}
fd = open(devname, O_RDWR | O_CLOEXEC);
if (fd == -1) {
if (errno == ENODEV || errno == ENOENT)
- fuse_log(FUSE_LOG_ERR,
+ fuse_log(
+ FUSE_LOG_ERR,
"fuse: device %s not found. Kernel module not loaded?\n",
devname);
else
fuse_log(FUSE_LOG_ERR, "fuse: failed to open %s: %s\n",
- devname, strerror(errno));
+ devname, strerror(errno));
return -1;
}
if (!O_CLOEXEC)
fcntl(fd, F_SETFD, FD_CLOEXEC);
- snprintf(tmp, sizeof(tmp), "fd=%i,rootmode=%o,user_id=%u,group_id=%u",
+ snprintf(tmp, sizeof(tmp), "fd=%i,rootmode=%o,user_id=%u,group_id=%u",
fd, stbuf.st_mode & S_IFMT, getuid(), getgid());
res = fuse_opt_add_opt(&mo->kernel_opts, tmp);
if (res == -1)
goto out_close;
+ return fd;
+
+out_close:
+ close(fd);
+ return -1;
+}
+
+/**
+ * Complete the mount operation with an already-opened fd
+ * @mnt: mountpoint
+ * @mo: mount options
+ * @mnt_opts: mount options to pass to the kernel
+ *
+ * Returns: 0 on success, -1 on failure, -2 if fusermount should be used
+ */
+static int fuse_kern_do_mount(const char *mnt, struct mount_opts *mo,
+ const char *mnt_opts)
+{
+ const char *devname = getenv(FUSE_KERN_DEVICE_ENV) ?: "/dev/fuse";
+ char *source = NULL;
+ char *type = NULL;
+ int res;
+
+ res = -ENOMEM;
source = malloc((mo->fsname ? strlen(mo->fsname) : 0) +
(mo->subtype ? strlen(mo->subtype) : 0) +
strlen(devname) + 32);
@@ -593,10 +617,11 @@ static int fuse_mount_sys(const char *mnt, struct mount_opts *mo,
if (mo->blkdev && errno == ENODEV &&
!fuse_mnt_check_fuseblk())
fuse_log(FUSE_LOG_ERR,
- "fuse: 'fuseblk' support missing\n");
+ "fuse: 'fuseblk' support missing\n");
else
- fuse_log(FUSE_LOG_ERR, "fuse: mount failed: %s\n",
- strerror(errno_save));
+ fuse_log(FUSE_LOG_ERR,
+ "fuse: mount failed: %s\n",
+ strerror(errno_save));
}
goto out_close;
@@ -619,17 +644,35 @@ static int fuse_mount_sys(const char *mnt, struct mount_opts *mo,
free(type);
free(source);
- return fd;
+ return 0;
out_umount:
umount2(mnt, 2); /* lazy umount */
out_close:
free(type);
free(source);
- close(fd);
return res;
}
+static int fuse_mount_sys(const char *mnt, struct mount_opts *mo,
+ const char *mnt_opts)
+{
+ int fd;
+ int res;
+
+ fd = fuse_kern_mount_prepare(mnt, mo);
+ if (fd == -1)
+ return -1;
+
+ res = fuse_kern_do_mount(mnt, mo, mnt_opts);
+ if (res) {
+ close(fd);
+ return res;
+ }
+
+ return fd;
+}
+
static int get_mnt_flag_opts(char **mnt_optsp, int flags)
{
int i;
@@ -678,6 +721,17 @@ void destroy_mount_opts(struct mount_opts *mo)
free(mo);
}
+static int fuse_kern_mount_get_base_mnt_opts(struct mount_opts *mo,
+ char **mnt_optsp)
+{
+ if (get_mnt_flag_opts(mnt_optsp, mo->flags) == -1)
+ return -1;
+ if (mo->kernel_opts && fuse_opt_add_opt(mnt_optsp, mo->kernel_opts) == -1)
+ return -1;
+ if (mo->mtab_opts && fuse_opt_add_opt(mnt_optsp, mo->mtab_opts) == -1)
+ return -1;
+ return 0;
+}
int fuse_kern_mount(const char *mountpoint, struct mount_opts *mo)
{
@@ -685,11 +739,7 @@ int fuse_kern_mount(const char *mountpoint, struct mount_opts *mo)
char *mnt_opts = NULL;
res = -1;
- if (get_mnt_flag_opts(&mnt_opts, mo->flags) == -1)
- goto out;
- if (mo->kernel_opts && fuse_opt_add_opt(&mnt_opts, mo->kernel_opts) == -1)
- goto out;
- if (mo->mtab_opts && fuse_opt_add_opt(&mnt_opts, mo->mtab_opts) == -1)
+ if (fuse_kern_mount_get_base_mnt_opts(mo, &mnt_opts) == -1)
goto out;
res = fuse_mount_sys(mountpoint, mo, mnt_opts);
--
2.43.0
^ permalink raw reply related [flat|nested] 59+ messages in thread
* [PATCH 07/19] Add FUSE_MOUNT_FALLBACK_NEEDED define for -2 mount errors
2026-03-23 17:44 [PATCH 00/19] libfuse: Add support for synchronous init Bernd Schubert
` (5 preceding siblings ...)
2026-03-23 17:45 ` [PATCH 06/19] mount.c: Split fuse_mount_sys to prepare privileged sync FUSE_INIT Bernd Schubert
@ 2026-03-23 17:45 ` Bernd Schubert
2026-03-23 22:36 ` Darrick J. Wong
2026-03-23 17:45 ` [PATCH 08/19] Refactor mount code / move common functions to mount_util.c Bernd Schubert
` (12 subsequent siblings)
19 siblings, 1 reply; 59+ messages in thread
From: Bernd Schubert @ 2026-03-23 17:45 UTC (permalink / raw)
To: linux-fsdevel
Cc: Miklos Szeredi, Joanne Koong, Darrick J. Wong, Bernd Schubert,
Bernd Schubert
From: Bernd Schubert <bschubert@ddn.com>
Magic numbers in the code are not good - we better use a define.
Signed-off-by: Bernd Schubert <bschubert@ddn.com>
---
lib/fuse_i.h | 3 +++
lib/mount.c | 6 +++---
2 files changed, 6 insertions(+), 3 deletions(-)
diff --git a/lib/fuse_i.h b/lib/fuse_i.h
index 9e3c5dc5021e210a2778e975a37ab609af324010..b4c1d3eef41010287f6c9555ec0b2442d904d192 100644
--- a/lib/fuse_i.h
+++ b/lib/fuse_i.h
@@ -217,6 +217,9 @@ struct fuse_chan *fuse_chan_get(struct fuse_chan *ch);
*/
void fuse_chan_put(struct fuse_chan *ch);
+/* Special return value for mount functions to indicate fallback to fusermount3 is needed */
+#define FUSE_MOUNT_FALLBACK_NEEDED -2
+
struct mount_opts *parse_mount_opts(struct fuse_args *args);
void destroy_mount_opts(struct mount_opts *mo);
void fuse_mount_version(void);
diff --git a/lib/mount.c b/lib/mount.c
index 6e404451cc9edc8e35434cc31f25612cfc4edca1..dec9d52274c13536648cacef959789f472c5682c 100644
--- a/lib/mount.c
+++ b/lib/mount.c
@@ -563,7 +563,7 @@ out_close:
* @mo: mount options
* @mnt_opts: mount options to pass to the kernel
*
- * Returns: 0 on success, -1 on failure, -2 if fusermount should be used
+ * Returns: 0 on success, -1 on failure, FUSE_MOUNT_FALLBACK_NEEDED if fusermount should be used
*/
static int fuse_kern_do_mount(const char *mnt, struct mount_opts *mo,
const char *mnt_opts)
@@ -611,7 +611,7 @@ static int fuse_kern_do_mount(const char *mnt, struct mount_opts *mo,
* case try falling back to fusermount3
*/
if (errno == EPERM) {
- res = -2;
+ res = FUSE_MOUNT_FALLBACK_NEEDED;
} else {
int errno_save = errno;
if (mo->blkdev && errno == ENODEV &&
@@ -749,7 +749,7 @@ int fuse_kern_mount(const char *mountpoint, struct mount_opts *mo)
umount2(mountpoint, MNT_DETACH); /* lazy umount */
res = -1;
}
- } else if (res == -2) {
+ } else if (res == FUSE_MOUNT_FALLBACK_NEEDED) {
if (mo->fusermount_opts &&
fuse_opt_add_opt(&mnt_opts, mo->fusermount_opts) == -1)
goto out;
--
2.43.0
^ permalink raw reply related [flat|nested] 59+ messages in thread
* [PATCH 08/19] Refactor mount code / move common functions to mount_util.c
2026-03-23 17:44 [PATCH 00/19] libfuse: Add support for synchronous init Bernd Schubert
` (6 preceding siblings ...)
2026-03-23 17:45 ` [PATCH 07/19] Add FUSE_MOUNT_FALLBACK_NEEDED define for -2 mount errors Bernd Schubert
@ 2026-03-23 17:45 ` Bernd Schubert
2026-03-23 22:40 ` Darrick J. Wong
2026-03-23 17:45 ` [PATCH 09/19] Move mount flags to mount_i.h Bernd Schubert
` (11 subsequent siblings)
19 siblings, 1 reply; 59+ messages in thread
From: Bernd Schubert @ 2026-03-23 17:45 UTC (permalink / raw)
To: linux-fsdevel
Cc: Miklos Szeredi, Joanne Koong, Darrick J. Wong, Bernd Schubert,
Bernd Schubert
From: Bernd Schubert <bschubert@ddn.com>
Also create the new "mount_i.h", which is independent of the
the rest of libfuse.
This is preparation for the new mount API, which goes into a new file.
Signed-off-by: Bernd Schubert <bschubert@ddn.com>
---
lib/fuse_i.h | 10 ++----
lib/mount.c | 96 ++++++++++++++++++++++++++++------------------------
lib/mount_bsd.c | 1 +
lib/mount_common_i.h | 31 +++++++++++++++++
lib/mount_i_linux.h | 32 ++++++++++++++++++
lib/mount_util.c | 24 +++++++++++++
lib/mount_util.h | 6 ++++
7 files changed, 148 insertions(+), 52 deletions(-)
diff --git a/lib/fuse_i.h b/lib/fuse_i.h
index b4c1d3eef41010287f6c9555ec0b2442d904d192..6d63c9fd2149eb4ae3b0e0170640a4ce2eed4705 100644
--- a/lib/fuse_i.h
+++ b/lib/fuse_i.h
@@ -19,14 +19,15 @@
#include <stdbool.h>
#include <stdatomic.h>
+#ifndef MIN
#define MIN(a, b) \
({ \
typeof(a) _a = (a); \
typeof(b) _b = (b); \
_a < _b ? _a : _b; \
})
+#endif
-struct mount_opts;
struct fuse_ring_pool;
struct fuse_req {
@@ -217,13 +218,8 @@ struct fuse_chan *fuse_chan_get(struct fuse_chan *ch);
*/
void fuse_chan_put(struct fuse_chan *ch);
-/* Special return value for mount functions to indicate fallback to fusermount3 is needed */
-#define FUSE_MOUNT_FALLBACK_NEEDED -2
-
-struct mount_opts *parse_mount_opts(struct fuse_args *args);
-void destroy_mount_opts(struct mount_opts *mo);
+/* Mount-related functions */
void fuse_mount_version(void);
-unsigned get_max_read(struct mount_opts *o);
void fuse_kern_unmount(const char *mountpoint, int fd);
int fuse_kern_mount(const char *mountpoint, struct mount_opts *mo);
diff --git a/lib/mount.c b/lib/mount.c
index dec9d52274c13536648cacef959789f472c5682c..fe353e2cc4579adb47473cac5db7d1bae2defb2c 100644
--- a/lib/mount.c
+++ b/lib/mount.c
@@ -16,6 +16,7 @@
#include "fuse_misc.h"
#include "fuse_opt.h"
#include "mount_util.h"
+#include "mount_i_linux.h"
#include <stdio.h>
#include <stdlib.h>
@@ -49,7 +50,6 @@
#define FUSERMOUNT_PROG "fusermount3"
#define FUSE_COMMFD_ENV "_FUSE_COMMFD"
#define FUSE_COMMFD2_ENV "_FUSE_COMMFD2"
-#define FUSE_KERN_DEVICE_ENV "FUSE_KERN_DEVICE"
#ifndef MS_DIRSYNC
#define MS_DIRSYNC 128
@@ -65,20 +65,6 @@ enum {
KEY_RO,
};
-struct mount_opts {
- int allow_other;
- int flags;
- int auto_unmount;
- int blkdev;
- char *fsname;
- char *subtype;
- char *subtype_opt;
- char *mtab_opts;
- char *fusermount_opts;
- char *kernel_opts;
- unsigned max_read;
-};
-
#define FUSE_MOUNT_OPT(t, p) { t, offsetof(struct mount_opts, p), 1 }
static const struct fuse_opt fuse_mount_opts[] = {
@@ -197,7 +183,7 @@ static const struct mount_flags mount_flags[] = {
{NULL, 0, 0}
};
-unsigned get_max_read(struct mount_opts *o)
+unsigned int get_max_read(struct mount_opts *o)
{
return o->max_read;
}
@@ -510,7 +496,7 @@ static int fuse_kern_mount_prepare(const char *mnt,
struct mount_opts *mo)
{
char tmp[128];
- const char *devname = getenv(FUSE_KERN_DEVICE_ENV) ?: "/dev/fuse";
+ const char *devname = fuse_mnt_get_devname();
struct stat stbuf;
int fd;
int res;
@@ -563,35 +549,24 @@ out_close:
* @mo: mount options
* @mnt_opts: mount options to pass to the kernel
*
- * Returns: 0 on success, -1 on failure, FUSE_MOUNT_FALLBACK_NEEDED if fusermount should be used
+ * Returns: 0 on success, -1 on failure,
+ * FUSE_MOUNT_FALLBACK_NEEDED if fusermount should be used
*/
static int fuse_kern_do_mount(const char *mnt, struct mount_opts *mo,
const char *mnt_opts)
{
- const char *devname = getenv(FUSE_KERN_DEVICE_ENV) ?: "/dev/fuse";
char *source = NULL;
char *type = NULL;
int res;
res = -ENOMEM;
- source = malloc((mo->fsname ? strlen(mo->fsname) : 0) +
- (mo->subtype ? strlen(mo->subtype) : 0) +
- strlen(devname) + 32);
-
- type = malloc((mo->subtype ? strlen(mo->subtype) : 0) + 32);
+ source = fuse_mnt_build_source(mo);
+ type = fuse_mnt_build_type(mo);
if (!type || !source) {
fuse_log(FUSE_LOG_ERR, "fuse: failed to allocate memory\n");
goto out_close;
}
- strcpy(type, mo->blkdev ? "fuseblk" : "fuse");
- if (mo->subtype) {
- strcat(type, ".");
- strcat(type, mo->subtype);
- }
- strcpy(source,
- mo->fsname ? mo->fsname : (mo->subtype ? mo->subtype : devname));
-
res = mount(source, mnt, type, mo->flags, mo->kernel_opts);
if (res == -1 && errno == ENODEV && mo->subtype) {
/* Probably missing subtype support */
@@ -627,20 +602,10 @@ static int fuse_kern_do_mount(const char *mnt, struct mount_opts *mo,
goto out_close;
}
-#ifndef IGNORE_MTAB
- if (geteuid() == 0) {
- char *newmnt = fuse_mnt_resolve_path("fuse", mnt);
- res = -1;
- if (!newmnt)
- goto out_umount;
+ res = fuse_mnt_add_mount_helper(mnt, source, type, mnt_opts);
+ if (res == -1)
+ goto out_umount;
- res = fuse_mnt_add_mount("fuse", source, newmnt, type,
- mnt_opts);
- free(newmnt);
- if (res == -1)
- goto out_umount;
- }
-#endif /* IGNORE_MTAB */
free(type);
free(source);
@@ -777,3 +742,44 @@ out:
free(mnt_opts);
return res;
}
+
+const char *fuse_mnt_get_devname(void)
+{
+ const char *devname = getenv(FUSE_KERN_DEVICE_ENV);
+
+ return devname ? devname : "/dev/fuse";
+}
+
+char *fuse_mnt_build_source(const struct mount_opts *mo)
+{
+ const char *devname = fuse_mnt_get_devname();
+ char *source;
+
+ source = malloc((mo->fsname ? strlen(mo->fsname) : 0) +
+ (mo->subtype ? strlen(mo->subtype) : 0) +
+ strlen(devname) + 32);
+ if (!source)
+ return NULL;
+
+ strcpy(source,
+ mo->fsname ? mo->fsname : (mo->subtype ? mo->subtype : devname));
+
+ return source;
+}
+
+char *fuse_mnt_build_type(const struct mount_opts *mo)
+{
+ char *type;
+
+ type = malloc((mo->subtype ? strlen(mo->subtype) : 0) + 32);
+ if (!type)
+ return NULL;
+
+ strcpy(type, mo->blkdev ? "fuseblk" : "fuse");
+ if (mo->subtype) {
+ strcat(type, ".");
+ strcat(type, mo->subtype);
+ }
+
+ return type;
+}
diff --git a/lib/mount_bsd.c b/lib/mount_bsd.c
index c12ab322e7dc86d5c8d2f2abc6b71488e82523cf..c5b831160f826c0e4620626a72c4b93427d18bab 100644
--- a/lib/mount_bsd.c
+++ b/lib/mount_bsd.c
@@ -10,6 +10,7 @@
#include "fuse_config.h"
#include "fuse_i.h"
+#include "mount_common_i.h"
#include "fuse_misc.h"
#include "fuse_opt.h"
#include "util.h"
diff --git a/lib/mount_common_i.h b/lib/mount_common_i.h
new file mode 100644
index 0000000000000000000000000000000000000000..d27d2aa624ae3806c61a0fe382c2d024080c9bb3
--- /dev/null
+++ b/lib/mount_common_i.h
@@ -0,0 +1,31 @@
+/*
+ * FUSE: Filesystem in Userspace
+ * Copyright (C) 2001-2007 Miklos Szeredi <miklos@szeredi.hu>
+ * 2026 Bernd Schubert <bernd@bsbernd.com>
+ *
+ * This program can be distributed under the terms of the GNU LGPLv2.
+ * See the file LGPL2.txt
+ */
+
+#ifndef FUSE_MOUNT_COMMON_I_H_
+#define FUSE_MOUNT_COMMON_I_H_
+
+/* Forward declaration for fuse_args */
+struct fuse_args;
+struct mount_opts;
+
+/* Special return value for mount functions to indicate fallback to fusermount3 is needed */
+#define FUSE_MOUNT_FALLBACK_NEEDED -2
+
+/* Environment variable for FUSE kernel device */
+#define FUSE_KERN_DEVICE_ENV "FUSE_KERN_DEVICE"
+
+/* Mount options management functions */
+struct mount_opts *parse_mount_opts(struct fuse_args *args);
+void destroy_mount_opts(struct mount_opts *mo);
+unsigned int get_max_read(struct mount_opts *o);
+char *fuse_mnt_build_source(const struct mount_opts *mo);
+char *fuse_mnt_build_type(const struct mount_opts *mo);
+
+
+#endif /* FUSE_MOUNT_COMMON_I_H_ */
diff --git a/lib/mount_i_linux.h b/lib/mount_i_linux.h
new file mode 100644
index 0000000000000000000000000000000000000000..abcd1b08012feedef6b4c8961b55ac847a27496a
--- /dev/null
+++ b/lib/mount_i_linux.h
@@ -0,0 +1,32 @@
+/*
+ * FUSE: Filesystem in Userspace
+ * Copyright (C) 2001-2007 Miklos Szeredi <miklos@szeredi.hu>
+ * 2026 Bernd Schubert <bernd@bsbernd.com>
+ *
+ * This program can be distributed under the terms of the GNU LGPLv2.
+ * See the file LGPL2.txt
+ */
+
+#ifndef FUSE_MOUNT_I_H_
+#define FUSE_MOUNT_I_H_
+
+/* Forward declaration for fuse_args */
+struct fuse_args;
+
+/* Mount options structure */
+struct mount_opts {
+ int allow_other;
+ int flags;
+ int auto_unmount;
+ int blkdev;
+ char *fsname;
+ char *subtype;
+ char *subtype_opt;
+ char *mtab_opts;
+ char *fusermount_opts;
+ char *kernel_opts;
+ unsigned int max_read;
+};
+
+
+#endif /* FUSE_MOUNT_I_H_ */
diff --git a/lib/mount_util.c b/lib/mount_util.c
index 8c0cdf72d978da68c95125964416b92668104924..a42a02a0b92f98778abb2c491cdc7a01260f56ee 100644
--- a/lib/mount_util.c
+++ b/lib/mount_util.c
@@ -375,3 +375,27 @@ int fuse_mnt_parse_fuse_fd(const char *mountpoint)
return -1;
}
+
+int fuse_mnt_add_mount_helper(const char *mnt, const char *source,
+ const char *type, const char *mnt_opts)
+{
+#ifndef IGNORE_MTAB
+ if (geteuid() == 0) {
+ char *newmnt = fuse_mnt_resolve_path("fuse", mnt);
+ int res;
+
+ if (!newmnt)
+ return -1;
+
+ res = fuse_mnt_add_mount("fuse", source, newmnt, type,
+ mnt_opts);
+ free(newmnt);
+ return res;
+ }
+#endif
+ (void)mnt;
+ (void)source;
+ (void)type;
+ (void)mnt_opts;
+ return 0;
+}
diff --git a/lib/mount_util.h b/lib/mount_util.h
index 9cb9077dd177381d712e34e7118bf73572142c6a..4688d00091f001b3ccd36b1ef3b7e799031ed773 100644
--- a/lib/mount_util.h
+++ b/lib/mount_util.h
@@ -7,6 +7,7 @@
*/
#include <sys/types.h>
+#include "mount_common_i.h" // IWYU pragma: keep
int fuse_mnt_add_mount(const char *progname, const char *fsname,
const char *mnt, const char *type, const char *opts);
@@ -16,3 +17,8 @@ int fuse_mnt_umount(const char *progname, const char *abs_mnt,
char *fuse_mnt_resolve_path(const char *progname, const char *orig);
int fuse_mnt_check_fuseblk(void);
int fuse_mnt_parse_fuse_fd(const char *mountpoint);
+
+/* Helper functions for mount operations */
+const char *fuse_mnt_get_devname(void);
+int fuse_mnt_add_mount_helper(const char *mnt, const char *source,
+ const char *type, const char *mnt_opts);
--
2.43.0
^ permalink raw reply related [flat|nested] 59+ messages in thread
* [PATCH 09/19] Move mount flags to mount_i.h
2026-03-23 17:44 [PATCH 00/19] libfuse: Add support for synchronous init Bernd Schubert
` (7 preceding siblings ...)
2026-03-23 17:45 ` [PATCH 08/19] Refactor mount code / move common functions to mount_util.c Bernd Schubert
@ 2026-03-23 17:45 ` Bernd Schubert
2026-03-23 22:45 ` Darrick J. Wong
2026-03-23 17:45 ` [PATCH 10/19] conftest.py: Add more valgrind filter patterns Bernd Schubert
` (10 subsequent siblings)
19 siblings, 1 reply; 59+ messages in thread
From: Bernd Schubert @ 2026-03-23 17:45 UTC (permalink / raw)
To: linux-fsdevel
Cc: Miklos Szeredi, Joanne Koong, Darrick J. Wong, Bernd Schubert,
Bernd Schubert
From: Bernd Schubert <bschubert@ddn.com>
This is preparation work for the new mount API, which goes into
its own file.
Signed-off-by: Bernd Schubert <bschubert@ddn.com>
---
lib/mount.c | 59 ++++++++---------------------------------------------
lib/mount_i_linux.h | 35 ++++++++++++++++++++++++++++++-
2 files changed, 42 insertions(+), 52 deletions(-)
diff --git a/lib/mount.c b/lib/mount.c
index fe353e2cc4579adb47473cac5db7d1bae2defb2c..b3ee9ba4c0a74c1d0d55f916e7046e064f8468b0 100644
--- a/lib/mount.c
+++ b/lib/mount.c
@@ -34,16 +34,6 @@
#include "fuse_mount_compat.h"
#ifdef __NetBSD__
-#include <perfuse.h>
-
-#define MS_RDONLY MNT_RDONLY
-#define MS_NOSUID MNT_NOSUID
-#define MS_NODEV MNT_NODEV
-#define MS_NOEXEC MNT_NOEXEC
-#define MS_SYNCHRONOUS MNT_SYNCHRONOUS
-#define MS_NOATIME MNT_NOATIME
-#define MS_NOSYMFOLLOW MNT_NOSYMFOLLOW
-
#define umount2(mnt, flags) unmount(mnt, (flags == 2) ? MNT_FORCE : 0)
#endif
@@ -51,10 +41,6 @@
#define FUSE_COMMFD_ENV "_FUSE_COMMFD"
#define FUSE_COMMFD2_ENV "_FUSE_COMMFD2"
-#ifndef MS_DIRSYNC
-#define MS_DIRSYNC 128
-#endif
-
enum {
KEY_KERN_FLAG,
KEY_KERN_OPT,
@@ -154,35 +140,6 @@ void fuse_mount_version(void)
FUSERMOUNT_PROG);
}
-struct mount_flags {
- const char *opt;
- unsigned long flag;
- int on;
-};
-
-static const struct mount_flags mount_flags[] = {
- {"rw", MS_RDONLY, 0},
- {"ro", MS_RDONLY, 1},
- {"suid", MS_NOSUID, 0},
- {"nosuid", MS_NOSUID, 1},
- {"dev", MS_NODEV, 0},
- {"nodev", MS_NODEV, 1},
- {"exec", MS_NOEXEC, 0},
- {"noexec", MS_NOEXEC, 1},
- {"async", MS_SYNCHRONOUS, 0},
- {"sync", MS_SYNCHRONOUS, 1},
- {"noatime", MS_NOATIME, 1},
- {"nodiratime", MS_NODIRATIME, 1},
- {"norelatime", MS_RELATIME, 0},
- {"nostrictatime", MS_STRICTATIME, 0},
- {"symfollow", MS_NOSYMFOLLOW, 0},
- {"nosymfollow", MS_NOSYMFOLLOW, 1},
-#ifndef __NetBSD__
- {"dirsync", MS_DIRSYNC, 1},
-#endif
- {NULL, 0, 0}
-};
-
unsigned int get_max_read(struct mount_opts *o)
{
return o->max_read;
@@ -192,13 +149,13 @@ static void set_mount_flag(const char *s, int *flags)
{
int i;
- for (i = 0; mount_flags[i].opt != NULL; i++) {
- const char *opt = mount_flags[i].opt;
+ for (i = 0; fuse_mount_flags[i].opt != NULL; i++) {
+ const char *opt = fuse_mount_flags[i].opt;
if (strcmp(opt, s) == 0) {
- if (mount_flags[i].on)
- *flags |= mount_flags[i].flag;
+ if (fuse_mount_flags[i].on)
+ *flags |= fuse_mount_flags[i].flag;
else
- *flags &= ~mount_flags[i].flag;
+ *flags &= ~fuse_mount_flags[i].flag;
return;
}
}
@@ -645,9 +602,9 @@ static int get_mnt_flag_opts(char **mnt_optsp, int flags)
if (!(flags & MS_RDONLY) && fuse_opt_add_opt(mnt_optsp, "rw") == -1)
return -1;
- for (i = 0; mount_flags[i].opt != NULL; i++) {
- if (mount_flags[i].on && (flags & mount_flags[i].flag) &&
- fuse_opt_add_opt(mnt_optsp, mount_flags[i].opt) == -1)
+ for (i = 0; fuse_mount_flags[i].opt != NULL; i++) {
+ if (fuse_mount_flags[i].on && (flags & fuse_mount_flags[i].flag) &&
+ fuse_opt_add_opt(mnt_optsp, fuse_mount_flags[i].opt) == -1)
return -1;
}
return 0;
diff --git a/lib/mount_i_linux.h b/lib/mount_i_linux.h
index abcd1b08012feedef6b4c8961b55ac847a27496a..c0de6228fce5a4d9070cc246ec76222b66de56fb 100644
--- a/lib/mount_i_linux.h
+++ b/lib/mount_i_linux.h
@@ -10,7 +10,8 @@
#ifndef FUSE_MOUNT_I_H_
#define FUSE_MOUNT_I_H_
-/* Forward declaration for fuse_args */
+#include <sys/mount.h>
+
struct fuse_args;
/* Mount options structure */
@@ -28,5 +29,37 @@ struct mount_opts {
unsigned int max_read;
};
+/* Mount flags mapping structure */
+struct mount_flags {
+ const char *opt;
+ unsigned long flag;
+ int on;
+};
+
+/* Mount flags table */
+static const struct mount_flags fuse_mount_flags[] = {
+ {"rw", MS_RDONLY, 0},
+ {"ro", MS_RDONLY, 1},
+ {"suid", MS_NOSUID, 0},
+ {"nosuid", MS_NOSUID, 1},
+ {"dev", MS_NODEV, 0},
+ {"nodev", MS_NODEV, 1},
+ {"exec", MS_NOEXEC, 0},
+ {"noexec", MS_NOEXEC, 1},
+ {"async", MS_SYNCHRONOUS, 0},
+ {"sync", MS_SYNCHRONOUS, 1},
+ {"noatime", MS_NOATIME, 1},
+ {"nodiratime", MS_NODIRATIME, 1},
+ {"norelatime", MS_RELATIME, 0},
+ {"nostrictatime", MS_STRICTATIME, 0},
+ {"symfollow", MS_NOSYMFOLLOW, 0},
+ {"nosymfollow", MS_NOSYMFOLLOW, 1},
+#ifndef __NetBSD__
+ {"dirsync", MS_DIRSYNC, 1},
+#endif
+ {NULL, 0, 0}
+};
+
+
#endif /* FUSE_MOUNT_I_H_ */
--
2.43.0
^ permalink raw reply related [flat|nested] 59+ messages in thread
* [PATCH 10/19] conftest.py: Add more valgrind filter patterns
2026-03-23 17:44 [PATCH 00/19] libfuse: Add support for synchronous init Bernd Schubert
` (8 preceding siblings ...)
2026-03-23 17:45 ` [PATCH 09/19] Move mount flags to mount_i.h Bernd Schubert
@ 2026-03-23 17:45 ` Bernd Schubert
2026-03-23 17:45 ` [PATCH 11/19] Add support for the new linux mount API Bernd Schubert
` (9 subsequent siblings)
19 siblings, 0 replies; 59+ messages in thread
From: Bernd Schubert @ 2026-03-23 17:45 UTC (permalink / raw)
To: linux-fsdevel
Cc: Miklos Szeredi, Joanne Koong, Darrick J. Wong, Bernd Schubert,
Bernd Schubert
From: Bernd Schubert <bschubert@ddn.com>
Valgrind complains that it does not know the fsmount syscall,
pytest checks for warnings, find that and fails the test
--14936-- WARNING: unhandled amd64-linux syscall: 430
--14936-- You may be able to write your own handler.
--14936-- Read the file README_MISSING_SYSCALL_OR_IOCTL.
--14936-- Nevertheless we consider this a bug. Please report
--14936-- it at http://valgrind.org/support/bug_reports.html.
fuse: fsopen(fuse) failed: Function not implemented
=========================== short test summary info
Obviously we want to filter our valgrind warnings.
Signed-off-by: Bernd Schubert <bschubert@ddn.com>
---
test/conftest.py | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/test/conftest.py b/test/conftest.py
index 291c9199b9860d664600638213382bd8cc182104..45e9672a92e687980ba93cb25ef711ad1c2e3633 100644
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -67,11 +67,16 @@ class OutputChecker:
cp = re.compile(pattern, flags)
(buf, cnt) = cp.subn('', buf, count=count)
+ # Filter out Valgrind output lines before checking for suspicious words
+ # ==PID== prefix: Valgrind standard messages (errors, info)
+ # --PID-- prefix: Valgrind warnings (e.g., unhandled syscalls)
+ buf = re.sub(r'^==[0-9]+== .*$', '', buf, flags=re.MULTILINE)
+ buf = re.sub(r'^--[0-9]+-- .*$', '', buf, flags=re.MULTILINE)
+
patterns = [ r'\b{}\b'.format(x) for x in
('exception', 'error', 'warning', 'fatal', 'traceback',
'fault', 'crash(?:ed)?', 'abort(?:ed)',
'uninitiali[zs]ed') ]
- patterns += ['^==[0-9]+== ']
for pattern in patterns:
cp = re.compile(pattern, re.IGNORECASE | re.MULTILINE)
--
2.43.0
^ permalink raw reply related [flat|nested] 59+ messages in thread
* [PATCH 11/19] Add support for the new linux mount API
2026-03-23 17:44 [PATCH 00/19] libfuse: Add support for synchronous init Bernd Schubert
` (9 preceding siblings ...)
2026-03-23 17:45 ` [PATCH 10/19] conftest.py: Add more valgrind filter patterns Bernd Schubert
@ 2026-03-23 17:45 ` Bernd Schubert
2026-03-23 23:42 ` Darrick J. Wong
2026-03-23 17:45 ` [PATCH 12/19] fuse mount: Support synchronous FUSE_INIT (privileged daemon) Bernd Schubert
` (8 subsequent siblings)
19 siblings, 1 reply; 59+ messages in thread
From: Bernd Schubert @ 2026-03-23 17:45 UTC (permalink / raw)
To: linux-fsdevel
Cc: Miklos Szeredi, Joanne Koong, Darrick J. Wong, Bernd Schubert,
Bernd Schubert
From: Bernd Schubert <bschubert@ddn.com>
So far only supported for fuse_session_mount(), which is called
from high and low level API, but not yet supported for
fuse_open_channel(), which used for privilege drop through
mount.fuse. Main goal for the new API is support for synchronous
FUSE_INIT and I don't think that is going to work with
fuse_open_channel(). At least not with io-uring support as long
as it is started from FUSE_INIT.
Signed-off-by: Bernd Schubert <bschubert@ddn.com>
---
lib/fuse_lowlevel.c | 75 +++++++++-
lib/meson.build | 3 +
lib/mount.c | 27 +++-
lib/mount_fsmount.c | 405 ++++++++++++++++++++++++++++++++++++++++++++++++++++
lib/mount_i_linux.h | 14 ++
meson.build | 19 ++-
6 files changed, 535 insertions(+), 8 deletions(-)
diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c
index 4a1c7c4c02a05706e077a7ea89fab3c81bfe22cd..626233df20f49fa89cd9327f94340169d7061f75 100644
--- a/lib/fuse_lowlevel.c
+++ b/lib/fuse_lowlevel.c
@@ -20,6 +20,9 @@
#include "util.h"
#include "fuse_uring_i.h"
#include "fuse_daemonize.h"
+#if defined(__linux__)
+#include "mount_i_linux.h"
+#endif
#include <pthread.h>
#include <stdatomic.h>
@@ -4398,6 +4401,64 @@ int fuse_session_custom_io_30(struct fuse_session *se,
offsetof(struct fuse_custom_io, clone_fd), fd);
}
+#if defined(HAVE_NEW_MOUNT_API)
+/* Only linux supports sync FUSE_INIT so far */
+static int fuse_session_mount_new_api(struct fuse_session *se,
+ const char *mountpoint)
+{
+ int fd = -1;
+ int res, err;
+ char *mnt_opts = NULL;
+ char *mnt_opts_with_fd = NULL;
+ char fd_opt[32];
+
+ res = fuse_kern_mount_get_base_mnt_opts(se->mo, &mnt_opts);
+ if (res == -1) {
+ fuse_log(FUSE_LOG_ERR,
+ "fuse: failed to get base mount options\n");
+ err = -EIO;
+ goto err;
+ }
+
+ fd = fuse_kern_mount_prepare(mountpoint, se->mo);
+ if (fd == -1) {
+ fuse_log(FUSE_LOG_ERR, "Mount preparation failed.\n");
+ err = -EIO;
+ goto err;
+ }
+
+ snprintf(fd_opt, sizeof(fd_opt), "fd=%i", fd);
+ if (fuse_opt_add_opt(&mnt_opts_with_fd, mnt_opts) == -1 ||
+ fuse_opt_add_opt(&mnt_opts_with_fd, fd_opt) == -1) {
+ err = -ENOMEM;
+ goto err;
+ }
+
+ err = fuse_kern_fsmount_mo(mountpoint, se->mo, mnt_opts_with_fd);
+err:
+ if (err) {
+ if (fd >= 0)
+ close(fd);
+ fd = -1;
+ se->fd = -1;
+ se->error = -errno;
+ }
+
+ free(mnt_opts);
+ free(mnt_opts_with_fd);
+ return fd;
+}
+#else
+static int fuse_session_mount_new_api(struct fuse_session *se,
+ const char *mountpoint)
+{
+ (void)se;
+ (void)mountpoint;
+
+ return -1;
+}
+#endif
+
int fuse_session_mount(struct fuse_session *se, const char *_mountpoint)
{
int fd;
@@ -4425,6 +4486,8 @@ int fuse_session_mount(struct fuse_session *se, const char *_mountpoint)
close(fd);
} while (fd >= 0 && fd <= 2);
+ /* Open channel */
+
/*
* To allow FUSE daemons to run without privileges, the caller may open
* /dev/fuse before launching the file system and pass on the file
@@ -4443,10 +4506,18 @@ int fuse_session_mount(struct fuse_session *se, const char *_mountpoint)
return 0;
}
- /* Open channel */
+ /* new linux mount api */
+ fd = fuse_session_mount_new_api(se, mountpoint);
+ if (fd >= 0)
+ goto out;
+
+ /* fall back to old API */
+ se->error = 0; /* reset error of new api */
fd = fuse_kern_mount(mountpoint, se->mo);
- if (fd == -1)
+ if (fd < 0)
goto error_out;
+
+out:
se->fd = fd;
/* Save mountpoint */
diff --git a/lib/meson.build b/lib/meson.build
index 5bd449ebffe7c9229df904d647d990c6c47f80b5..5fd738a589c5aba97a738d5eedbf0f9962e4adfc 100644
--- a/lib/meson.build
+++ b/lib/meson.build
@@ -7,6 +7,9 @@ libfuse_sources = ['fuse.c', 'fuse_i.h', 'fuse_loop.c', 'fuse_loop_mt.c',
if host_machine.system().startswith('linux')
libfuse_sources += [ 'mount.c' ]
+ if private_cfg.get('HAVE_NEW_MOUNT_API', false)
+ libfuse_sources += [ 'mount_fsmount.c' ]
+ endif
else
libfuse_sources += [ 'mount_bsd.c' ]
endif
diff --git a/lib/mount.c b/lib/mount.c
index b3ee9ba4c0a74c1d0d55f916e7046e064f8468b0..30fd4d2f9bbb84c817b2363b2075456acd1c1255 100644
--- a/lib/mount.c
+++ b/lib/mount.c
@@ -449,8 +449,8 @@ static int fuse_mount_fusermount(const char *mountpoint, struct mount_opts *mo,
#define O_CLOEXEC 0
#endif
-static int fuse_kern_mount_prepare(const char *mnt,
- struct mount_opts *mo)
+int fuse_kern_mount_prepare(const char *mnt,
+ struct mount_opts *mo)
{
char tmp[128];
const char *devname = fuse_mnt_get_devname();
@@ -500,6 +500,26 @@ out_close:
return -1;
}
+#if defined(HAVE_NEW_MOUNT_API)
+/**
+ * Wrapper for fuse_kern_fsmount that accepts struct mount_opts
+ * @mnt: mountpoint
+ * @mo: mount options
+ * @mnt_opts: mount options to pass to the kernel
+ *
+ * Returns: 0 on success, -1 on failure with errno set
+ */
+int fuse_kern_fsmount_mo(const char *mnt, struct mount_opts *mo,
+ const char *mnt_opts)
+{
+ const char *devname = fuse_mnt_get_devname();
+
+ return fuse_kern_fsmount(mnt, mo->flags, mo->blkdev, mo->fsname,
+ mo->subtype, devname, mo->kernel_opts,
+ mnt_opts);
+}
+#endif
+
/**
* Complete the mount operation with an already-opened fd
* @mnt: mountpoint
@@ -643,8 +663,7 @@ void destroy_mount_opts(struct mount_opts *mo)
free(mo);
}
-static int fuse_kern_mount_get_base_mnt_opts(struct mount_opts *mo,
- char **mnt_optsp)
+int fuse_kern_mount_get_base_mnt_opts(struct mount_opts *mo, char **mnt_optsp)
{
if (get_mnt_flag_opts(mnt_optsp, mo->flags) == -1)
return -1;
diff --git a/lib/mount_fsmount.c b/lib/mount_fsmount.c
new file mode 100644
index 0000000000000000000000000000000000000000..cba998bc60c783a5edc0c16570f7e5512b7f1253
--- /dev/null
+++ b/lib/mount_fsmount.c
@@ -0,0 +1,405 @@
+/*
+ * FUSE: Filesystem in Userspace
+ * Copyright (C) 2001-2007 Miklos Szeredi <miklos@szeredi.hu>
+ * 2026 Bernd Schubert <bernd@bsbernd.com>
+ *
+ * New Linux mount API (fsopen/fsconfig/fsmount/move_mount) support.
+ *
+ * This program can be distributed under the terms of the GNU LGPLv2.
+ * See the file LGPL2.txt.
+ */
+
+#define _GNU_SOURCE
+
+#include "fuse_config.h"
+#include "fuse_misc.h"
+#include "mount_util.h"
+#include "mount_i_linux.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <sys/mount.h>
+#include <sys/syscall.h>
+
+/* Mount attribute flags for fsmount() - from linux/mount.h */
+#ifndef MOUNT_ATTR_RDONLY
+#define MOUNT_ATTR_RDONLY 0x00000001
+#endif
+#ifndef MOUNT_ATTR_NOSUID
+#define MOUNT_ATTR_NOSUID 0x00000002
+#endif
+#ifndef MOUNT_ATTR_NODEV
+#define MOUNT_ATTR_NODEV 0x00000004
+#endif
+#ifndef MOUNT_ATTR_NOEXEC
+#define MOUNT_ATTR_NOEXEC 0x00000008
+#endif
+#ifndef MOUNT_ATTR__ATIME
+#define MOUNT_ATTR__ATIME 0x00000070
+#endif
+#ifndef MOUNT_ATTR_RELATIME
+#define MOUNT_ATTR_RELATIME 0x00000000
+#endif
+#ifndef MOUNT_ATTR_NOATIME
+#define MOUNT_ATTR_NOATIME 0x00000010
+#endif
+#ifndef MOUNT_ATTR_STRICTATIME
+#define MOUNT_ATTR_STRICTATIME 0x00000020
+#endif
+#ifndef MOUNT_ATTR_NODIRATIME
+#define MOUNT_ATTR_NODIRATIME 0x00000080
+#endif
+#ifndef MOUNT_ATTR_NOSYMFOLLOW
+#define MOUNT_ATTR_NOSYMFOLLOW 0x00200000
+#endif
+
+/*
+ * Convert MS_* mount flags to MOUNT_ATTR_* mount attributes.
+ * These flags are passed to fsmount(), not fsconfig().
+ * Mount attributes control mount-point level behavior.
+ */
+static unsigned long ms_flags_to_mount_attrs(unsigned long flags)
+{
+ unsigned long attrs = 0;
+
+ if (flags & MS_NOSUID)
+ attrs |= MOUNT_ATTR_NOSUID;
+ if (flags & MS_NODEV)
+ attrs |= MOUNT_ATTR_NODEV;
+ if (flags & MS_NOEXEC)
+ attrs |= MOUNT_ATTR_NOEXEC;
+ if (flags & MS_NOATIME)
+ attrs |= MOUNT_ATTR_NOATIME;
+ else if (flags & MS_RELATIME)
+ attrs |= MOUNT_ATTR_RELATIME;
+ else if (flags & MS_STRICTATIME)
+ attrs |= MOUNT_ATTR_STRICTATIME;
+ if (flags & MS_NODIRATIME)
+ attrs |= MOUNT_ATTR_NODIRATIME;
+ if (flags & MS_NOSYMFOLLOW)
+ attrs |= MOUNT_ATTR_NOSYMFOLLOW;
+
+ return attrs;
+}
+
+/*
+ * Apply VFS superblock flags to the filesystem context.
+ * Only handles flags that are filesystem parameters (ro, sync, dirsync).
+ * Mount attributes (nosuid, nodev, etc.) are handled separately via fsmount().
+ */
+static int apply_mount_flags(int fsfd, unsigned long flags)
+{
+ int res;
+
+ /* Handle read-only flag */
+ if (flags & MS_RDONLY) {
+ res = fsconfig(fsfd, FSCONFIG_SET_FLAG, "ro", NULL, 0);
+ if (res == -1) {
+ fprintf(stderr,
+ "fuse: fsconfig SET_FLAG ro failed: %s\n",
+ strerror(errno));
+ return -errno;
+ }
+ }
+
+ /* Handle sync flag */
+ if (flags & MS_SYNCHRONOUS) {
+ res = fsconfig(fsfd, FSCONFIG_SET_FLAG, "sync", NULL, 0);
+ if (res == -1) {
+ fprintf(stderr,
+ "fuse: fsconfig SET_FLAG sync failed: %s\n",
+ strerror(errno));
+ return -errno;
+ }
+ }
+
+#ifndef __NetBSD__
+ /* Handle dirsync flag */
+ if (flags & MS_DIRSYNC) {
+ res = fsconfig(fsfd, FSCONFIG_SET_FLAG, "dirsync", NULL, 0);
+ if (res == -1) {
+ fprintf(stderr,
+ "fuse: fsconfig SET_FLAG dirsync failed: %s\n",
+ strerror(errno));
+ return -errno;
+ }
+ }
+#endif
+
+ return 0;
+}
+
+static int apply_opt_fd(int fsfd, const char *value)
+{
+ int res;
+
+ /* The fd parameter is a u32 value, not a file descriptor to pass */
+ res = fsconfig(fsfd, FSCONFIG_SET_STRING, "fd", value, 0);
+ if (res == -1) {
+ fprintf(stderr, "fuse: fsconfig SET_STRING fd=%s failed: %s\n",
+ value, strerror(errno));
+ return -errno;
+ }
+ return 0;
+}
+
+static int apply_opt_string(int fsfd, const char *key, const char *value)
+{
+ int res;
+
+ res = fsconfig(fsfd, FSCONFIG_SET_STRING, key, value, 0);
+ if (res == -1) {
+ fprintf(stderr,
+ "fuse: fsconfig SET_STRING %s=%s failed: %s\n",
+ key, value, strerror(errno));
+ return -errno;
+ }
+ return 0;
+}
+
+static int apply_opt_flag(int fsfd, const char *opt)
+{
+ int res;
+
+ res = fsconfig(fsfd, FSCONFIG_SET_FLAG, opt, NULL, 0);
+ if (res == -1) {
+ fprintf(stderr, "fuse: fsconfig SET_FLAG %s failed: %s\n",
+ opt, strerror(errno));
+ return -errno;
+ }
+ return 0;
+}
+
+static int apply_opt_key_value(int fsfd, char *opt)
+{
+ char *eq;
+ const char *key;
+ const char *value;
+
+ eq = strchr(opt, '=');
+ if (!eq)
+ return apply_opt_flag(fsfd, opt);
+
+ *eq = '\0';
+ key = opt;
+ value = eq + 1;
+
+ if (strcmp(key, "fd") == 0)
+ return apply_opt_fd(fsfd, value);
+
+ return apply_opt_string(fsfd, key, value);
+}
+
+/**
+ * Check if an option is a mount attribute (handled by fsmount, not fsconfig)
+ */
+static int is_mount_attr_opt(const char *opt)
+{
+ /* These options are mount attributes passed to fsmount(), not fsconfig() */
+ return strcmp(opt, "nosuid") == 0 ||
+ strcmp(opt, "suid") == 0 ||
+ strcmp(opt, "nodev") == 0 ||
+ strcmp(opt, "dev") == 0 ||
+ strcmp(opt, "noexec") == 0 ||
+ strcmp(opt, "exec") == 0 ||
+ strcmp(opt, "noatime") == 0 ||
+ strcmp(opt, "atime") == 0 ||
+ strcmp(opt, "nodiratime") == 0 ||
+ strcmp(opt, "diratime") == 0 ||
+ strcmp(opt, "relatime") == 0 ||
+ strcmp(opt, "norelatime") == 0 ||
+ strcmp(opt, "strictatime") == 0 ||
+ strcmp(opt, "nostrictatime") == 0 ||
+ strcmp(opt, "nosymfollow") == 0 ||
+ strcmp(opt, "symfollow") == 0;
+}
+
+/*
+ * Parse kernel options string and apply via fsconfig
+ * Options are comma-separated key=value pairs
+ */
+static int apply_mount_opts(int fsfd, const char *opts)
+{
+ char *opts_copy;
+ char *opt;
+ char *saveptr;
+ int res;
+
+ if (!opts || !*opts)
+ return 0;
+
+ opts_copy = strdup(opts);
+ if (!opts_copy) {
+ fprintf(stderr, "fuse: failed to allocate memory\n");
+ return -ENOMEM;
+ }
+
+ opt = strtok_r(opts_copy, ",", &saveptr);
+ while (opt) {
+ /* Skip mount attributes - they're handled by fsmount(), not fsconfig() */
+ if (!is_mount_attr_opt(opt)) {
+ res = apply_opt_key_value(fsfd, opt);
+ if (res < 0) {
+ free(opts_copy);
+ return res;
+ }
+ }
+ opt = strtok_r(NULL, ",", &saveptr);
+ }
+
+ free(opts_copy);
+ return 0;
+}
+
+
+/**
+ * Mount using the new Linux mount API (fsopen/fsconfig/fsmount/move_mount)
+ * @mnt: mountpoint
+ * @flags: mount flags (MS_NOSUID, MS_NODEV, etc.)
+ * @blkdev: 1 for fuseblk, 0 for fuse
+ * @fsname: filesystem name (or NULL)
+ * @subtype: filesystem subtype (or NULL)
+ * @source_dev: device name for building source string
+ * @kernel_opts: kernel mount options string
+ * @mnt_opts: additional mount options to pass to the kernel
+ *
+ * Returns: 0 on success, -1 on failure with errno set
+ */
+int fuse_kern_fsmount(const char *mnt, unsigned long flags, int blkdev,
+ const char *fsname, const char *subtype,
+ const char *source_dev, const char *kernel_opts,
+ const char *mnt_opts)
+{
+ const char *type;
+ char *source = NULL;
+ int fsfd = -1;
+ int mntfd = -1;
+ int err, res;
+ unsigned long mount_attrs;
+
+ /* Determine filesystem type */
+ type = blkdev ? "fuseblk" : "fuse";
+
+ /* Try to open filesystem context */
+ fsfd = fsopen(type, FSOPEN_CLOEXEC);
+ if (fsfd == -1) {
+ fprintf(stderr, "fuse: fsopen(%s) failed: %s\n", type,
+ strerror(errno));
+ return -1;
+ }
+
+ /* Build source string */
+ source = malloc((fsname ? strlen(fsname) : 0) +
+ (subtype ? strlen(subtype) : 0) +
+ strlen(source_dev) + 32);
+ err = -ENOMEM;
+ if (!source) {
+ fprintf(stderr, "fuse: failed to allocate memory\n");
+ goto out_close_fsfd;
+ }
+
+ strcpy(source, fsname ? fsname : (subtype ? subtype : source_dev));
+
+ /* Configure source */
+ res = fsconfig(fsfd, FSCONFIG_SET_STRING, "source", source, 0);
+ if (res == -1) {
+ err = -errno;
+ fprintf(stderr, "fuse: fsconfig source failed: %s\n",
+ strerror(errno));
+ goto out_free;
+ }
+
+ /* Apply VFS superblock flags (ro, sync, dirsync) */
+ err = apply_mount_flags(fsfd, flags);
+ if (err < 0) {
+ fprintf(stderr, "fuse: failed to apply mount flags\n");
+ goto out_free;
+ }
+
+ /* Apply kernel options */
+ err = apply_mount_opts(fsfd, kernel_opts);
+ if (err < 0) {
+ fprintf(stderr,
+ "fuse: failed to apply kernel options '%s'\n",
+ kernel_opts);
+ goto out_free;
+ }
+
+ /* Apply additional mount options */
+ err = apply_mount_opts(fsfd, mnt_opts);
+ if (err < 0) {
+ fprintf(stderr,
+ "fuse: failed to apply additional mount options '%s'\n",
+ mnt_opts);
+ goto out_free;
+ }
+
+ /* Create the filesystem instance */
+ res = fsconfig(fsfd, FSCONFIG_CMD_CREATE, NULL, NULL, 0);
+ if (res == -1) {
+ err = -errno;
+ fprintf(stderr, "fuse: fsconfig CREATE failed: %s\n",
+ strerror(errno));
+ goto out_free;
+ }
+
+ /* Convert MS_* flags to MOUNT_ATTR_* for fsmount() */
+ mount_attrs = ms_flags_to_mount_attrs(flags);
+
+ /* Create mount object with mount attributes */
+ mntfd = fsmount(fsfd, FSMOUNT_CLOEXEC, mount_attrs);
+ if (mntfd == -1) {
+ err = -errno;
+ fprintf(stderr, "fuse: fsmount failed: %s\n",
+ strerror(errno));
+ goto out_free;
+ }
+
+ close(fsfd);
+ fsfd = -1;
+
+ /* Attach to mount point */
+ if (move_mount(mntfd, "", AT_FDCWD, mnt, MOVE_MOUNT_F_EMPTY_PATH) ==
+ -1) {
+ err = -errno;
+ fprintf(stderr, "fuse: move_mount failed: %s\n",
+ strerror(errno));
+ goto out_close_mntfd;
+ }
+
+ err = fuse_mnt_add_mount_helper(mnt, source, type, mnt_opts);
+ if (err == -1)
+ goto out_umount;
+
+ close(mntfd);
+ free(source);
+ return 0;
+
+out_umount:
+ {
+ /* race free umount */
+ char fd_path[64];
+
+ snprintf(fd_path, sizeof(fd_path), "/proc/self/fd/%d", mntfd);
+ if (umount2(fd_path, MNT_DETACH) == -1 && errno != EINVAL) {
+ fprintf(stderr,
+ "fuse: cleanup umount failed: %s\n",
+ strerror(errno));
+ }
+ }
+out_close_mntfd:
+ if (mntfd != -1)
+ close(mntfd);
+out_free:
+ free(source);
+out_close_fsfd:
+ if (fsfd != -1)
+ close(fsfd);
+ errno = -err;
+ return -1;
+}
+
diff --git a/lib/mount_i_linux.h b/lib/mount_i_linux.h
index c0de6228fce5a4d9070cc246ec76222b66de56fb..52eab9e650c055142feec329264f82c2b08be0d5 100644
--- a/lib/mount_i_linux.h
+++ b/lib/mount_i_linux.h
@@ -60,6 +60,20 @@ static const struct mount_flags fuse_mount_flags[] = {
{NULL, 0, 0}
};
+int fuse_kern_mount_prepare(const char *mnt, struct mount_opts *mo);
+
+int fuse_kern_mount_get_base_mnt_opts(struct mount_opts *mo, char **mnt_optsp);
+
+int fuse_kern_fsmount(const char *mnt, unsigned long flags, int blkdev,
+ const char *fsname, const char *subtype,
+ const char *source_dev, const char *kernel_opts,
+ const char *mnt_opts);
+
+int fuse_kern_fsmount_mo(const char *mnt, struct mount_opts *mo,
+ const char *mnt_opts);
+
+int fuse_kern_fsmount_mo(const char *mnt, struct mount_opts *mo,
+ const char *mnt_opts);
#endif /* FUSE_MOUNT_I_H_ */
diff --git a/meson.build b/meson.build
index 80c5f1dc0bd3565c11ad3084dac08e28dab611dc..465a7bbfd14fe9c45897f197ef37db76079d20ee 100644
--- a/meson.build
+++ b/meson.build
@@ -30,7 +30,7 @@ if platform == 'darwin'
'https://www.fuse-t.org/ instead')
elif platform == 'cygwin' or platform == 'windows'
error('libfuse does not support Windows.\n' +
- 'Take a look at http://www.secfs.net/winfsp/ instead')
+ 'Take a look at http://www.secfs.net/winfsp/ instead')
endif
cc = meson.get_compiler('c')
@@ -118,6 +118,21 @@ special_funcs = {
return -1;
}
}
+ ''',
+ 'new_mount_api': '''
+ #define _GNU_SOURCE
+ #include <sys/mount.h>
+ #include <linux/mount.h>
+ #include <unistd.h>
+ #include <fcntl.h>
+
+ int main(void) {
+ int fsfd = fsopen("fuse", FSOPEN_CLOEXEC);
+ int res = fsconfig(fsfd, FSCONFIG_SET_STRING, "source", "test", 0);
+ int mntfd = fsmount(fsfd, FSMOUNT_CLOEXEC, 0);
+ res = move_mount(mntfd, "", AT_FDCWD, "/mnt", MOVE_MOUNT_F_EMPTY_PATH);
+ return 0;
+ }
'''
}
@@ -308,7 +323,7 @@ configure_file(output: 'libfuse_config.h',
include_dirs = include_directories('include', 'lib', '.')
# Common dependencies
-thread_dep = dependency('threads')
+thread_dep = dependency('threads')
#
# Read build files from sub-directories
--
2.43.0
^ permalink raw reply related [flat|nested] 59+ messages in thread
* [PATCH 12/19] fuse mount: Support synchronous FUSE_INIT (privileged daemon)
2026-03-23 17:44 [PATCH 00/19] libfuse: Add support for synchronous init Bernd Schubert
` (10 preceding siblings ...)
2026-03-23 17:45 ` [PATCH 11/19] Add support for the new linux mount API Bernd Schubert
@ 2026-03-23 17:45 ` Bernd Schubert
2026-03-24 0:03 ` Darrick J. Wong
2026-03-23 17:45 ` [PATCH 13/19] Add fuse_session_set_debug() to enable debug output without foreground Bernd Schubert
` (7 subsequent siblings)
19 siblings, 1 reply; 59+ messages in thread
From: Bernd Schubert @ 2026-03-23 17:45 UTC (permalink / raw)
To: linux-fsdevel
Cc: Miklos Szeredi, Joanne Koong, Darrick J. Wong, Bernd Schubert,
Bernd Schubert
From: Bernd Schubert <bschubert@ddn.com>
Add synchronous FUSE_INIT processing during mount() to
enable early daemonization with proper error reporting
to the parent process.
A new mount thread is needed that handles FUSE_INIT and
possible other requests at mount time (like getxattr for selinux).
The kernel sends FUSE_INIT during the mount() syscall. Without a thread
to process it, mount() blocks forever.
Mount thread lifetime:
Created before mount() syscall in fuse_start_sync_init_worker()
Processes requests until se->mount_finished is set (after mount() returns)
Joined after successful mount in fuse_wait_sync_init_completion()
Cancelled if mount fails (direct → fusermount3 fallback)
Key changes:
Add init_thread, init_error, mount_finished to struct fuse_session
Use FUSE_DEV_IOC_SYNC_INIT ioctl for kernel support
Fall back to async FUSE_INIT if unsupported
Auto-enabled when fuse_daemonize_active() or via
fuse_session_want_sync_init()
Allows parent to report mount/init failures instead of
exiting immediately after fork.
Note: For now synchronous FUSE_INIT is only supported for privileged
mounts.
Signed-off-by: Bernd Schubert <bschubert@ddn.com>
---
include/fuse_lowlevel.h | 12 +++
lib/fuse_daemonize.c | 8 ++
lib/fuse_i.h | 16 ++++
lib/fuse_lowlevel.c | 192 ++++++++++++++++++++++++++++++++++++++++++++++--
lib/mount.c | 5 +-
lib/mount_fsmount.c | 5 +-
6 files changed, 229 insertions(+), 9 deletions(-)
diff --git a/include/fuse_lowlevel.h b/include/fuse_lowlevel.h
index ee0bd8d71d95e4d57ebb4873dca0f2b36e22a649..d8626f85bdaf497534cd2835a589e30f1f4e2466 100644
--- a/include/fuse_lowlevel.h
+++ b/include/fuse_lowlevel.h
@@ -2429,6 +2429,18 @@ void fuse_session_process_buf(struct fuse_session *se,
*/
int fuse_session_receive_buf(struct fuse_session *se, struct fuse_buf *buf);
+/**
+ * Request synchronous FUSE_INIT, i.e. FUSE_INIT is handled by the
+ * kernel before mount is returned.
+ *
+ * As FUSE_INIT also starts io-uring ring threads, fork() must not be
+ * called after this if io-uring is enabled. Also see
+ * fuse_session_daemonize_start().
+ *
+ * This must be called before fuse_session_mount() to have any effect.
+ */
+void fuse_session_want_sync_init(struct fuse_session *se);
+
/**
* Check if the request is submitted through fuse-io-uring
*/
diff --git a/lib/fuse_daemonize.c b/lib/fuse_daemonize.c
index 5d191e7d737d04876d02ef6bd526061c003d2ab7..1a32ef74093f231091b4f541e5b9136bff72024f 100644
--- a/lib/fuse_daemonize.c
+++ b/lib/fuse_daemonize.c
@@ -9,6 +9,7 @@
#define _GNU_SOURCE
#include "fuse_daemonize.h"
+#include "fuse_i.h"
#include <fcntl.h>
#include <poll.h>
@@ -282,3 +283,10 @@ bool fuse_daemonize_active(void)
return dm != NULL && (dm->daemonized || dm->active);
}
+
+bool fuse_daemonize_set(void)
+{
+ struct fuse_daemonize *dm = atomic_load(&daemonize);
+
+ return dm != NULL;
+}
diff --git a/lib/fuse_i.h b/lib/fuse_i.h
index 6d63c9fd2149eb4ae3b0e0170640a4ce2eed4705..93ab7ac2fadf9395af70487c7626cc57c2948d56 100644
--- a/lib/fuse_i.h
+++ b/lib/fuse_i.h
@@ -112,6 +112,10 @@ struct fuse_session {
/* synchronous FUSE_INIT support */
bool want_sync_init;
+ pthread_t init_thread;
+ int init_error;
+ _Atomic bool terminate_mount_worker;
+ int init_wakeup_fd;
/* io_uring */
struct fuse_session_uring uring;
@@ -221,7 +225,11 @@ void fuse_chan_put(struct fuse_chan *ch);
/* Mount-related functions */
void fuse_mount_version(void);
void fuse_kern_unmount(const char *mountpoint, int fd);
+int fuse_kern_mount_get_base_mnt_opts(struct mount_opts *mo, char **mnt_optsp);
int fuse_kern_mount(const char *mountpoint, struct mount_opts *mo);
+int fuse_kern_mount_prepare(const char *mountpoint, struct mount_opts *mo);
+int fuse_kern_do_mount(const char *mountpoint, struct mount_opts *mo,
+ const char *mnt_opts);
int fuse_send_reply_iov_nofree(fuse_req_t req, int error, struct iovec *iov,
int count);
@@ -255,6 +263,14 @@ int fuse_session_loop_mt_312(struct fuse_session *se, struct fuse_loop_config *c
*/
int fuse_loop_cfg_verify(struct fuse_loop_config *config);
+/**
+ * Check if daemonization is set.
+ *
+ * @return true if set, false otherwise
+ */
+bool fuse_daemonize_set(void);
+
+
/*
* This can be changed dynamically on recent kernels through the
diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c
index 626233df20f49fa89cd9327f94340169d7061f75..b10def03f3666757d312f87f177a560483691d6f 100644
--- a/lib/fuse_lowlevel.c
+++ b/lib/fuse_lowlevel.c
@@ -4230,6 +4230,7 @@ fuse_session_new_versioned(struct fuse_args *args,
goto out1;
}
se->fd = -1;
+ se->init_wakeup_fd = -1;
se->conn.max_write = FUSE_DEFAULT_MAX_PAGES_LIMIT * getpagesize();
se->bufsize = se->conn.max_write + FUSE_BUFFER_HEADER_SIZE;
se->conn.max_readahead = UINT_MAX;
@@ -4402,6 +4403,167 @@ int fuse_session_custom_io_30(struct fuse_session *se,
}
#if defined(HAVE_NEW_MOUNT_API)
+
+/* Worker thread for synchronous FUSE_INIT */
+static void *session_sync_init_worker(void *data)
+{
+ struct fuse_session *se = (struct fuse_session *)data;
+ struct fuse_buf fbuf = {
+ .mem = NULL,
+ };
+ struct pollfd pfds[2];
+ int res;
+
+ pfds[0].fd = se->fd;
+ pfds[0].events = POLLIN;
+ pfds[0].revents = 0;
+ pfds[1].fd = se->init_wakeup_fd;
+ pfds[1].events = POLLIN;
+ pfds[1].revents = 0;
+
+ /*
+ * Process requests until mount completes. With SELinux there may be
+ * additional requests (like getattr) after FUSE_INIT before mount
+ * returns.
+ */
+ while (!atomic_load(&se->terminate_mount_worker)) {
+ res = poll(pfds, 2, -1);
+ if (res == -1) {
+ if (errno == EINTR)
+ continue;
+ se->init_error = -errno;
+ break;
+ }
+
+ if (pfds[1].revents & POLLIN)
+ break;
+
+ if (pfds[0].revents & POLLIN) {
+ res = fuse_session_receive_buf_internal(se, &fbuf, NULL);
+ if (res == -EINTR)
+ continue;
+ if (res <= 0) {
+ se->init_error = res < 0 ? res : -EINVAL;
+ break;
+ }
+
+ fuse_session_process_buf_internal(se, &fbuf, NULL);
+ }
+ }
+
+ fuse_buf_free(&fbuf);
+ return NULL;
+}
+
+/* Enable synchronous FUSE_INIT and start worker thread */
+static int session_start_sync_init(struct fuse_session *se, int fd)
+{
+ int err;
+ int res;
+
+ if (!se->want_sync_init &&
+ (se->uring.enable && !fuse_daemonize_set())) {
+ if (se->debug)
+ fuse_log(FUSE_LOG_DEBUG,
+ "fuse: sync init not enabled\n");
+ return 0;
+ }
+
+ /* Try to enable synchronous FUSE_INIT */
+ res = ioctl(fd, FUSE_DEV_IOC_SYNC_INIT);
+ if (res) {
+ /* ENOTTY means kernel doesn't support sync init - not an error */
+ if (errno != ENOTTY) {
+ fuse_log(
+ FUSE_LOG_ERR,
+ "fuse: failed to enable sync init: %s\n",
+ strerror(errno));
+ } else if (se->debug) {
+ fuse_log(
+ FUSE_LOG_DEBUG,
+ "fuse: kernel doesn't support sync init\n");
+ }
+ return -ENOTTY;
+ }
+
+ if (se->debug)
+ fuse_log(FUSE_LOG_DEBUG,
+ "fuse: synchronous FUSE_INIT enabled\n");
+
+ se->init_error = 0;
+ se->terminate_mount_worker = false;
+
+ se->init_wakeup_fd = eventfd(0, EFD_CLOEXEC);
+ if (se->init_wakeup_fd == -1) {
+ fuse_log(
+ FUSE_LOG_ERR,
+ "fuse: failed to create eventfd for init worker: %s\n",
+ strerror(errno));
+ return -EIO;
+ }
+
+ err = pthread_create(&se->init_thread, NULL,
+ session_sync_init_worker, se);
+ if (err != 0) {
+ fuse_log(
+ FUSE_LOG_ERR,
+ "fuse: failed to create init worker thread: %s\n",
+ strerror(err));
+ close(se->init_wakeup_fd);
+ se->init_wakeup_fd = -1;
+ return -EIO;
+ }
+
+ return 0;
+}
+
+/* Wait for synchronous FUSE_INIT to complete */
+static int session_wait_sync_init_completion(struct fuse_session *se)
+{
+ void *retval;
+ int err;
+ uint64_t val = 1;
+
+ if (se->init_thread == 0)
+ return 0;
+
+ se->terminate_mount_worker = true;
+
+ if (se->init_wakeup_fd != -1) {
+ ssize_t res = write(se->init_wakeup_fd, &val, sizeof(val));
+
+ if (res != sizeof(val)) {
+ fuse_log(FUSE_LOG_ERR,
+ "fuse: failed to signal init worker: %s\n",
+ strerror(errno));
+ }
+ }
+
+ err = pthread_join(se->init_thread, &retval);
+ if (err != 0) {
+ fuse_log(FUSE_LOG_ERR, "fuse: failed to join init worker thread: %s\n",
+ strerror(err));
+ return -1;
+ }
+
+ if (se->init_wakeup_fd != -1) {
+ close(se->init_wakeup_fd);
+ se->init_wakeup_fd = -1;
+ }
+
+ if (se->init_error != 0) {
+ fuse_log(FUSE_LOG_ERR, "fuse: init worker failed: %d\n", se->init_error);
+ return -1;
+ }
+
+ if (fuse_session_exited(se)) {
+ fuse_log(FUSE_LOG_ERR, "FUSE_INIT failed: session exited\n");
+ return -1;
+ }
+
+ return 0;
+}
+
/* Only linux supports sync FUSE_INIT so far */
static int fuse_session_mount_new_api(struct fuse_session *se,
const char *mountpoint)
@@ -4414,8 +4576,7 @@ static int fuse_session_mount_new_api(struct fuse_session *se,
res = fuse_kern_mount_get_base_mnt_opts(se->mo, &mnt_opts);
if (res == -1) {
- fuse_log(FUSE_LOG_ERR,
- "fuse: failed to get base mount options\n");
+ fuse_log(FUSE_LOG_ERR, "fuse: failed to get base mount options\n");
err = -EIO;
goto err;
}
@@ -4427,6 +4588,17 @@ static int fuse_session_mount_new_api(struct fuse_session *se,
goto err;
}
+ /*
+ * Enable synchronous FUSE_INIT and start worker thread, sync init
+ * failure is not an error
+ */
+ se->fd = fd;
+ err = session_start_sync_init(se, fd);
+ if (err) {
+ /* ENOTTY means kernel doesn't support sync init - not an error */
+ if (err != -ENOTTY)
+ goto err;
+ }
snprintf(fd_opt, sizeof(fd_opt), "fd=%i", fd);
if (fuse_opt_add_opt(&mnt_opts_with_fd, mnt_opts) == -1 ||
fuse_opt_add_opt(&mnt_opts_with_fd, fd_opt) == -1) {
@@ -4436,13 +4608,16 @@ static int fuse_session_mount_new_api(struct fuse_session *se,
err = fuse_kern_fsmount_mo(mountpoint, se->mo, mnt_opts_with_fd);
err:
- if (err) {
+ if (err < 0) {
if (fd >= 0)
close(fd);
fd = -1;
se->fd = -1;
se->error = -errno;
}
+ /* Wait for synchronous FUSE_INIT to complete */
+ if (session_wait_sync_init_completion(se) < 0)
+ fuse_log(FUSE_LOG_ERR, "fuse: sync init completion failed\n");
free(mnt_opts);
free(mnt_opts_with_fd);
@@ -4452,8 +4627,8 @@ err:
static int fuse_session_mount_new_api(struct fuse_session *se,
const char *mountpoint)
{
- (void)se;
- (void)mountpoint;
+ (void) se;
+ (void) mountpoint;
return -1;
}
@@ -4825,3 +5000,10 @@ void fuse_session_stop_teardown_watchdog(void *data)
pthread_join(tt->thread_id, NULL);
fuse_tt_destruct(tt);
}
+
+void fuse_session_want_sync_init(struct fuse_session *se)
+{
+ if (se == NULL)
+ return;
+ se->want_sync_init = true;
+}
diff --git a/lib/mount.c b/lib/mount.c
index 30fd4d2f9bbb84c817b2363b2075456acd1c1255..12df49d9109cf918cc41aa75c5fdf84231d4d5ff 100644
--- a/lib/mount.c
+++ b/lib/mount.c
@@ -30,6 +30,7 @@
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/wait.h>
+#include <sys/ioctl.h>
#include "fuse_mount_compat.h"
@@ -529,8 +530,8 @@ int fuse_kern_fsmount_mo(const char *mnt, struct mount_opts *mo,
* Returns: 0 on success, -1 on failure,
* FUSE_MOUNT_FALLBACK_NEEDED if fusermount should be used
*/
-static int fuse_kern_do_mount(const char *mnt, struct mount_opts *mo,
- const char *mnt_opts)
+int fuse_kern_do_mount(const char *mnt, struct mount_opts *mo,
+ const char *mnt_opts)
{
char *source = NULL;
char *type = NULL;
diff --git a/lib/mount_fsmount.c b/lib/mount_fsmount.c
index cba998bc60c783a5edc0c16570f7e5512b7f1253..f1fec790bb80f8815d485a068dc7efdff1746309 100644
--- a/lib/mount_fsmount.c
+++ b/lib/mount_fsmount.c
@@ -287,8 +287,9 @@ int fuse_kern_fsmount(const char *mnt, unsigned long flags, int blkdev,
/* Try to open filesystem context */
fsfd = fsopen(type, FSOPEN_CLOEXEC);
if (fsfd == -1) {
- fprintf(stderr, "fuse: fsopen(%s) failed: %s\n", type,
- strerror(errno));
+ if (errno != EPERM)
+ fprintf(stderr, "fuse: fsopen(%s) failed: %s\n", type,
+ strerror(errno));
return -1;
}
--
2.43.0
^ permalink raw reply related [flat|nested] 59+ messages in thread
* [PATCH 13/19] Add fuse_session_set_debug() to enable debug output without foreground
2026-03-23 17:44 [PATCH 00/19] libfuse: Add support for synchronous init Bernd Schubert
` (11 preceding siblings ...)
2026-03-23 17:45 ` [PATCH 12/19] fuse mount: Support synchronous FUSE_INIT (privileged daemon) Bernd Schubert
@ 2026-03-23 17:45 ` Bernd Schubert
2026-03-24 0:04 ` Darrick J. Wong
2026-03-23 17:45 ` [PATCH 14/19] Move more generic mount code to mount_util.{c,h} Bernd Schubert
` (6 subsequent siblings)
19 siblings, 1 reply; 59+ messages in thread
From: Bernd Schubert @ 2026-03-23 17:45 UTC (permalink / raw)
To: linux-fsdevel
Cc: Miklos Szeredi, Joanne Koong, Darrick J. Wong, Bernd Schubert,
Bernd Schubert
From: Bernd Schubert <bschubert@ddn.com>
One might want to print debug out in background mode, which the command
line parameter does not easily allow. Or one might want to enable and
disable at run time.
Signed-off-by: Bernd Schubert <bschubert@ddn.com>
---
include/fuse_lowlevel.h | 12 ++++++++++++
lib/fuse_lowlevel.c | 5 +++++
lib/fuse_versionscript | 1 +
3 files changed, 18 insertions(+)
diff --git a/include/fuse_lowlevel.h b/include/fuse_lowlevel.h
index d8626f85bdaf497534cd2835a589e30f1f4e2466..d85929e291a77de8caad7d6b3d9ac5b092ce0e62 100644
--- a/include/fuse_lowlevel.h
+++ b/include/fuse_lowlevel.h
@@ -2441,6 +2441,18 @@ int fuse_session_receive_buf(struct fuse_session *se, struct fuse_buf *buf);
*/
void fuse_session_want_sync_init(struct fuse_session *se);
+/**
+ * Enable debug output
+ *
+ * This allows to enable debug output without a command line parameter and
+ * without the enforcement of the command line parameter to run in foreground.
+ * The daemon needs to handle either fuse_log output via stderr, or
+ * redirection to its own logs or via syslog.
+ *
+ * @param se the session
+ */
+void fuse_session_set_debug(struct fuse_session *se);
+
/**
* Check if the request is submitted through fuse-io-uring
*/
diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c
index b10def03f3666757d312f87f177a560483691d6f..a7293a3898c37c3877eadf965d310ae2aa5cc2d1 100644
--- a/lib/fuse_lowlevel.c
+++ b/lib/fuse_lowlevel.c
@@ -5007,3 +5007,8 @@ void fuse_session_want_sync_init(struct fuse_session *se)
return;
se->want_sync_init = true;
}
+
+void fuse_session_set_debug(struct fuse_session *se)
+{
+ se->debug = 1;
+}
diff --git a/lib/fuse_versionscript b/lib/fuse_versionscript
index f1765d39e13bc9b1f53e625b9a091c5fa53f5afd..64bf75ac2cb252d066ac301be8fc024b59f903ac 100644
--- a/lib/fuse_versionscript
+++ b/lib/fuse_versionscript
@@ -230,6 +230,7 @@ FUSE_3.19 {
fuse_daemonize_start;
fuse_daemonize_signal;
fuse_daemonize_active;
+ fuse_session_set_debug;
} FUSE_3.18;
# Local Variables:
--
2.43.0
^ permalink raw reply related [flat|nested] 59+ messages in thread
* [PATCH 14/19] Move more generic mount code to mount_util.{c,h}
2026-03-23 17:44 [PATCH 00/19] libfuse: Add support for synchronous init Bernd Schubert
` (12 preceding siblings ...)
2026-03-23 17:45 ` [PATCH 13/19] Add fuse_session_set_debug() to enable debug output without foreground Bernd Schubert
@ 2026-03-23 17:45 ` Bernd Schubert
2026-03-24 0:06 ` Darrick J. Wong
2026-03-23 17:45 ` [PATCH 15/19] Split the fusermount do_mount function Bernd Schubert
` (5 subsequent siblings)
19 siblings, 1 reply; 59+ messages in thread
From: Bernd Schubert @ 2026-03-23 17:45 UTC (permalink / raw)
To: linux-fsdevel
Cc: Miklos Szeredi, Joanne Koong, Darrick J. Wong, Bernd Schubert,
Bernd Schubert
From: Bernd Schubert <bschubert@ddn.com>
This is to allow fusermount to use the code from mount_fsmount.c.
I.e. avoid code dup and add just re-use the new linux api mount
functions from that file for fusermount.
Signed-off-by: Bernd Schubert <bschubert@ddn.com>
---
lib/mount.c | 7 -------
lib/mount_common_i.h | 2 --
lib/mount_i_linux.h | 3 ++-
lib/mount_util.c | 10 ++++++++++
lib/mount_util.h | 5 +++++
5 files changed, 17 insertions(+), 10 deletions(-)
diff --git a/lib/mount.c b/lib/mount.c
index 12df49d9109cf918cc41aa75c5fdf84231d4d5ff..263b05051c236458b830c40181bce7f494803800 100644
--- a/lib/mount.c
+++ b/lib/mount.c
@@ -720,13 +720,6 @@ out:
return res;
}
-const char *fuse_mnt_get_devname(void)
-{
- const char *devname = getenv(FUSE_KERN_DEVICE_ENV);
-
- return devname ? devname : "/dev/fuse";
-}
-
char *fuse_mnt_build_source(const struct mount_opts *mo)
{
const char *devname = fuse_mnt_get_devname();
diff --git a/lib/mount_common_i.h b/lib/mount_common_i.h
index d27d2aa624ae3806c61a0fe382c2d024080c9bb3..b015e8f98d46980891216e93358aae9f7836d156 100644
--- a/lib/mount_common_i.h
+++ b/lib/mount_common_i.h
@@ -24,8 +24,6 @@ struct mount_opts;
struct mount_opts *parse_mount_opts(struct fuse_args *args);
void destroy_mount_opts(struct mount_opts *mo);
unsigned int get_max_read(struct mount_opts *o);
-char *fuse_mnt_build_source(const struct mount_opts *mo);
-char *fuse_mnt_build_type(const struct mount_opts *mo);
#endif /* FUSE_MOUNT_COMMON_I_H_ */
diff --git a/lib/mount_i_linux.h b/lib/mount_i_linux.h
index 52eab9e650c055142feec329264f82c2b08be0d5..867105019fa57576682091d1a650302f31e450b3 100644
--- a/lib/mount_i_linux.h
+++ b/lib/mount_i_linux.h
@@ -74,6 +74,7 @@ int fuse_kern_fsmount_mo(const char *mnt, struct mount_opts *mo,
int fuse_kern_fsmount_mo(const char *mnt, struct mount_opts *mo,
const char *mnt_opts);
-
+char *fuse_mnt_build_source(const struct mount_opts *mo);
+char *fuse_mnt_build_type(const struct mount_opts *mo);
#endif /* FUSE_MOUNT_I_H_ */
diff --git a/lib/mount_util.c b/lib/mount_util.c
index a42a02a0b92f98778abb2c491cdc7a01260f56ee..6a7908cb74b429a1ffd811678cfd39c35093b265 100644
--- a/lib/mount_util.c
+++ b/lib/mount_util.c
@@ -10,6 +10,9 @@
#include "fuse_config.h"
#include "mount_util.h"
+#ifdef __linux__
+#include "mount_i_linux.h"
+#endif
#include <stdio.h>
#include <unistd.h>
@@ -399,3 +402,10 @@ int fuse_mnt_add_mount_helper(const char *mnt, const char *source,
(void)mnt_opts;
return 0;
}
+
+const char *fuse_mnt_get_devname(void)
+{
+ const char *devname = getenv(FUSE_KERN_DEVICE_ENV);
+
+ return devname ? devname : "/dev/fuse";
+}
diff --git a/lib/mount_util.h b/lib/mount_util.h
index 4688d00091f001b3ccd36b1ef3b7e799031ed773..00397943a46a58e640d15b0c0531e5bc6e76006b 100644
--- a/lib/mount_util.h
+++ b/lib/mount_util.h
@@ -6,6 +6,9 @@
See the file LGPL2.txt.
*/
+#ifndef FUSE_MOUNT_UTIL_H_
+#define FUSE_MOUNT_UTIL_H_
+
#include <sys/types.h>
#include "mount_common_i.h" // IWYU pragma: keep
@@ -22,3 +25,5 @@ int fuse_mnt_parse_fuse_fd(const char *mountpoint);
const char *fuse_mnt_get_devname(void);
int fuse_mnt_add_mount_helper(const char *mnt, const char *source,
const char *type, const char *mnt_opts);
+
+#endif /* FUSE_MOUNT_UTIL_H_ */
--
2.43.0
^ permalink raw reply related [flat|nested] 59+ messages in thread
* [PATCH 15/19] Split the fusermount do_mount function
2026-03-23 17:44 [PATCH 00/19] libfuse: Add support for synchronous init Bernd Schubert
` (13 preceding siblings ...)
2026-03-23 17:45 ` [PATCH 14/19] Move more generic mount code to mount_util.{c,h} Bernd Schubert
@ 2026-03-23 17:45 ` Bernd Schubert
2026-03-24 0:14 ` Darrick J. Wong
2026-03-23 17:45 ` [PATCH 16/19] fusermount: Refactor extract_x_options Bernd Schubert
` (4 subsequent siblings)
19 siblings, 1 reply; 59+ messages in thread
From: Bernd Schubert @ 2026-03-23 17:45 UTC (permalink / raw)
To: linux-fsdevel
Cc: Miklos Szeredi, Joanne Koong, Darrick J. Wong, Bernd Schubert,
Bernd Schubert
From: Bernd Schubert <bschubert@ddn.com>
We will need new API and old API and need to pass the options to
two different functions - factor out the option parsing.
Signed-off-by: Bernd Schubert <bschubert@ddn.com>
---
util/fusermount.c | 298 +++++++++++++++++++++++++++++++++++++-----------------
1 file changed, 205 insertions(+), 93 deletions(-)
diff --git a/util/fusermount.c b/util/fusermount.c
index f17b44f51142c682b339d0ce2287f7c00d644454..ecf509bb80d5cd129f6e582f1ec666502c55603a 100644
--- a/util/fusermount.c
+++ b/util/fusermount.c
@@ -917,30 +917,132 @@ static int mount_notrunc(const char *source, const char *target,
return mount(source, target, filesystemtype, mountflags, data);
}
+struct mount_params {
+ /* Input parameters */
+ int fd; /* /dev/fuse file descriptor */
+ mode_t rootmode; /* Root mode from stat */
+ const char *dev; /* Device path (/dev/fuse) */
-static int do_mount(const char *mnt, const char **typep, mode_t rootmode,
- int fd, const char *opts, const char *dev, char **sourcep,
- char **mnt_optsp)
+ /* Parsed mount options */
+ unsigned long flags; /* Mount flags (MS_NOSUID, etc.) */
+ char *optbuf; /* Kernel mount options buffer */
+ char *fsname; /* Filesystem name from options */
+ char *subtype; /* Subtype from options */
+ int blkdev; /* Block device flag */
+
+ /* Generated mount parameters */
+ char *source; /* Mount source string */
+ char *type; /* Filesystem type string */
+ char *mnt_opts; /* Mount table options */
+
+ /* Pointer for optbuf manipulation */
+ char *optbuf_end; /* Points to end of optbuf for sprintf */
+};
+
+static void free_mount_params(struct mount_params *mp)
+{
+ free(mp->optbuf);
+ free(mp->fsname);
+ free(mp->subtype);
+ free(mp->source);
+ free(mp->type);
+ free(mp->mnt_opts);
+}
+
+/*
+ * Check if option is deprecated large_read.
+ *
+ * Returns true if the option should be skipped (large_read on kernel > 2.4),
+ * false otherwise (all other options or large_read on old kernels).
+ */
+static bool check_large_read(const char *opt, unsigned int len)
+{
+ struct utsname utsname;
+ unsigned int kmaj, kmin;
+ int res;
+
+ if (!opt_eq(opt, len, "large_read"))
+ return false;
+
+ res = uname(&utsname);
+ if (res == 0 &&
+ sscanf(utsname.release, "%u.%u", &kmaj, &kmin) == 2 &&
+ (kmaj > 2 || (kmaj == 2 && kmin > 4))) {
+ fprintf(stderr,
+ "%s: note: 'large_read' mount option is deprecated for %i.%i kernels\n",
+ progname, kmaj, kmin);
+ return true;
+ }
+ return false;
+}
+
+/*
+ * Check if user has permission to use allow_other or allow_root options.
+ *
+ * Returns -1 if permission denied, 0 if allowed or option is not
+ * allow_other/allow_root.
+ */
+static int check_allow_permission(const char *opt, unsigned int len)
+{
+ if (getuid() != 0 && !user_allow_other &&
+ (opt_eq(opt, len, "allow_other") || opt_eq(opt, len, "allow_root"))) {
+ fprintf(stderr, "%s: option %.*s only allowed if 'user_allow_other' is set in %s\n",
+ progname, len, opt, FUSE_CONF);
+ return -1;
+ }
+ return 0;
+}
+
+/*
+ * Process generic mount option.
+ *
+ * Handles mount flags (ro, rw, suid, etc.), kernel options
+ * (default_permissions, allow_other, max_read, blksize), or exits on
+ * unknown options.
+ */
+static int process_generic_option(const char *opt, unsigned int len,
+ unsigned long *flags, char **dest)
+{
+ int on;
+ int flag;
+
+ if (find_mount_flag(opt, len, &on, &flag)) {
+ if (on)
+ *flags |= flag;
+ else
+ *flags &= ~flag;
+ return 0;
+ }
+
+ if (opt_eq(opt, len, "default_permissions") ||
+ opt_eq(opt, len, "allow_other") ||
+ begins_with(opt, "max_read=") ||
+ begins_with(opt, "blksize=")) {
+ memcpy(*dest, opt, len);
+ *dest += len;
+ **dest = ',';
+ (*dest)++;
+ return 0;
+ }
+
+ fprintf(stderr, "%s: unknown option '%.*s'\n", progname, len, opt);
+ exit(1);
+}
+
+static int prepare_mount(const char *opts, struct mount_params *mp)
{
int res;
- int flags = MS_NOSUID | MS_NODEV;
- char *optbuf;
- char *mnt_opts = NULL;
const char *s;
char *d;
- char *fsname = NULL;
- char *subtype = NULL;
- char *source = NULL;
- char *type = NULL;
- int blkdev = 0;
- optbuf = (char *) malloc(strlen(opts) + 128);
- if (!optbuf) {
+ mp->flags = MS_NOSUID | MS_NODEV;
+ mp->optbuf = (char *) malloc(strlen(opts) + 128);
+ if (!mp->optbuf) {
fprintf(stderr, "%s: failed to allocate memory\n", progname);
return -1;
}
- for (s = opts, d = optbuf; *s;) {
+ for (s = opts, d = mp->optbuf; *s;) {
unsigned len;
const char *fsname_str = "fsname=";
const char *subtype_str = "subtype=";
@@ -953,10 +1055,10 @@ static int do_mount(const char *mnt, const char **typep, mode_t rootmode,
break;
}
if (begins_with(s, fsname_str)) {
- if (!get_string_opt(s, len, fsname_str, &fsname))
+ if (!get_string_opt(s, len, fsname_str, &mp->fsname))
goto err;
} else if (begins_with(s, subtype_str)) {
- if (!get_string_opt(s, len, subtype_str, &subtype))
+ if (!get_string_opt(s, len, subtype_str, &mp->subtype))
goto err;
} else if (opt_eq(s, len, "blkdev")) {
if (getuid() != 0) {
@@ -965,7 +1067,7 @@ static int do_mount(const char *mnt, const char **typep, mode_t rootmode,
progname);
goto err;
}
- blkdev = 1;
+ mp->blkdev = 1;
} else if (opt_eq(s, len, "auto_unmount")) {
auto_unmount = 1;
} else if (!opt_eq(s, len, "nonempty") &&
@@ -973,122 +1075,132 @@ static int do_mount(const char *mnt, const char **typep, mode_t rootmode,
!begins_with(s, "rootmode=") &&
!begins_with(s, "user_id=") &&
!begins_with(s, "group_id=")) {
- int on;
- int flag;
- int skip_option = 0;
- if (opt_eq(s, len, "large_read")) {
- struct utsname utsname;
- unsigned kmaj, kmin;
- res = uname(&utsname);
- if (res == 0 &&
- sscanf(utsname.release, "%u.%u",
- &kmaj, &kmin) == 2 &&
- (kmaj > 2 || (kmaj == 2 && kmin > 4))) {
- fprintf(stderr, "%s: note: 'large_read' mount option is deprecated for %i.%i kernels\n", progname, kmaj, kmin);
- skip_option = 1;
- }
- }
- if (getuid() != 0 && !user_allow_other &&
- (opt_eq(s, len, "allow_other") ||
- opt_eq(s, len, "allow_root"))) {
- fprintf(stderr, "%s: option %.*s only allowed if 'user_allow_other' is set in %s\n", progname, len, s, FUSE_CONF);
+ bool skip;
+
+ if (check_allow_permission(s, len) == -1)
goto err;
- }
- if (!skip_option) {
- if (find_mount_flag(s, len, &on, &flag)) {
- if (on)
- flags |= flag;
- else
- flags &= ~flag;
- } else if (opt_eq(s, len, "default_permissions") ||
- opt_eq(s, len, "allow_other") ||
- begins_with(s, "max_read=") ||
- begins_with(s, "blksize=")) {
- memcpy(d, s, len);
- d += len;
- *d++ = ',';
- } else {
- fprintf(stderr, "%s: unknown option '%.*s'\n", progname, len, s);
- exit(1);
- }
- }
+
+ skip = check_large_read(s, len);
+
+ /*
+ * Skip deprecated large_read to avoid passing it to
+ * kernel which would reject it as unknown option.
+ */
+ if (!skip)
+ process_generic_option(s, len, &mp->flags, &d);
}
s += len;
if (*s)
s++;
}
*d = '\0';
- res = get_mnt_opts(flags, optbuf, &mnt_opts);
+ res = get_mnt_opts(mp->flags, mp->optbuf, &mp->mnt_opts);
if (res == -1)
goto err;
+ mp->optbuf_end = d;
+
sprintf(d, "fd=%i,rootmode=%o,user_id=%u,group_id=%u",
- fd, rootmode, getuid(), getgid());
+ mp->fd, mp->rootmode, getuid(), getgid());
- source = malloc((fsname ? strlen(fsname) : 0) +
- (subtype ? strlen(subtype) : 0) + strlen(dev) + 32);
+ mp->source = malloc((mp->fsname ? strlen(mp->fsname) : 0) +
+ (mp->subtype ? strlen(mp->subtype) : 0) + strlen(mp->dev) + 32);
- type = malloc((subtype ? strlen(subtype) : 0) + 32);
- if (!type || !source) {
+ mp->type = malloc((mp->subtype ? strlen(mp->subtype) : 0) + 32);
+ if (!mp->type || !mp->source) {
fprintf(stderr, "%s: failed to allocate memory\n", progname);
goto err;
}
- if (subtype)
- sprintf(type, "%s.%s", blkdev ? "fuseblk" : "fuse", subtype);
+ if (mp->subtype)
+ sprintf(mp->type, "%s.%s", mp->blkdev ? "fuseblk" : "fuse", mp->subtype);
else
- strcpy(type, blkdev ? "fuseblk" : "fuse");
+ strcpy(mp->type, mp->blkdev ? "fuseblk" : "fuse");
- if (fsname)
- strcpy(source, fsname);
+ if (mp->fsname)
+ strcpy(mp->source, mp->fsname);
else
- strcpy(source, subtype ? subtype : dev);
+ strcpy(mp->source, mp->subtype ? mp->subtype : mp->dev);
- res = mount_notrunc(source, mnt, type, flags, optbuf);
- if (res == -1 && errno == ENODEV && subtype) {
+ return 0;
+
+err:
+ free_mount_params(mp);
+ return -1;
+}
+
+/*
+ * Perform the actual mount operation using prepared parameters.
+ *
+ * Returns 0 on success, -1 on failure.
+ */
+static int perform_mount(const char *mnt, struct mount_params *mp)
+{
+ int res;
+
+ res = mount_notrunc(mp->source, mnt, mp->type, mp->flags, mp->optbuf);
+ if (res == -1 && errno == ENODEV && mp->subtype) {
/* Probably missing subtype support */
- strcpy(type, blkdev ? "fuseblk" : "fuse");
- if (fsname) {
- if (!blkdev)
- sprintf(source, "%s#%s", subtype, fsname);
+ strcpy(mp->type, mp->blkdev ? "fuseblk" : "fuse");
+ if (mp->fsname) {
+ if (!mp->blkdev)
+ sprintf(mp->source, "%s#%s", mp->subtype, mp->fsname);
} else {
- strcpy(source, type);
+ strcpy(mp->source, mp->type);
}
- res = mount_notrunc(source, mnt, type, flags, optbuf);
+ res = mount_notrunc(mp->source, mnt, mp->type, mp->flags, mp->optbuf);
}
if (res == -1 && errno == EINVAL) {
/* It could be an old version not supporting group_id */
- sprintf(d, "fd=%i,rootmode=%o,user_id=%u",
- fd, rootmode, getuid());
- res = mount_notrunc(source, mnt, type, flags, optbuf);
+ sprintf(mp->optbuf_end, "fd=%i,rootmode=%o,user_id=%u",
+ mp->fd, mp->rootmode, getuid());
+ res = mount_notrunc(mp->source, mnt, mp->type, mp->flags, mp->optbuf);
}
if (res == -1) {
int errno_save = errno;
- if (blkdev && errno == ENODEV && !fuse_mnt_check_fuseblk())
+ if (mp->blkdev && errno == ENODEV && !fuse_mnt_check_fuseblk())
fprintf(stderr, "%s: 'fuseblk' support missing\n",
progname);
else
fprintf(stderr, "%s: mount failed: %s\n", progname,
strerror(errno_save));
- goto err;
+ return -1;
}
- *sourcep = source;
- *typep = type;
- *mnt_optsp = mnt_opts;
- free(fsname);
- free(optbuf);
return 0;
+}
-err:
- free(fsname);
- free(subtype);
- free(source);
- free(type);
- free(mnt_opts);
- free(optbuf);
- return -1;
+static int do_mount(const char *mnt, const char **typep, mode_t rootmode,
+ int fd, const char *opts, const char *dev, char **sourcep,
+ char **mnt_optsp)
+{
+ struct mount_params mp = { .fd = fd }; /* implicit zero of other params */
+ int res;
+
+ mp.rootmode = rootmode;
+ mp.dev = dev;
+
+ res = prepare_mount(opts, &mp);
+ if (res == -1)
+ return -1;
+
+ res = perform_mount(mnt, &mp);
+ if (res == -1) {
+ free_mount_params(&mp);
+ return -1;
+ }
+
+ *sourcep = mp.source;
+ *typep = mp.type;
+ *mnt_optsp = mp.mnt_opts;
+
+ /* Free only the intermediate allocations, not the returned ones */
+ free(mp.fsname);
+ free(mp.subtype);
+ free(mp.optbuf);
+
+ return 0;
}
static int check_perm(const char **mntp, struct stat *stbuf, int *mountpoint_fd)
--
2.43.0
^ permalink raw reply related [flat|nested] 59+ messages in thread
* [PATCH 16/19] fusermount: Refactor extract_x_options
2026-03-23 17:44 [PATCH 00/19] libfuse: Add support for synchronous init Bernd Schubert
` (14 preceding siblings ...)
2026-03-23 17:45 ` [PATCH 15/19] Split the fusermount do_mount function Bernd Schubert
@ 2026-03-23 17:45 ` Bernd Schubert
2026-03-24 0:18 ` Darrick J. Wong
2026-03-23 17:45 ` [PATCH 17/19] Make fusermount work bidirectional for sync init Bernd Schubert
` (3 subsequent siblings)
19 siblings, 1 reply; 59+ messages in thread
From: Bernd Schubert @ 2026-03-23 17:45 UTC (permalink / raw)
To: linux-fsdevel
Cc: Miklos Szeredi, Joanne Koong, Darrick J. Wong, Bernd Schubert,
Bernd Schubert
From: Bernd Schubert <bschubert@ddn.com>
Just to make it better readable.
Also add a NULL check for *regular_opts and *x_prefixed_opts,
there will be multiple callers in an upcoming commit and
it becomes harder to track that no caller has an error.
Signed-off-by: Bernd Schubert <bschubert@ddn.com>
---
util/fusermount.c | 58 ++++++++++++++++++++++++++++++++-----------------------
1 file changed, 34 insertions(+), 24 deletions(-)
diff --git a/util/fusermount.c b/util/fusermount.c
index ecf509bb80d5cd129f6e582f1ec666502c55603a..80b42a594e89cdc2f43824f5e274892522fd8cce 100644
--- a/util/fusermount.c
+++ b/util/fusermount.c
@@ -112,22 +112,31 @@ static struct mntent *GETMNTENT(FILE *stream)
/*
* Take a ',' separated option string and extract "x-" options
+ * @original: The original option string
+ * @regular_opts: The regular options
+ * @x_prefixed_opts: The "x-" options
*/
-static int extract_x_options(const char *original, char **non_x_opts,
- char **x_opts)
+static int extract_x_options(const char *original, char **regular_opts,
+ char **x_prefixed_opts)
{
size_t orig_len;
const char *opt, *opt_end;
orig_len = strlen(original) + 1;
- *non_x_opts = calloc(1, orig_len);
- *x_opts = calloc(1, orig_len);
+ if (*regular_opts != NULL || *x_prefixed_opts != NULL) {
+ fprintf(stderr, "%s: regular_opts or x_prefixed_opts not NULL\n",
+ __func__);
+ return -EINVAL;
+ }
- size_t non_x_opts_len = orig_len;
- size_t x_opts_len = orig_len;
+ *regular_opts = calloc(1, orig_len);
+ *x_prefixed_opts = calloc(1, orig_len);
- if (*non_x_opts == NULL || *x_opts == NULL) {
+ size_t regular_opts_len = orig_len;
+ size_t x_prefixed_opts_len = orig_len;
+
+ if (*regular_opts == NULL || *x_prefixed_opts == NULL) {
fprintf(stderr, "%s: Failed to allocate %zuB.\n",
__func__, orig_len);
return -ENOMEM;
@@ -143,16 +152,16 @@ static int extract_x_options(const char *original, char **non_x_opts,
size_t opt_len = opt_end - opt;
size_t opt_len_left = orig_len - (opt - original);
size_t buf_len;
- bool is_x_opts;
+ bool is_x_prefixed_opts;
if (strncmp(opt, "x-", MIN(2, opt_len_left)) == 0) {
- buf_len = x_opts_len;
- is_x_opts = true;
- opt_buf = *x_opts;
+ buf_len = x_prefixed_opts_len;
+ is_x_prefixed_opts = true;
+ opt_buf = *x_prefixed_opts;
} else {
- buf_len = non_x_opts_len;
- is_x_opts = false;
- opt_buf = *non_x_opts;
+ buf_len = regular_opts_len;
+ is_x_prefixed_opts = false;
+ opt_buf = *regular_opts;
}
if (buf_len < orig_len) {
@@ -163,7 +172,8 @@ static int extract_x_options(const char *original, char **non_x_opts,
/* omits ',' */
if ((ssize_t)(buf_len - opt_len) < 0) {
/* This would be a bug */
- fprintf(stderr, "%s: no buf space left in copy, orig='%s'\n",
+ fprintf(stderr,
+ "%s: no buf space left in copy, orig='%s'\n",
__func__, original);
return -EIO;
}
@@ -171,10 +181,10 @@ static int extract_x_options(const char *original, char **non_x_opts,
strncat(opt_buf, opt, opt_end - opt);
buf_len -= opt_len;
- if (is_x_opts)
- x_opts_len = buf_len;
+ if (is_x_prefixed_opts)
+ x_prefixed_opts_len = buf_len;
else
- non_x_opts_len = buf_len;
+ regular_opts_len = buf_len;
}
return 0;
@@ -1379,7 +1389,7 @@ static int mount_fuse(const char *mnt, const char *opts, const char **type)
const char *real_mnt = mnt;
int mountpoint_fd = -1;
char *do_mount_opts = NULL;
- char *x_opts = NULL;
+ char *x_prefixed_opts = NULL;
fd = open_fuse_device(dev);
if (fd == -1)
@@ -1397,7 +1407,7 @@ static int mount_fuse(const char *mnt, const char *opts, const char **type)
}
// Extract any options starting with "x-"
- res= extract_x_options(opts, &do_mount_opts, &x_opts);
+ res = extract_x_options(opts, &do_mount_opts, &x_prefixed_opts);
if (res)
goto fail_close_fd;
@@ -1420,14 +1430,14 @@ static int mount_fuse(const char *mnt, const char *opts, const char **type)
}
if (geteuid() == 0) {
- if (x_opts && strlen(x_opts) > 0) {
+ if (x_prefixed_opts && strlen(x_prefixed_opts) > 0) {
/*
* Add back the options starting with "x-" to opts from
* do_mount. +2 for ',' and '\0'
*/
size_t mnt_opts_len = strlen(mnt_opts);
size_t x_mnt_opts_len = mnt_opts_len+
- strlen(x_opts) + 2;
+ strlen(x_prefixed_opts) + 2;
char *x_mnt_opts = calloc(1, x_mnt_opts_len);
if (mnt_opts_len) {
@@ -1435,7 +1445,7 @@ static int mount_fuse(const char *mnt, const char *opts, const char **type)
strncat(x_mnt_opts, ",", 2);
}
- strncat(x_mnt_opts, x_opts,
+ strncat(x_mnt_opts, x_prefixed_opts,
x_mnt_opts_len - mnt_opts_len - 2);
free(mnt_opts);
@@ -1452,7 +1462,7 @@ static int mount_fuse(const char *mnt, const char *opts, const char **type)
out_free:
free(source);
free(mnt_opts);
- free(x_opts);
+ free(x_prefixed_opts);
free(do_mount_opts);
return fd;
--
2.43.0
^ permalink raw reply related [flat|nested] 59+ messages in thread
* [PATCH 17/19] Make fusermount work bidirectional for sync init
2026-03-23 17:44 [PATCH 00/19] libfuse: Add support for synchronous init Bernd Schubert
` (15 preceding siblings ...)
2026-03-23 17:45 ` [PATCH 16/19] fusermount: Refactor extract_x_options Bernd Schubert
@ 2026-03-23 17:45 ` Bernd Schubert
2026-03-24 19:35 ` Darrick J. Wong
2026-03-23 17:45 ` [PATCH 18/19] New mount API: Filter out "user=" Bernd Schubert
` (2 subsequent siblings)
19 siblings, 1 reply; 59+ messages in thread
From: Bernd Schubert @ 2026-03-23 17:45 UTC (permalink / raw)
To: linux-fsdevel
Cc: Miklos Szeredi, Joanne Koong, Darrick J. Wong, Bernd Schubert,
Bernd Schubert
From: Bernd Schubert <bschubert@ddn.com>
Signed-off-by: Bernd Schubert <bschubert@ddn.com>
---
doc/README.fusermount | 359 ++++++++++++++++++++++++++++++++++++++++++++++++++
util/fusermount.c | 317 ++++++++++++++++++++++++++++++++++++++++++--
util/meson.build | 2 +-
3 files changed, 665 insertions(+), 13 deletions(-)
diff --git a/doc/README.fusermount b/doc/README.fusermount
new file mode 100644
index 0000000000000000000000000000000000000000..54a3bac4f58964a4ed312d6f6bc15606fed1e647
--- /dev/null
+++ b/doc/README.fusermount
@@ -0,0 +1,359 @@
+Synchronous FUSE_INIT Protocol
+================================
+
+Overview
+--------
+
+The sync-init feature enables the FUSE library to start worker threads and
+perform initialization ioctl calls BEFORE the actual mount() syscall happens.
+This is required for the kernel's synchronous FUSE_INIT feature, where the
+mount() syscall blocks until the FUSE daemon processes the INIT request.
+
+Without this feature, there would be a deadlock:
+- mount() blocks waiting for INIT response
+- Worker threads can't start because mount() hasn't returned
+- INIT request can't be processed because worker threads aren't running
+
+
+Protocol Flow
+-------------
+
+Traditional mount flow:
+ 1. Library calls fusermount3
+ 2. fusermount3 opens /dev/fuse
+ 3. fusermount3 performs mount() syscall
+ 4. fusermount3 sends fd to library
+ 5. Library starts worker threads
+ 6. Worker threads process FUSE requests
+
+Sync-init mount flow:
+ 1. Library calls fusermount3 with --sync-init flag
+ 2. fusermount3 opens /dev/fuse
+ 3. fusermount3 sends fd to library
+ 4. Library receives fd
+ 5. Library performs FUSE_DEV_IOC_SYNC_INIT ioctl
+ 6. Library starts worker threads
+ 7. Library sends "proceed" signal to fusermount3
+ 8. fusermount3 performs mount() syscall (blocks until INIT completes)
+ 9. Worker threads process INIT request
+ 10. mount() syscall completes
+ 11. fusermount3 exits
+
+
+Implementation Details
+----------------------
+
+Bidirectional Communication:
+ - Uses the existing unix socket (_FUSE_COMMFD environment variable)
+ - Simple 1-byte protocol for signaling
+ - Library signals fusermount3 when ready to proceed with mount
+
+fusermount3 Changes:
+ - New --sync-init command-line option
+ - Split mount operation into two phases:
+ * mount_fuse_prepare(): Opens device, prepares parameters
+ * mount_fuse_finish_fsmount(): Performs actual mount() syscall
+ - wait_for_signal(): Waits for library to signal readiness
+ - struct mount_context: Preserves state between phases
+
+Library Changes:
+ - fuse_session_mount_new_api(): Uses new protocol when available
+ - Sends "proceed" signal after worker thread is ready
+ - Handles both old and new mount protocols for compatibility
+
+
+Backward Compatibility
+----------------------
+
+The implementation maintains full backward compatibility:
+ - Old library + new fusermount3: Works (uses traditional flow)
+ - New library + old fusermount3: Falls back to traditional flow
+ - New library + new fusermount3: Uses sync-init flow when appropriate
+
+
+Error Handling
+--------------
+
+If any step fails during the sync-init flow:
+ - fusermount3 closes the fd and exits with error
+ - Library detects failure and cleans up
+ - No mount is left in inconsistent state
+
+Connection closure:
+ - If library closes socket before signaling, fusermount3 detects and exits
+ - If fusermount3 crashes, library detects closed socket
+
+
+Security Considerations
+-----------------------
+
+The sync-init protocol does not introduce new security concerns:
+ - Uses the same privilege separation as traditional mount
+ - Socket communication is already established and trusted
+ - No new privileged operations are added
+ - File descriptor passing uses existing SCM_RIGHTS mechanism
+
+
+Performance Impact
+------------------
+
+Minimal performance impact:
+ - One additional recv() call in fusermount3
+ - One additional send() call in library
+ - Total overhead: ~2 context switches
+ - Only affects mount time, not runtime performance
+
+
+Future Enhancements
+-------------------
+
+Potential improvements:
+ - Extended protocol for more complex initialization sequences
+ - Support for multiple worker threads coordination
+ - Enhanced error reporting through the socket
+ - Timeout mechanisms for detecting hung initialization
+
+
+ASCII Workflow Diagrams
+========================
+
+1. Traditional Mount Flow (without --sync-init, async INIT)
+------------------------------------------------------------
+
+Library fusermount3 Kernel
+ | | |
+ |--- spawn fusermount3 ---->| |
+ | | |
+ | [open /dev/fuse] |
+ | |------- open -------->|
+ | |<------ fd ---------- |
+ | | |
+ | [mount() syscall] |
+ | |------ mount -------->|
+ | |<----- success ------ | [mount returns immediately]
+ | | | [INIT queued in kernel]
+ | [send_fd(fd)] |
+ |<------- fd --------------| |
+ | | |
+ | [fusermount3 exits] |
+ | |
+ | [start worker thread] |
+ | [worker reads /dev/fuse] |
+ |---------------------------------------- read -->|
+ |<--------------------------------------- INIT ---| [dequeued from kernel]
+ | |
+ | OK: INIT was queued, worker reads it later |
+ | Works fine for async INIT |
+
+
+1b. Problem: Synchronous INIT without --sync-init
+--------------------------------------------------
+
+Library fusermount3 Kernel
+ | | |
+ |--- spawn fusermount3 ---->| |
+ | | |
+ | [open /dev/fuse] |
+ | |------- open -------->|
+ | |<------ fd ---------- |
+ | | |
+ | [mount() syscall] |
+ | |------ mount -------->|
+ | | | [mount BLOCKS waiting for INIT]
+ | | (BLOCKED) | [needs worker to process INIT]
+ | | |
+ | [waiting for fd...] | |
+ | | |
+ | | |
+ | DEADLOCK: mount() waits for INIT response |
+ | but worker thread not started yet |
+ | because we're waiting for fd |
+
+
+2. Sync-Init Mount Flow (with --sync-init)
+-------------------------------------------
+
+Library fusermount3 Kernel
+ | | |
+ |--- spawn fusermount3 ---->| |
+ | with --sync-init | |
+ | | |
+ | [open /dev/fuse] |
+ | |------- open -------->|
+ | |<------ fd ---------- |
+ | | |
+ | [send_fd(fd)] |
+ |<------- fd --------------| |
+ | | |
+ | [wait_for_signal()] |
+ | | (BLOCKED) |
+ | | |
+ | [ioctl SYNC_INIT] | |
+ |---------------------------------------- ioctl -->|
+ | |
+ | [start worker thread] |
+ | [worker ready] |
+ | | |
+ |--- "proceed" signal ----->| |
+ | [signal received] |
+ | | |
+ | [mount() syscall] |
+ | |------ mount -------->|
+ | | | [mount blocks]
+ | | | [sends INIT]
+ |<------------------------------------------------ |
+ | | |
+ | [worker processes INIT] | |
+ |------------------------------------------------->|
+ | | | [mount unblocks]
+ | |<----- success ------ |
+ | | |
+ | [fusermount3 exits] |
+ | |
+ | SUCCESS: Worker ready before mount() |
+ | INIT processed synchronously |
+
+
+3. Error Scenario: Library Crashes Before Signaling
+----------------------------------------------------
+
+Library fusermount3 Kernel
+ | | |
+ |--- spawn fusermount3 ---->| |
+ | with --sync-init | |
+ | | |
+ | [open /dev/fuse] |
+ | |------- open -------->|
+ | |<------ fd ---------- |
+ | | |
+ | [send_fd(fd)] |
+ |<------- fd --------------| |
+ | | |
+ | [wait_for_signal()] |
+ | | (BLOCKED) |
+ | | |
+ X [library crashes] | |
+ | | |
+ | [recv() returns 0] |
+ | [socket closed] |
+ | | |
+ | [cleanup and exit] |
+ | X |
+ | |
+ | RESULT: Clean failure, no mount performed |
+
+
+4. Detailed Function Call Flow
+-------------------------------
+
+Library (lib/fuse_lowlevel.c):
+fuse_session_mount_new_api()
+ |
+ +-- fuse_kern_mount_prepare() [lib/mount.c]
+ | |
+ | +-- fuse_mount_fusermount() [lib/mount_util.c]
+ | |
+ | +-- socketpair() [create comm socket]
+ | |
+ | +-- fork()
+ | |
+ | +-- [child] execl("fusermount3", "--sync-init", ...)
+ | |
+ | +-- [parent] receive_fd() <--- BLOCKS until fd arrives
+ | |
+ | +-- recvmsg(SCM_RIGHTS)
+ | |
+ | +-- return fd
+ |
+ +-- session_start_sync_init() [lib/fuse_lowlevel.c]
+ | |
+ | +-- ioctl(fd, FUSE_DEV_IOC_SYNC_INIT)
+ | |
+ | +-- pthread_create(worker_thread)
+ | |
+ | +-- return
+ |
+ +-- fuse_fusermount_proceed_mnt(socket) [lib/mount.c] <--- NEW: Bidirectional handshake
+ |
+ +-- send(socket, "proceed", 1) <--- Signal fusermount3 to proceed
+ |
+ +-- recv(socket, &status, 1) <--- BLOCKS until mount result arrives
+ | |
+ | +-- [fusermount3 performs mount and sends status byte]
+ |
+ +-- if (status != 0) return -1 <--- Mount failed
+ |
+ +-- return 0 <--- Mount succeeded
+
+
+Utility (util/fusermount.c):
+fusermount3 main() with --sync-init
+ |
+ +-- mount_fuse_sync_init() [util/fusermount.c]
+ |
+ +-- mount_fuse_prepare() [util/fusermount.c]
+ | |
+ | +-- open("/dev/fuse")
+ | |
+ | +-- check_perm() [util/fusermount.c]
+ | |
+ | +-- return fd
+ |
+ +-- send_fd(socket, fd) [util/fusermount.c]
+ | |
+ | +-- sendmsg(SCM_RIGHTS)
+ |
+ +-- wait_for_signal(socket) [util/fusermount.c] <--- BLOCKS until library signals
+ | |
+ | +-- recv(socket, buf, 1)
+ | |
+ | +-- return 0
+ |
+ +-- mount_fuse_finish_fsmount() [util/fusermount.c]
+ | |
+ | +-- fuse_kern_fsmount() [lib/mount_fsmount.c]
+ | | |
+ | | +-- fsopen("fuse", FSOPEN_CLOEXEC)
+ | | | |
+ | | | +-- [kernel creates filesystem context]
+ | | |
+ | | +-- fsconfig(fsfd, SET_STRING, "source", ...)
+ | | +-- fsconfig(fsfd, SET_STRING, "fd", fd_value, ...)
+ | | +-- fsconfig(fsfd, ...) [apply mount options]
+ | | +-- fsconfig(fsfd, CMD_CREATE, ...)
+ | | |
+ | | +-- fsmount(fsfd, FSMOUNT_CLOEXEC, mount_attrs)
+ | | | |
+ | | | +-- [kernel sends FUSE_INIT here]
+ | | | |
+ | | | +-- [worker thread processes INIT]
+ | | | |
+ | | | +-- [fsmount returns mntfd]
+ | | |
+ | | +-- move_mount(mntfd, "", AT_FDCWD, target, ...)
+ | | | |
+ | | | +-- [attach mount to target directory]
+ | | | |
+ | | | +-- [no blocking - INIT already processed]
+ | | |
+ | | +-- add_mount() [lib/mount_fsmount.c - update /etc/mtab]
+ | | |
+ | | +-- return 0 on success, -1 on failure
+ | |
+ | +-- if mount failed: return -1
+ | +-- if mount succeeded: continue
+ |
+ +-- send_status_byte(socket) [util/fusermount.c] <--- NEW: Send result to library
+ | |
+ | +-- status = (mount_result == 0) ? 0 : 1
+ | +-- send(socket, &status, 1)
+ | |
+ | +-- return
+ |
+ +-- return 0
+
+
+Note: The new mount API (fsopen/fsconfig/fsmount/move_mount) is REQUIRED
+ for sync-init because fsmount() triggers FUSE_INIT before the mount
+ is attached. This allows the worker thread to process INIT before
+ move_mount() completes, preventing deadlock.
diff --git a/util/fusermount.c b/util/fusermount.c
index 80b42a594e89cdc2f43824f5e274892522fd8cce..808b4afd89ceb49273c944d43bffe5033e27549b 100644
--- a/util/fusermount.c
+++ b/util/fusermount.c
@@ -957,6 +957,7 @@ static void free_mount_params(struct mount_params *mp)
free(mp->source);
free(mp->type);
free(mp->mnt_opts);
+ memset(mp, 0, sizeof(*mp));
}
/*
@@ -1378,6 +1379,179 @@ static int open_fuse_device(const char *dev)
return fd;
}
+#ifdef HAVE_NEW_MOUNT_API
+/* Forward declaration from lib/mount_fsmount.c */
+int fuse_kern_fsmount(const char *mnt, unsigned long flags, int blkdev,
+ const char *fsname, const char *subtype,
+ const char *source_dev, const char *kernel_opts,
+ const char *mnt_opts);
+#endif
+
+/*
+ * Context for split mount operation (sync-init mode)
+ */
+struct mount_context {
+ int fd;
+ const char *dev;
+ struct stat stbuf;
+ char *source;
+ char *mnt_opts;
+ char *x_opts;
+ const char *type;
+};
+
+/*
+ * Phase 1: Open device and prepare for mount (sync-init mode)
+ * Returns fd on success, -1 on failure
+ */
+static int mount_fuse_prepare(const char *mnt, const char *opts,
+ struct mount_context *ctx)
+{
+ int res;
+ int mountpoint_fd = -1;
+ char *do_mount_opts = NULL;
+ const char *real_mnt = mnt;
+
+ memset(ctx, 0, sizeof(*ctx));
+ ctx->dev = getenv(FUSE_KERN_DEVICE_ENV) ?: FUSE_DEV;
+
+ ctx->fd = open_fuse_device(ctx->dev);
+ if (ctx->fd == -1)
+ return -1;
+
+ drop_privs();
+ read_conf();
+
+ if (getuid() != 0 && mount_max != -1) {
+ int mount_count = count_fuse_fs();
+
+ if (mount_count >= mount_max) {
+ fprintf(stderr,
+ "%s: too many FUSE filesystems mounted; mount_max=N can be set in %s\n",
+ progname, FUSE_CONF);
+ goto fail_close_fd;
+ }
+ }
+
+ res = extract_x_options(opts, &do_mount_opts, &ctx->x_opts);
+ if (res)
+ goto fail_close_fd;
+
+ res = check_perm(&real_mnt, &ctx->stbuf, &mountpoint_fd);
+ restore_privs();
+
+ if (mountpoint_fd != -1)
+ close(mountpoint_fd);
+
+ if (res == -1)
+ goto fail_close_fd;
+
+ free(do_mount_opts);
+ return ctx->fd;
+
+fail_close_fd:
+ close(ctx->fd);
+ free(do_mount_opts);
+ free(ctx->x_opts);
+ ctx->fd = -1;
+ return -1;
+}
+
+#ifdef HAVE_NEW_MOUNT_API
+/*
+ * Phase 2: Perform the actual mount using new mount API (sync-init mode)
+ * Returns 0 on success, -1 on failure
+ */
+static int mount_fuse_finish_fsmount(const char *mnt, const char *opts,
+ struct mount_context *ctx,
+ const char **type)
+{
+ int res;
+ char *do_mount_opts = NULL;
+ char *x_prefixed_opts = NULL;
+ struct mount_params mp = { .fd = ctx->fd };
+ char *final_mnt_opts = NULL;
+
+ /* Extract x-options */
+ res = extract_x_options(opts, &do_mount_opts, &x_prefixed_opts);
+ if (res)
+ goto fail;
+
+ /* Prepare mount parameters */
+ mp.rootmode = ctx->stbuf.st_mode & S_IFMT;
+ mp.dev = ctx->dev;
+
+ res = prepare_mount(do_mount_opts, &mp);
+ if (res == -1)
+ goto fail;
+
+ /* Merge x-options if running as root */
+ final_mnt_opts = mp.mnt_opts;
+ if (geteuid() == 0 && ctx->x_opts && strlen(ctx->x_opts) > 0) {
+ size_t mnt_opts_len = strlen(mp.mnt_opts);
+ size_t x_mnt_opts_len = mnt_opts_len + strlen(ctx->x_opts) + 2;
+ char *x_mnt_opts = calloc(1, x_mnt_opts_len);
+
+ if (!x_mnt_opts)
+ goto fail_free_params;
+
+ if (mnt_opts_len) {
+ strcpy(x_mnt_opts, mp.mnt_opts);
+ strncat(x_mnt_opts, ",", 2);
+ }
+ strncat(x_mnt_opts, ctx->x_opts,
+ x_mnt_opts_len - mnt_opts_len - 2);
+
+ final_mnt_opts = x_mnt_opts;
+ }
+
+ /* Use new mount API */
+ res = fuse_kern_fsmount(mnt, mp.flags, mp.blkdev,
+ mp.fsname, mp.subtype, ctx->dev,
+ mp.optbuf, final_mnt_opts);
+ if (res == -1)
+ goto fail_free_merged;
+
+ /* Change to root directory */
+ res = chdir("/");
+ if (res == -1) {
+ fprintf(stderr, "%s: failed to chdir to '/'\n", progname);
+ goto fail_free_merged;
+ }
+
+ /* Store results in context */
+ ctx->source = mp.source;
+ ctx->type = mp.type;
+ ctx->mnt_opts = final_mnt_opts;
+ *type = mp.type;
+
+ res = 0;
+
+ /* Only free what is not assigned to ctx */
+ free(mp.fsname);
+ free(mp.subtype);
+ free(mp.optbuf);
+ if (final_mnt_opts != mp.mnt_opts)
+ free(mp.mnt_opts);
+
+out:
+ free(do_mount_opts);
+ free(x_prefixed_opts);
+
+ return res;
+
+fail_free_merged:
+ if (final_mnt_opts != mp.mnt_opts)
+ free(final_mnt_opts);
+fail_free_params:
+ free_mount_params(&mp);
+fail:
+ res = -1;
+ goto out;
+}
+#endif /* HAVE_NEW_MOUNT_API */
+
+
static int mount_fuse(const char *mnt, const char *opts, const char **type)
{
int res;
@@ -1473,6 +1647,75 @@ fail_close_fd:
goto out_free;
}
+/* Forward declarations for helper functions */
+static int send_fd(int sock_fd, int fd);
+static int wait_for_signal(int sock_fd);
+
+#ifdef HAVE_NEW_MOUNT_API
+/*
+ * Perform sync-init mount using new mount API
+ * Returns 0 on success, -1 on failure
+ */
+static int mount_fuse_sync_init(const char *mnt, const char *opts,
+ int cfd, const char **type)
+{
+ struct mount_context ctx;
+ int fd, res;
+ int32_t status, send_res;
+
+ /* Phase 1: Open device and prepare */
+ fd = mount_fuse_prepare(mnt, opts, &ctx);
+ if (fd == -1)
+ return -1;
+
+ /* Send fd to caller so it can start worker thread */
+ res = send_fd(cfd, fd);
+ if (res != 0) {
+ close(fd);
+ free(ctx.x_opts);
+ return -1;
+ }
+
+ /* Wait for caller to signal that worker thread is ready */
+ res = wait_for_signal(cfd);
+ if (res != 0) {
+ close(fd);
+ free(ctx.x_opts);
+ return -1;
+ }
+
+ /* Phase 2: Perform the actual mount using new API */
+ res = mount_fuse_finish_fsmount(mnt, opts, &ctx, type);
+
+ /* Send mount result back to caller (4-byte error code) */
+ status = (res == 0) ? 0 : -(int32_t)errno;
+ do {
+ send_res = send(cfd, &status, sizeof(status), 0);
+ } while (send_res == -1 && errno == EINTR);
+ if (send_res != sizeof(status)) {
+ fprintf(stderr, "%s: failed to send mount status: %s\n",
+ progname, strerror(errno));
+ }
+
+ if (res == -1) {
+ close(fd);
+ free(ctx.source);
+ free(ctx.mnt_opts);
+ free(ctx.x_opts);
+ return -1;
+ }
+
+ close(fd);
+
+ /* Cleanup */
+ free(ctx.source);
+ free(ctx.mnt_opts);
+ free(ctx.x_opts);
+
+ return 0;
+}
+#endif /* HAVE_NEW_MOUNT_API */
+
static int send_fd(int sock_fd, int fd)
{
int retval;
@@ -1509,6 +1752,30 @@ static int send_fd(int sock_fd, int fd)
return 0;
}
+/*
+ * Wait for a signal byte from the caller.
+ * Returns 0 on success, -1 on error.
+ */
+static int wait_for_signal(int sock_fd)
+{
+ char buf[1];
+ int res;
+
+ do {
+ res = recv(sock_fd, buf, sizeof(buf), 0);
+ } while (res == -1 && errno == EINTR);
+ if (res != 1) {
+ if (res == 0)
+ fprintf(stderr, "%s: connection closed while waiting for signal\n",
+ progname);
+ else
+ fprintf(stderr, "%s: error receiving signal: %s\n",
+ progname, strerror(errno));
+ return -1;
+ }
+ return 0;
+}
+
/* Helper for should_auto_unmount
*
* fusermount typically has the s-bit set - initial open of `mnt` was as root
@@ -1700,6 +1967,7 @@ int main(int argc, char *argv[])
const char *opts = "";
const char *type = NULL;
int setup_auto_unmount_only = 0;
+ int sync_init_mode = 0;
static const struct option long_opts[] = {
{"unmount", no_argument, NULL, 'u'},
@@ -1712,6 +1980,7 @@ int main(int argc, char *argv[])
// They'ne meant for internal use by mount.c
{"auto-unmount", no_argument, NULL, 'U'},
{"comm-fd", required_argument, NULL, 'c'},
+ {"sync-init", no_argument, NULL, 'S'},
{0, 0, 0, 0}};
progname = strdup(argc > 0 ? argv[0] : "fusermount");
@@ -1746,6 +2015,9 @@ int main(int argc, char *argv[])
case 'c':
commfd = optarg;
break;
+ case 'S':
+ sync_init_mode = 1;
+ break;
case 'z':
lazy = 1;
break;
@@ -1823,21 +2095,42 @@ int main(int argc, char *argv[])
if (setup_auto_unmount_only)
goto wait_for_auto_unmount;
- fd = mount_fuse(mnt, opts, &type);
- if (fd == -1)
- goto err_out;
+ if (sync_init_mode) {
+#ifdef HAVE_NEW_MOUNT_API
+ res = mount_fuse_sync_init(mnt, opts, cfd, &type);
+ if (res == -1)
+ goto err_out;
- res = send_fd(cfd, fd);
- if (res != 0) {
- umount2(mnt, MNT_DETACH); /* lazy umount */
+ if (!auto_unmount) {
+ free(mnt);
+ free((void *) type);
+ return 0;
+ }
+ /* Continue to auto_unmount handling below */
+#else
+ fprintf(stderr, "%s: sync-init mode requires new mount API support\n",
+ progname);
+ fprintf(stderr, "%s: kernel or headers too old (need fsopen/fsmount)\n",
+ progname);
goto err_out;
- }
- close(fd);
+#endif
+ } else {
+ fd = mount_fuse(mnt, opts, &type);
+ if (fd == -1)
+ goto err_out;
- if (!auto_unmount) {
- free(mnt);
- free((void*) type);
- return 0;
+ res = send_fd(cfd, fd);
+ if (res != 0) {
+ umount2(mnt, MNT_DETACH); /* lazy umount */
+ goto err_out;
+ }
+ close(fd);
+
+ if (!auto_unmount) {
+ free(mnt);
+ free((void *) type);
+ return 0;
+ }
}
wait_for_auto_unmount:
diff --git a/util/meson.build b/util/meson.build
index 0e4b1cce95377e73af7dc45655a7088315497ddb..731ef95488461ac21c21b1972a96d58b1187dc5a 100644
--- a/util/meson.build
+++ b/util/meson.build
@@ -1,6 +1,6 @@
fuseconf_path = join_paths(get_option('prefix'), get_option('sysconfdir'), 'fuse.conf')
-executable('fusermount3', ['fusermount.c', '../lib/mount_util.c', '../lib/util.c'],
+executable('fusermount3', ['fusermount.c', '../lib/mount_util.c', '../lib/mount_fsmount.c', '../lib/util.c'],
include_directories: include_dirs,
install: true,
install_dir: get_option('bindir'),
--
2.43.0
^ permalink raw reply related [flat|nested] 59+ messages in thread
* [PATCH 18/19] New mount API: Filter out "user="
2026-03-23 17:44 [PATCH 00/19] libfuse: Add support for synchronous init Bernd Schubert
` (16 preceding siblings ...)
2026-03-23 17:45 ` [PATCH 17/19] Make fusermount work bidirectional for sync init Bernd Schubert
@ 2026-03-23 17:45 ` Bernd Schubert
2026-03-24 19:51 ` Darrick J. Wong
2026-03-23 17:45 ` [PATCH 19/19] Add support for sync-init of unprivileged daemons Bernd Schubert
2026-03-24 0:19 ` [PATCH 00/19] libfuse: Add support for synchronous init Darrick J. Wong
19 siblings, 1 reply; 59+ messages in thread
From: Bernd Schubert @ 2026-03-23 17:45 UTC (permalink / raw)
To: linux-fsdevel
Cc: Miklos Szeredi, Joanne Koong, Darrick J. Wong, Bernd Schubert,
Bernd Schubert
From: Bernd Schubert <bschubert@ddn.com>
This gets added in the fusermount process and kernel then fails
the mount.
Signed-off-by: Bernd Schubert <bschubert@ddn.com>
---
lib/mount_fsmount.c | 13 ++++++++++++-
1 file changed, 12 insertions(+), 1 deletion(-)
diff --git a/lib/mount_fsmount.c b/lib/mount_fsmount.c
index f1fec790bb80f8815d485a068dc7efdff1746309..76c14cf9a22465160fc6b206ca9b6c9e7300adba 100644
--- a/lib/mount_fsmount.c
+++ b/lib/mount_fsmount.c
@@ -218,6 +218,16 @@ static int is_mount_attr_opt(const char *opt)
strcmp(opt, "symfollow") == 0;
}
+/**
+ * Check if an option is a mount table option (not passed to fsconfig)
+ */
+static int is_mtab_only_opt(const char *opt)
+{
+ /* These options are for /run/mount/utab only, not for the kernel */
+ return strncmp(opt, "user=", 5) == 0 ||
+ strcmp(opt, "rw") == 0;
+}
+
/*
* Parse kernel options string and apply via fsconfig
* Options are comma-separated key=value pairs
@@ -241,7 +251,8 @@ static int apply_mount_opts(int fsfd, const char *opts)
opt = strtok_r(opts_copy, ",", &saveptr);
while (opt) {
/* Skip mount attributes - they're handled by fsmount(), not fsconfig() */
- if (!is_mount_attr_opt(opt)) {
+ /* Skip mtab-only options - they're for /run/mount/utab, not kernel */
+ if (!is_mount_attr_opt(opt) && !is_mtab_only_opt(opt)) {
res = apply_opt_key_value(fsfd, opt);
if (res < 0) {
free(opts_copy);
--
2.43.0
^ permalink raw reply related [flat|nested] 59+ messages in thread
* [PATCH 19/19] Add support for sync-init of unprivileged daemons
2026-03-23 17:44 [PATCH 00/19] libfuse: Add support for synchronous init Bernd Schubert
` (17 preceding siblings ...)
2026-03-23 17:45 ` [PATCH 18/19] New mount API: Filter out "user=" Bernd Schubert
@ 2026-03-23 17:45 ` Bernd Schubert
2026-03-24 20:21 ` Darrick J. Wong
2026-03-24 0:19 ` [PATCH 00/19] libfuse: Add support for synchronous init Darrick J. Wong
19 siblings, 1 reply; 59+ messages in thread
From: Bernd Schubert @ 2026-03-23 17:45 UTC (permalink / raw)
To: linux-fsdevel
Cc: Miklos Szeredi, Joanne Koong, Darrick J. Wong, Bernd Schubert,
Bernd Schubert
From: Bernd Schubert <bschubert@ddn.com>
This makes use of the bidirectional fusermount. Added is
doc/README.mount, which explains the new bidirectional
communication with fusermount.
Signed-off-by: Bernd Schubert <bschubert@ddn.com>
---
doc/README.mount | 86 ++++++++++++++++++++++++
doc/README.sync-init | 184 +++++++++++++++++++++++++++++++++++++++++++++++++++
lib/fuse_lowlevel.c | 115 ++++++++++++++++++++++++++------
lib/mount.c | 126 ++++++++++++++++++++++++++++++++++-
lib/mount_i_linux.h | 7 ++
util/fusermount.c | 2 -
6 files changed, 494 insertions(+), 26 deletions(-)
diff --git a/doc/README.mount b/doc/README.mount
new file mode 100644
index 0000000000000000000000000000000000000000..526382ad8a5f6b405a7cb1927b79bacd6c2c2c5c
--- /dev/null
+++ b/doc/README.mount
@@ -0,0 +1,86 @@
+FUSE Mount API Flowcharts
+=========================
+
+Old Mount API
+-------------
+
+fuse_kern_mount()
+ |
+ +-- fuse_mount_sys()
+ | +-- Try direct mount → mount() syscall
+ | +-- On EPERM: fuse_mount_fusermount()
+ | +-- socketpair()
+ | +-- spawn fusermount3 (no --sync-init)
+ | +-- fusermount3: open /dev/fuse, mount(), send fd
+ | +-- receive_fd() → return fd
+ |
+ +-- Worker threads started AFTER mount
+ └─> FUSE_INIT asynchronous (queued in kernel)
+
+
+New Mount API - Privileged Mount
+---------------------------------
+
+fuse_session_mount_new_api()
+ |
+ +-- fuse_kern_mount_prepare() → open /dev/fuse → fd
+ |
+ +-- session_start_sync_init(se, fd)
+ | +-- ioctl(fd, FUSE_DEV_IOC_SYNC_INIT)
+ | +-- pthread_create(worker) → ready to process FUSE_INIT
+ |
+ +-- fuse_kern_fsmount_mo()
+ | +-- fsopen/fsconfig/fsmount (BLOCKS until FUSE_INIT completes)
+ | +-- Worker processes FUSE_INIT during fsmount()
+ | +-- move_mount()
+ |
+ +-- session_wait_sync_init_completion(se) → pthread_join
+ └─> return fd
+
+
+New Mount API - EPERM Fallback (fusermount3 with sync-init)
+------------------------------------------------------------
+
+fuse_session_mount_new_api()
+ |
+ +-- fuse_kern_mount_prepare() → open /dev/fuse → fd1
+ |
+ +-- session_start_sync_init(se, fd1)
+ | +-- ioctl(fd1, FUSE_DEV_IOC_SYNC_INIT)
+ | +-- pthread_create(worker) → ready with fd1
+ |
+ +-- fuse_kern_fsmount_mo() → EPERM
+ |
+ +-- *** FALLBACK TO FUSERMOUNT3 WITH SYNC-INIT ***
+ |
+ +-- session_wait_sync_init_completion(se)
+ | +-- pthread_cancel/join → terminate worker with wrong fd1
+ |
+ +-- close(fd1)
+ |
+ +-- fuse_mount_fusermount_sync_init() [NEW]
+ | +-- socketpair()
+ | +-- spawn fusermount3 --sync-init
+ | +-- fusermount3: open /dev/fuse → fd2, send fd2
+ | +-- receive_fd() → fd2
+ | +-- fusermount3 waits for signal
+ | └─> return fd2, sock
+ |
+ +-- session_start_sync_init(se, fd2)
+ | +-- ioctl(fd2, FUSE_DEV_IOC_SYNC_INIT)
+ | +-- pthread_create(worker) → ready with fd2
+ |
+ +-- send_proceed_signal(sock) [NEW]
+ | +-- send(sock, "\0", 1) → signal fusermount3
+ |
+ +-- fusermount3: mount() (BLOCKS)
+ | +-- Kernel sends FUSE_INIT to fd2
+ | +-- Worker processes FUSE_INIT
+ | +-- mount() returns
+ |
+ +-- close(sock)
+ |
+ +-- session_wait_sync_init_completion(se) → pthread_join
+ |
+ └─> return fd2
+
diff --git a/doc/README.sync-init b/doc/README.sync-init
new file mode 100644
index 0000000000000000000000000000000000000000..44e47a2eef2c45026abaa19562537eef37f256b9
--- /dev/null
+++ b/doc/README.sync-init
@@ -0,0 +1,184 @@
+FUSE Synchronous vs Asynchronous FUSE_INIT
+============================================
+
+This document explains the difference between asynchronous and synchronous
+FUSE_INIT processing, and when each mode is used.
+
+
+Overview
+--------
+
+FUSE_INIT is the initial handshake between the kernel FUSE module and the
+userspace filesystem daemon. During this handshake, the kernel and daemon
+negotiate capabilities, protocol version, and various feature flags.
+
+Asynchronous FUSE_INIT (Traditional Behavior)
+----------------------------------------------
+
+In the traditional asynchronous mode:
+
+1. mount() syscall completes and returns to caller
+2. Filesystem appears mounted to the system
+3. FUSE daemon starts worker threads
+4. Worker threads process FUSE_INIT request
+5. Filesystem becomes fully operational
+
+Timeline:
+ mount() -----> returns
+ |
+ v
+ FUSE_INIT sent
+ |
+ v
+ daemon processes FUSE_INIT
+ |
+ v
+ filesystem ready
+
+Limitations:
+
+1. **No early requests**: The kernel cannot send requests (like getxattr)
+ during the mount() syscall. This breaks SELinux, which needs to query
+ extended attributes on the root inode immediately upon mounting.
+
+2. **Daemonization timing**: With the old fuse_daemonize() API, the daemon
+ must call it AFTER mount, because there's no way to report mount failures
+ to the parent process if daemonization happens first.
+
+3. **No custom root inode**: The root inode ID is hardcoded to FUSE_ROOT_ID (1)
+ because FUSE_INIT hasn't been processed yet when the mount completes.
+
+4. **Thread startup after mount**: io_uring threads and other worker threads
+ can only start after mount() returns, not before.
+
+
+Synchronous FUSE_INIT (New Behavior)
+-------------------------------------
+
+Kernel support: Linux kernel commit dfb84c330794 (v6.18+)
+libfuse support: libfuse 3.19+
+
+In synchronous mode:
+
+1. FUSE daemon opens /dev/fuse
+2. Daemon calls ioctl(fd, FUSE_DEV_IOC_SYNC_INIT)
+3. Daemon starts worker thread
+4. Daemon calls mount() syscall
+5. Kernel sends FUSE_INIT during mount() - mount() blocks
+6. Worker thread processes FUSE_INIT while mount() is blocked
+7. Worker thread may process additional requests (getxattr, etc.)
+8. mount() syscall completes and returns
+9. Filesystem is fully operational
+
+Timeline:
+ open /dev/fuse
+ |
+ v
+ ioctl(FUSE_DEV_IOC_SYNC_INIT)
+ |
+ v
+ start worker thread
+ |
+ v
+ mount() -----> blocks
+ | |
+ | v
+ | FUSE_INIT sent
+ | |
+ | v
+ | worker processes FUSE_INIT
+ | |
+ | v
+ | (possible getxattr, etc.)
+ | |
+ +-------> returns
+ |
+ v
+ filesystem ready
+
+Advantages:
+
+1. **SELinux support**: The kernel can send getxattr requests during mount()
+ to query security labels on the root inode.
+
+2. **Early daemonization**: The daemon can fork BEFORE mount using the new
+ fuse_daemonize_start()/signal() API, and report mount failures to the
+ parent process.
+
+3. **Custom root inode**: The daemon can specify a custom root inode ID
+ during FUSE_INIT, before mount() completes.
+
+4. **Thread startup before mount**: io_uring threads and worker threads
+ start before mount(), ensuring they're ready to handle requests.
+
+5. **Better error reporting**: Mount failures and initialization errors
+ can be properly reported to the parent process when using the new
+ daemonization API.
+
+
+When Synchronous FUSE_INIT is Used
+-----------------------------------
+
+libfuse automatically enables synchronous FUSE_INIT when:
+
+1. The application calls fuse_session_want_sync_init(), OR
+2. The new daemonization API is used (fuse_daemonize_start() was called)
+
+Synchronous FUSE_INIT requires:
+- Kernel support (commit dfb84c330794 or later)
+- Worker thread started before mount()
+- ioctl(FUSE_DEV_IOC_SYNC_INIT) succeeds
+
+If the kernel doesn't support synchronous FUSE_INIT, libfuse automatically
+falls back to asynchronous mode.
+
+
+Implementation Details
+----------------------
+
+The synchronous FUSE_INIT implementation uses a worker thread:
+
+- **session_sync_init_worker()**: Thread function that polls /dev/fuse
+ and processes FUSE_INIT and any subsequent requests until mount completes.
+
+- **session_start_sync_init()**: Creates the worker thread before mount().
+ Calls ioctl(FUSE_DEV_IOC_SYNC_INIT) to enable kernel support.
+
+- **session_wait_sync_init_completion()**: Waits for the worker thread
+ to complete after mount() returns. Checks for errors.
+
+The worker thread processes requests in a loop until se->terminate_mount_worker
+is set, which happens after mount() completes successfully.
+
+
+Compatibility
+-------------
+
+Synchronous FUSE_INIT is fully backward compatible:
+
+- Old kernels: ioctl returns ENOTTY, libfuse falls back to async mode
+- Old applications: Continue to work with async FUSE_INIT
+- New applications on old kernels: Graceful fallback to async mode
+- New applications on new kernels: Automatic sync mode when appropriate
+
+
+Example: Enabling Synchronous FUSE_INIT
+----------------------------------------
+
+Explicit request:
+ struct fuse_session *se = fuse_session_new(...);
+ fuse_session_want_sync_init(se);
+ fuse_session_mount(se, mountpoint);
+
+Automatic (with new daemonization API):
+ fuse_daemonize_start(0); // Triggers sync init automatically
+ fuse_session_mount(se, mountpoint);
+
+
+See Also
+--------
+
+- doc/README.daemonize - New daemonization API documentation
+- doc/README.fusermount - Synchronous FUSE_INIT protocol with fusermount3
+- doc/README.mount - Mount implementation details
+
diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c
index a7293a3898c37c3877eadf965d310ae2aa5cc2d1..da966217ed841744a20bee60de8ae615d1015b47 100644
--- a/lib/fuse_lowlevel.c
+++ b/lib/fuse_lowlevel.c
@@ -41,6 +41,7 @@
#include <assert.h>
#include <sys/file.h>
#include <sys/ioctl.h>
+#include <sys/wait.h>
#include <stdalign.h>
#include <poll.h>
@@ -4551,6 +4552,8 @@ static int session_wait_sync_init_completion(struct fuse_session *se)
se->init_wakeup_fd = -1;
}
+ se->init_thread = 0;
+
if (se->init_error != 0) {
fuse_log(FUSE_LOG_ERR, "fuse: init worker failed: %d\n", se->init_error);
return -1;
@@ -4564,56 +4567,125 @@ static int session_wait_sync_init_completion(struct fuse_session *se)
return 0;
}
-/* Only linux supports sync FUSE_INIT so far */
+/*
+ * Mount using the new Linux mount API (fsopen/fsconfig/fsmount/move_mount)
+ * Sync-init is only supported with the new API, as the mount might hang
+ * in case of daemon crash during FUSE_INIT. That also means once the sync init
+ * ioctl succeed fallback is not allowed anymore.
+ * Returns: fd on success, -1 on failure
+ */
static int fuse_session_mount_new_api(struct fuse_session *se,
- const char *mountpoint)
+ const char *mountpoint, bool *fall_back)
{
int fd = -1;
+ int sock_fd = -1;
+ pid_t fusermount_pid = -1;
int res, err;
char *mnt_opts = NULL;
char *mnt_opts_with_fd = NULL;
char fd_opt[32];
res = fuse_kern_mount_get_base_mnt_opts(se->mo, &mnt_opts);
+ err = -EIO;
if (res == -1) {
fuse_log(FUSE_LOG_ERR, "fuse: failed to get base mount options\n");
- err = -EIO;
goto err;
}
fd = fuse_kern_mount_prepare(mountpoint, se->mo);
if (fd == -1) {
fuse_log(FUSE_LOG_ERR, "Mount preparation failed.\n");
- err = -EIO;
goto err;
}
- /*
- * Enable synchronous FUSE_INIT and start worker thread, sync init
- * failure is not an error
- */
+ *fall_back = true;
se->fd = fd;
err = session_start_sync_init(se, fd);
if (err) {
/* ENOTTY means kernel doesn't support sync init - not an error */
if (err != -ENOTTY)
goto err;
+ } else {
+ *fall_back = false;
}
+
+
snprintf(fd_opt, sizeof(fd_opt), "fd=%i", fd);
+ err = -ENOMEM;
if (fuse_opt_add_opt(&mnt_opts_with_fd, mnt_opts) == -1 ||
fuse_opt_add_opt(&mnt_opts_with_fd, fd_opt) == -1) {
- err = -ENOMEM;
goto err;
}
+ /* Try to mount directly */
err = fuse_kern_fsmount_mo(mountpoint, se->mo, mnt_opts_with_fd);
+
+ /* If mount failed with EPERM, fall back to fusermount3 with sync-init */
+ if (err < 0 && errno == EPERM) {
+ if (se->debug)
+ fuse_log(FUSE_LOG_DEBUG,
+ "fuse: privileged mount failed with EPERM, falling back to fusermount3\n");
+
+ /* Terminate worker thread with wrong fd */
+ if (session_wait_sync_init_completion(se) < 0)
+ fuse_log(FUSE_LOG_ERR, "fuse: sync init completion failed\n");
+
+ /* Close the privileged fd */
+ close(fd);
+ fd = -1;
+ se->fd = -1;
+
+ /* Call fusermount3 with --sync-init */
+ err = -ENOTSUP;
+ fd = mount_fusermount_obtain_fd(mountpoint, se->mo, mnt_opts,
+ &sock_fd, &fusermount_pid);
+ if (fd < 0) {
+ fuse_log(
+ FUSE_LOG_ERR,
+ "fuse: fusermount3 sync-init failed\n");
+ goto err;
+ }
+
+ /* Start worker thread with correct fd from fusermount3 */
+ se->fd = fd;
+ err = session_start_sync_init(se, fd);
+ if (err) {
+ if (err != -ENOTTY) {
+ fuse_log(
+ FUSE_LOG_ERR,
+ "fuse: failed to start sync init worker\n");
+ goto err_with_sock;
+ }
+ } else {
+ *fall_back = false;
+ }
+
+ /* Send proceed signal and wait for mount result */
+ err = fuse_fusermount_proceed_mnt(sock_fd);
+ if (err < 0) {
+ err = -EIO;
+ goto err_with_sock;
+ }
+ } else if (err < 0) {
+ /* Mount failed with non-EPERM error, bail out */
+ goto err;
+ }
+
+err_with_sock:
+ if (sock_fd >= 0) {
+ close(sock_fd);
+ /* Reap fusermount3 child process to prevent zombie */
+ if (fusermount_pid > 0)
+ waitpid(fusermount_pid, NULL, 0);
+ }
err:
if (err < 0) {
+ /* Close fd first to unblock worker thread */
if (fd >= 0)
close(fd);
fd = -1;
se->fd = -1;
- se->error = -errno;
+ se->error = err;
}
/* Wait for synchronous FUSE_INIT to complete */
if (session_wait_sync_init_completion(se) < 0)
@@ -4625,10 +4697,11 @@ err:
}
#else
static int fuse_session_mount_new_api(struct fuse_session *se,
- const char *mountpoint)
+ const char *mountpoint, bool *fall_back)
{
(void) se;
(void) mountpoint;
+ (void) fall_back;
return -1;
}
@@ -4638,6 +4711,7 @@ int fuse_session_mount(struct fuse_session *se, const char *_mountpoint)
{
int fd;
char *mountpoint;
+ bool fall_back;
if (_mountpoint == NULL) {
fuse_log(FUSE_LOG_ERR, "Invalid null-ptr mountpoint!\n");
@@ -4681,21 +4755,18 @@ int fuse_session_mount(struct fuse_session *se, const char *_mountpoint)
return 0;
}
- /* new linux mount api */
- fd = fuse_session_mount_new_api(se, mountpoint);
- if (fd >= 0)
- goto out;
+ /* new linux mount api (and sync init) */
+ fd = fuse_session_mount_new_api(se, mountpoint, &fall_back);
/* fall back to old API */
- se->error = 0; /* reset error of new api */
- fd = fuse_kern_mount(mountpoint, se->mo);
- if (fd < 0)
- goto error_out;
+ if (fall_back && fd < 0) {
+ se->error = 0; /* reset error of new api */
+ fd = fuse_kern_mount(mountpoint, se->mo);
+ if (fd < 0)
+ goto error_out;
+ }
-out:
se->fd = fd;
-
- /* Save mountpoint */
se->mountpoint = mountpoint;
return 0;
diff --git a/lib/mount.c b/lib/mount.c
index 263b05051c236458b830c40181bce7f494803800..985938ea0be3e1affad19adad527a31ac2ca6034 100644
--- a/lib/mount.c
+++ b/lib/mount.c
@@ -41,6 +41,7 @@
#define FUSERMOUNT_PROG "fusermount3"
#define FUSE_COMMFD_ENV "_FUSE_COMMFD"
#define FUSE_COMMFD2_ENV "_FUSE_COMMFD2"
+#define ARG_FD_ENTRY_SIZE 30
enum {
KEY_KERN_FLAG,
@@ -313,7 +314,7 @@ static int setup_auto_unmount(const char *mountpoint, int quiet)
return -1;
}
- char arg_fd_entry[30];
+ char arg_fd_entry[ARG_FD_ENTRY_SIZE];
snprintf(arg_fd_entry, sizeof(arg_fd_entry), "%i", fds[0]);
setenv(FUSE_COMMFD_ENV, arg_fd_entry, 1);
/*
@@ -386,7 +387,7 @@ static int fuse_mount_fusermount(const char *mountpoint, struct mount_opts *mo,
return -1;
}
- char arg_fd_entry[30];
+ char arg_fd_entry[ARG_FD_ENTRY_SIZE];
snprintf(arg_fd_entry, sizeof(arg_fd_entry), "%i", fds[0]);
setenv(FUSE_COMMFD_ENV, arg_fd_entry, 1);
/*
@@ -446,6 +447,127 @@ static int fuse_mount_fusermount(const char *mountpoint, struct mount_opts *mo,
return fd;
}
+/*
+ * Mount using fusermount3 with --sync-init flag for bidirectional fd exchange
+ * Used by new mount API when privileged mount fails with EPERM
+ *
+ * Returns: fd on success, -1 on failure
+ * On success, *sock_fd_out contains the socket fd for signaling fusermount3
+ */
+int mount_fusermount_obtain_fd(const char *mountpoint, struct mount_opts *mo,
+ const char *opts, int *sock_fd_out,
+ pid_t *pid_out)
+{
+ int fds[2];
+ pid_t pid;
+ int res;
+ char arg_fd_entry[ARG_FD_ENTRY_SIZE];
+ posix_spawn_file_actions_t action;
+ int fd, status;
+
+ (void)mo;
+
+ if (!mountpoint) {
+ fuse_log(FUSE_LOG_ERR, "fuse: missing mountpoint parameter\n");
+ return -1;
+ }
+
+ res = socketpair(PF_UNIX, SOCK_STREAM, 0, fds);
+ if (res == -1) {
+ fuse_log(FUSE_LOG_ERR, "Running %s: socketpair() failed: %s\n",
+ FUSERMOUNT_PROG, strerror(errno));
+ return -1;
+ }
+
+ snprintf(arg_fd_entry, sizeof(arg_fd_entry), "%i", fds[0]);
+ setenv(FUSE_COMMFD_ENV, arg_fd_entry, 1);
+ snprintf(arg_fd_entry, sizeof(arg_fd_entry), "%i", fds[1]);
+ setenv(FUSE_COMMFD2_ENV, arg_fd_entry, 1);
+
+ char const *const argv[] = {
+ FUSERMOUNT_PROG,
+ "--sync-init",
+ "-o", opts ? opts : "",
+ "--",
+ mountpoint,
+ NULL,
+ };
+
+ posix_spawn_file_actions_init(&action);
+ posix_spawn_file_actions_addclose(&action, fds[1]);
+ status = fusermount_posix_spawn(&action, argv, &pid);
+ posix_spawn_file_actions_destroy(&action);
+
+ if (status != 0) {
+ close(fds[0]);
+ close(fds[1]);
+ return -1;
+ }
+
+ close(fds[0]);
+
+ fd = receive_fd(fds[1]);
+ if (fd < 0) {
+ close(fds[1]);
+ waitpid(pid, NULL, 0);
+ return -1;
+ }
+
+ fcntl(fd, F_SETFD, FD_CLOEXEC);
+
+ /* Return socket fd for later signaling */
+ *sock_fd_out = fds[1];
+ *pid_out = pid;
+
+ return fd;
+}
+
+/*
+ * Send proceed signal to fusermount3 and wait for mount result
+ * Returns: 0 on success, -1 on failure
+ */
+int fuse_fusermount_proceed_mnt(int sock_fd)
+{
+ char buf = '\0';
+ ssize_t res;
+
+ /* Send proceed signal */
+ do {
+ res = send(sock_fd, &buf, 1, 0);
+ } while (res == -1 && errno == EINTR);
+
+ if (res != 1) {
+ fuse_log(FUSE_LOG_ERR, "fuse: failed to send proceed signal: %s\n",
+ strerror(errno));
+ return -1;
+ }
+
+ /* Wait for mount result from fusermount3 (4-byte error code) */
+ int32_t status;
+
+ do {
+ res = recv(sock_fd, &status, sizeof(status), 0);
+ } while (res == -1 && errno == EINTR);
+
+ if (res != sizeof(status)) {
+ if (res == 0)
+ fuse_log(FUSE_LOG_ERR, "fuse: fusermount3 closed connection\n");
+ else
+ fuse_log(FUSE_LOG_ERR, "fuse: failed to receive mount status: %s\n",
+ strerror(errno));
+ return -1;
+ }
+
+ if (status != 0) {
+ if (status != -EPERM)
+ fuse_log(FUSE_LOG_ERR, "fuse: fusermount3 mount failed: %s\n",
+ strerror(-status));
+ return -1;
+ }
+
+ return 0;
+}
+
#ifndef O_CLOEXEC
#define O_CLOEXEC 0
#endif
diff --git a/lib/mount_i_linux.h b/lib/mount_i_linux.h
index 867105019fa57576682091d1a650302f31e450b3..2a3386443afa73a508454124332527c35a184398 100644
--- a/lib/mount_i_linux.h
+++ b/lib/mount_i_linux.h
@@ -76,5 +76,12 @@ int fuse_kern_fsmount_mo(const char *mnt, struct mount_opts *mo,
const char *mnt_opts);
char *fuse_mnt_build_source(const struct mount_opts *mo);
char *fuse_mnt_build_type(const struct mount_opts *mo);
+int mount_fusermount_obtain_fd(const char *mountpoint,
+ struct mount_opts *mo,
+ const char *opts, int *sock_fd_out,
+ pid_t *pid_out);
+
+int fuse_fusermount_proceed_mnt(int sock_fd);
+
#endif /* FUSE_MOUNT_I_H_ */
diff --git a/util/fusermount.c b/util/fusermount.c
index 808b4afd89ceb49273c944d43bffe5033e27549b..e6d973687bb4eef0d7e6626a1028cc32dd177e89 100644
--- a/util/fusermount.c
+++ b/util/fusermount.c
@@ -1397,7 +1397,6 @@ struct mount_context {
char *source;
char *mnt_opts;
char *x_opts;
- const char *type;
};
/*
@@ -1521,7 +1520,6 @@ static int mount_fuse_finish_fsmount(const char *mnt, const char *opts,
/* Store results in context */
ctx->source = mp.source;
- ctx->type = mp.type;
ctx->mnt_opts = final_mnt_opts;
*type = mp.type;
--
2.43.0
^ permalink raw reply related [flat|nested] 59+ messages in thread
* Re: [PATCH 02/19] Add 'STRCPY' to the checkpatch ignore option
2026-03-23 17:44 ` [PATCH 02/19] Add 'STRCPY' to the checkpatch ignore option Bernd Schubert
@ 2026-03-23 21:03 ` Darrick J. Wong
0 siblings, 0 replies; 59+ messages in thread
From: Darrick J. Wong @ 2026-03-23 21:03 UTC (permalink / raw)
To: Bernd Schubert
Cc: linux-fsdevel, Miklos Szeredi, Joanne Koong, Bernd Schubert
On Mon, Mar 23, 2026 at 06:44:57PM +0100, Bernd Schubert wrote:
> From: Bernd Schubert <bschubert@ddn.com>
>
> checkpatch.pl is a kernel copy and the strcpy warning is not valid
> for libfuse.
>
> Signed-off-by: Bernd Schubert <bschubert@ddn.com>
I agree with this, having been bonked on the nose for it before :)
Reviewed-by: "Darrick J. Wong" <djwong@kernel.org>
--D
> ---
> .github/workflows/checkpatch.yml | 2 +-
> 1 file changed, 1 insertion(+), 1 deletion(-)
>
> diff --git a/.github/workflows/checkpatch.yml b/.github/workflows/checkpatch.yml
> index 4a98c91cddeab686c782a1d34cd5d15f86f0b42d..8dead95b065bc81449a98fa7ea94fac7463d2334 100644
> --- a/.github/workflows/checkpatch.yml
> +++ b/.github/workflows/checkpatch.yml
> @@ -35,7 +35,7 @@ jobs:
> fi
> subject=$(git log -1 --format=%s $commit)
> echo "Checking commit: $commit - $subject"
> - if ! ./checkpatch.pl --max-line-length=100 --no-tree --ignore MAINTAINERS,SPDX_LICENSE_TAG,COMMIT_MESSAGE,FILE_PATH_CHANGES,EMAIL_SUBJECT,AVOID_EXTERNS,GIT_COMMIT_ID,ENOSYS_SYSCALL,ENOSYS,FROM_SIGN_OFF_MISMATCH,QUOTED_COMMIT_ID,PREFER_ATTRIBUTE_ALWAYS_UNUSED,PREFER_DEFINED_ATTRIBUTE_MACRO -g $commit; then
> + if ! ./checkpatch.pl --max-line-length=100 --no-tree --ignore MAINTAINERS,SPDX_LICENSE_TAG,COMMIT_MESSAGE,FILE_PATH_CHANGES,EMAIL_SUBJECT,AVOID_EXTERNS,GIT_COMMIT_ID,ENOSYS_SYSCALL,ENOSYS,FROM_SIGN_OFF_MISMATCH,QUOTED_COMMIT_ID,PREFER_ATTRIBUTE_ALWAYS_UNUSED,PREFER_DEFINED_ATTRIBUTE_MACRO,STRCPY -g $commit; then
> echo "checkpatch.pl found issues in commit $commit - $subject"
> exit 1
> fi
>
> --
> 2.43.0
>
>
^ permalink raw reply [flat|nested] 59+ messages in thread
* Re: [PATCH 03/19] checkpatch.pl: Add _Atomic to $Attribute patttern
2026-03-23 17:44 ` [PATCH 03/19] checkpatch.pl: Add _Atomic to $Attribute patttern Bernd Schubert
@ 2026-03-23 21:09 ` Darrick J. Wong
0 siblings, 0 replies; 59+ messages in thread
From: Darrick J. Wong @ 2026-03-23 21:09 UTC (permalink / raw)
To: Bernd Schubert
Cc: linux-fsdevel, Miklos Szeredi, Joanne Koong, Bernd Schubert
On Mon, Mar 23, 2026 at 06:44:58PM +0100, Bernd Schubert wrote:
> From: Bernd Schubert <bschubert@ddn.com>
>
> WARNING: Missing a blank line after declarations
> + int init_error;
> + _Atomic bool terminate_mount_worker;
>
> This change should go to linux upstream.
>
> Signed-off-by: Bernd Schubert <bschubert@ddn.com>
That seems fine to me, dunno if the kernel people will accept it.
But for here it's certainly ok since libfuse uses _Atomic variables.
Acked-by: "Darrick J. Wong" <djwong@kernel.org>
--D
> ---
> checkpatch.pl | 3 ++-
> 1 file changed, 2 insertions(+), 1 deletion(-)
>
> diff --git a/checkpatch.pl b/checkpatch.pl
> index 9eed3683ad76caffbbb2418e5dbea7551d374406..2d44e0e6edac303731c7637de00ed35d7b1dfcb7 100755
> --- a/checkpatch.pl
> +++ b/checkpatch.pl
> @@ -520,7 +520,8 @@ our $Attribute = qr{
> ____cacheline_aligned_in_smp|
> ____cacheline_internodealigned_in_smp|
> __weak|
> - __alloc_size\s*\(\s*\d+\s*(?:,\s*\d+\s*)?\)
> + __alloc_size\s*\(\s*\d+\s*(?:,\s*\d+\s*)?\)|
> + _Atomic
> }x;
> our $Modifier;
> our $Inline = qr{inline|__always_inline|noinline|__inline|__inline__};
>
> --
> 2.43.0
>
>
^ permalink raw reply [flat|nested] 59+ messages in thread
* Re: [PATCH 05/19] Sync fuse_kernel.h with linux-6.18
2026-03-23 17:45 ` [PATCH 05/19] Sync fuse_kernel.h with linux-6.18 Bernd Schubert
@ 2026-03-23 21:16 ` Darrick J. Wong
0 siblings, 0 replies; 59+ messages in thread
From: Darrick J. Wong @ 2026-03-23 21:16 UTC (permalink / raw)
To: Bernd Schubert
Cc: linux-fsdevel, Miklos Szeredi, Joanne Koong, Bernd Schubert
On Mon, Mar 23, 2026 at 06:45:00PM +0100, Bernd Schubert wrote:
> From: Bernd Schubert <bschubert@ddn.com>
>
> Signed-off-by: Bernd Schubert <bschubert@ddn.com>
Reviewed-by: "Darrick J. Wong" <djwong@kernel.org>
--D
> ---
> include/fuse_kernel.h | 1 +
> 1 file changed, 1 insertion(+)
>
> diff --git a/include/fuse_kernel.h b/include/fuse_kernel.h
> index f0dee3d6cf51b0eb83104c1f30f70b8ba9d710cd..c13e1f9a2f12bd39f535188cb5466688eba42263 100644
> --- a/include/fuse_kernel.h
> +++ b/include/fuse_kernel.h
> @@ -1138,6 +1138,7 @@ struct fuse_backing_map {
> #define FUSE_DEV_IOC_BACKING_OPEN _IOW(FUSE_DEV_IOC_MAGIC, 1, \
> struct fuse_backing_map)
> #define FUSE_DEV_IOC_BACKING_CLOSE _IOW(FUSE_DEV_IOC_MAGIC, 2, uint32_t)
> +#define FUSE_DEV_IOC_SYNC_INIT _IO(FUSE_DEV_IOC_MAGIC, 3)
>
> struct fuse_lseek_in {
> uint64_t fh;
>
> --
> 2.43.0
>
^ permalink raw reply [flat|nested] 59+ messages in thread
* Re: [PATCH 04/19] Add a new daemonize API
2026-03-23 17:44 ` [PATCH 04/19] Add a new daemonize API Bernd Schubert
@ 2026-03-23 22:28 ` Darrick J. Wong
2026-03-24 17:36 ` Bernd Schubert
0 siblings, 1 reply; 59+ messages in thread
From: Darrick J. Wong @ 2026-03-23 22:28 UTC (permalink / raw)
To: Bernd Schubert; +Cc: linux-fsdevel, Miklos Szeredi, Joanne Koong
On Mon, Mar 23, 2026 at 06:44:59PM +0100, Bernd Schubert wrote:
> In complex fuse file systems one often wants
> a) fork
> b) start extra threads and system initialization (like network
> connection and RDMA memory registration).
> c) Start the fuse session
>
> With fuse_daemonize() there is no way to return a notification
> if step b) or c) failed. Therefore exising examples do
> the fuse_daemonize() after fuse_session_mount().
> Step, i.e. starting extra threads and possible failures are
> do not exist in these examples - unhandled use case.
Er, this sentence is a little garbled. Is this supposed to say "The
example servers do not cover step b. This is an un-demonstrated use
case"?
> With the FUSE_SYNC_INIT feature, which does FUSE_INIT at mount time
> and not after the mount anymore, it becomes even more complex
> as FUSE_INIT triggers startup of fuse_io_uring threads. That
> means for FUSE_SYNC_INIT forking/daemonization has to be done
> _before_ the fuse_session_mount().
Hey, that /is/ a neat trick!
> A new API is introduced to overcome the limitations of
> fuse_daemonize()
>
> fuse_daemonize_start() - fork, but foreground process does not
> terminate yet and watches the background.
>
> fuse_daemonize_signal() - background daemon signals to
> the foreground process success or failure.
>
> fuse_daemonize_active() - helper function for the high level
> interface, which needs to handle both APIs. fuse_daemonize()
> is called within fuse_main(), which now needs to know if the caller
> actually already used the new API itself.
>
> The object 'struct fuse_daemonize *' is allocated dynamically
> and stored a global variable in fuse_daemonize.c, because
> - high level fuse_main_real_versioned() needs to know
> if already daemonized
> - high level daemons do not have access to struct fuse_session
> - FUSE_SYNC_INIT in later commits can only be done if the new
> API (or a file system internal) daemonization is used.
>
> Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
> ---
> doc/README.daemonize | 186 +++++++++++++++++++++++++++++
> example/passthrough_hp.cc | 18 ++-
> include/fuse_daemonize.h | 71 +++++++++++
> include/meson.build | 3 +-
> lib/fuse_daemonize.c | 284 ++++++++++++++++++++++++++++++++++++++++++++
> lib/fuse_i.h | 4 +-
> lib/fuse_lowlevel.c | 1 +
> lib/fuse_versionscript | 3 +
> lib/helper.c | 13 +-
> lib/meson.build | 3 +-
> test/test_want_conversion.c | 1 +
> 11 files changed, 576 insertions(+), 11 deletions(-)
>
> diff --git a/doc/README.daemonize b/doc/README.daemonize
> new file mode 100644
> index 0000000000000000000000000000000000000000..f7473361b3a39369291e9401dfc5f7928ff4160b
> --- /dev/null
> +++ b/doc/README.daemonize
> @@ -0,0 +1,186 @@
> +FUSE Daemonization API
> +======================
> +
> +This document describes the FUSE daemonization APIs, including the legacy
> +fuse_daemonize() function and the new fuse_daemonize_start()/signal() API
> +introduced in libfuse 3.19.
> +
> +
> +Overview
> +--------
> +
> +FUSE filesystems often need to run as background daemons. Daemonization
> +involves forking the process, creating a new session, and redirecting
> +standard file descriptors. The challenge is properly reporting initialization
> +failures to the parent process.
Yeah, I found this part pretty murky when writing fuse[24]fs -- the
internal library failures are fuse_log()'d, but then what if the fuse
server has its own logging library? libext2fs has its own, and there's
rather a lot of shenanigans that go on to make sure that libfuse and
libext2fs print to the same streams.
If you want to run as a systemd service, stdout/stderr are usually
routed to journald so there's no need to mess with stdout/stderr. But
then if you're running as a system service you don't want daemonize
anyway.
Ok enough rambling about my thing. I appreciate you documenting the old
fuse_daemonize in detail:
> +Old API: fuse_daemonize()
> +--------------------------
> +
> +Function signature:
> + int fuse_daemonize(int foreground);
> +
> +Location: lib/helper.c
> +
> +This is the legacy daemonization API, primarily used with the high-level
> +fuse_main() interface.
> +
> +Behavior:
> +- If foreground=0: forks the process, creates new session, redirects stdio
It creates a new session? I see that a pipe gets created between parent
and child, but I don't see a new fuse_session or ... oh, you meant a new
*Unix* session via setsid(). Can you say "creates new Unix session"?
> +- If foreground=1: only changes directory to "/"
> +- Parent waits for a single byte on a pipe before exiting
> +- Child writes completion byte immediately after redirecting stdio
> +- Always changes directory to "/"
> +
> +Limitations:
> +1. No failure reporting: The parent receives notification immediately after
> + fork/setsid, before any meaningful initialization (like mounting the
> + filesystem or starting threads).
<nod> and I gather that's what fuse_daemonize_signal() is for.
> +2. Timing constraint: Must be called AFTER fuse_session_mount() in existing
> + examples, because there's no way to report mount failures to the parent.
> +
> +3. Thread initialization: Cannot report failures from complex initialization
> + steps like:
> + - Starting worker threads
Where do those errors go? It looks like by default they go to stderr,
which is /dev/null after daemonization, right?
> + - Network connection setup
> + - RDMA memory registration
> + - Resource allocation
> +
> +4. FUSE_SYNC_INIT incompatibility: With the FUSE_SYNC_INIT feature, FUSE_INIT
> + happens at mount time and may start io_uring threads. This requires
> + daemonization BEFORE mount, which the old API cannot handle properly.
(For anyone following at home, fuse-containers cannot use synchronous
init because the new fuservicemount helper opens /dev/fuse and calls
mount() at the behest of the fuse server, so I'm mostly interested in
this as an exercise in getting to know libfuse.)
> +
> +Example usage (old API):
> + fuse = fuse_new(...);
> + fuse_mount(fuse, mountpoint);
> + fuse_daemonize(opts.foreground); // After mount, can't report mount failure
> + fuse_set_signal_handlers(se);
> + fuse_session_loop(se);
> +
> +
> +New API: fuse_daemonize_start() / fuse_daemonize_signal()
> +----------------------------------------------------------
> +
> +Functions:
> + int fuse_daemonize_start(unsigned int flags);
> + void fuse_daemonize_signal(int status);
> + bool fuse_daemonize_active(void);
> +
> +Location: lib/fuse_daemonize.c, include/fuse_daemonize.h
> +Available since: libfuse 3.19
> +
> +This new API solves the limitations of fuse_daemonize() by splitting
> +daemonization into two phases:
> +
> +1. fuse_daemonize_start() - Fork and setup, but parent waits
> +2. fuse_daemonize_signal() - Signal success/failure to parent
> +
> +
> +fuse_daemonize_start()
> +----------------------
> +
> +Flags:
> +- FUSE_DAEMONIZE_NO_CHDIR: Don't change directory to "/"
> +- FUSE_DAEMONIZE_NO_BACKGROUND: Don't fork (foreground mode)
> +
> +Behavior:
> +- Unless NO_BACKGROUND: forks the process
> +- Parent waits for status signal from child
> +- Child creates new session and continues
> +- Unless NO_CHDIR: changes directory to "/"
> +- Closes stdin immediately in child
> +- Starts a watcher thread to detect parent death
> +- Returns 0 in child on success, negative errno on error
> +
> +Parent death detection:
> +- Uses a "death pipe" - parent keeps write end open
> +- Child's watcher thread polls the read end
> +- When parent dies, pipe gets POLLHUP and child exits
> +- Prevents orphaned daemons if parent is killed
> +
> +
> +fuse_daemonize_signal()
> +-----------------------
> +
> +Status values:
> +- FUSE_DAEMONIZE_SUCCESS (0): Initialization succeeded
> +- FUSE_DAEMONIZE_FAILURE (1): Initialization failed
> +
> +Behavior:
> +- Signals the parent process with success or failure status
Does this get written back through the death pipe? Or does it use
process signals?
> +- Parent exits with EXIT_SUCCESS or EXIT_FAILURE accordingly
> +- On success: redirects stdout/stderr to /dev/null
> +- Stops the parent watcher thread
> +- Cleans up pipes and internal state
I think these might be better called fuse_daemonize_success() and
fuse_daemonize_fail() since one of them does more than just signal the
parent process.
> +- Safe to call multiple times
> +- Safe to call even if fuse_daemonize_start() failed
> +
> +
> +fuse_daemonize_active()
> +-----------------------
> +
> +Returns true if daemonization is active and waiting for signal.
> +
> +Used by the high-level fuse_main() to detect if the application already
> +called the new API, avoiding double-daemonization.
> +
> +
> +Example usage (new API):
> +-------------------------
> +
> + // Start daemonization BEFORE mount
> + unsigned int daemon_flags = 0;
> + if (foreground)
> + daemon_flags |= FUSE_DAEMONIZE_NO_BACKGROUND;
> +
> + if (fuse_daemonize_start(daemon_flags) != 0)
> + goto error;
> +
> + // Mount can now fail and be reported to parent
> + if (fuse_session_mount(se, mountpoint) != 0)
> + goto error_signal;
> +
> + // Complex initialization can fail and be reported
> + if (setup_threads() != 0)
> + goto error_signal;
> +
> + if (setup_network() != 0)
> + goto error_signal;
Hrmm, interesting, this solves the problem of fuse2fs having to wait
until FUSE_INIT to create background threads.
> +
> + // Signal success - parent exits with EXIT_SUCCESS
> + // This is typically done in the init() callback after FUSE_INIT
> + fuse_daemonize_signal(FUSE_DAEMONIZE_SUCCESS);
> +
> + // Run main loop
> + fuse_session_loop(se);
> +
> + return 0;
> +
> +error_signal:
> + // Signal failure - parent exits with EXIT_FAILURE
> + fuse_daemonize_signal(FUSE_DAEMONIZE_FAILURE);
> +error:
> + return 1;
> +
> +
> +When to signal success
> +----------------------
> +
> +The success signal should be sent after all critical initialization is
> +complete. For FUSE filesystems, this is typically in the init() callback,
> +after FUSE_INIT has been processed successfully.
> +
> +Example (from passthrough_hp.cc):
> + static void sfs_init(void *userdata, fuse_conn_info *conn) {
> + // ... initialization ...
> + fuse_daemonize_signal(FUSE_DAEMONIZE_SUCCESS);
> + }
> +
> +This ensures the parent only exits after:
> +- Mount succeeded
> +- FUSE_INIT completed
> +- All threads started
> +- Filesystem is ready to serve requests
Very nice! Does this new daemonization work for async-init servers?
> diff --git a/example/passthrough_hp.cc b/example/passthrough_hp.cc
> index 9f795c5546ee8312e1393c5b8fcebfd77724fb49..a0da5c7e6ab07f921df65db9c700e7de77fe1598 100644
> --- a/example/passthrough_hp.cc
> +++ b/example/passthrough_hp.cc
> @@ -55,6 +55,7 @@
> #include <errno.h>
> #include <ftw.h>
> #include <fuse_lowlevel.h>
> +#include <fuse_daemonize.h>
> #include <inttypes.h>
> #include <string.h>
> #include <sys/file.h>
> @@ -243,6 +244,9 @@ static void sfs_init(void *userdata, fuse_conn_info *conn)
>
> /* Try a large IO by default */
> conn->max_write = 4 * 1024 * 1024;
> +
> + /* Signal successful init to parent */
> + fuse_daemonize_signal(FUSE_DAEMONIZE_SUCCESS);
> }
>
> static void sfs_getattr(fuse_req_t req, fuse_ino_t ino, fuse_file_info *fi)
> @@ -1580,6 +1584,7 @@ int main(int argc, char *argv[])
> {
> struct fuse_loop_config *loop_config = NULL;
> void *teardown_watchog = NULL;
> + unsigned int daemon_flags = 0;
>
> // Parse command line options
> auto options{ parse_options(argc, argv) };
> @@ -1638,10 +1643,14 @@ int main(int argc, char *argv[])
>
> fuse_loop_cfg_set_clone_fd(loop_config, fs.clone_fd);
>
> - if (fuse_session_mount(se, argv[2]) != 0)
> + /* Start daemonization before mount so parent can report mount failure */
> + if (fs.foreground)
> + daemon_flags |= FUSE_DAEMONIZE_NO_BACKGROUND;
> + if (fuse_daemonize_start(daemon_flags) != 0)
> goto err_out3;
>
> - fuse_daemonize(fs.foreground);
> + if (fuse_session_mount(se, argv[2]) != 0)
> + goto err_out4;
>
> if (!fs.foreground)
> fuse_log_enable_syslog("passthrough-hp", LOG_PID | LOG_CONS,
> @@ -1650,7 +1659,7 @@ int main(int argc, char *argv[])
> teardown_watchog = fuse_session_start_teardown_watchdog(
> se, fs.root.stop_timeout_secs, NULL, NULL);
> if (teardown_watchog == NULL)
> - goto err_out3;
> + goto err_out4;
>
> if (options.count("single"))
> ret = fuse_session_loop(se);
> @@ -1659,6 +1668,9 @@ int main(int argc, char *argv[])
>
> fuse_session_unmount(se);
>
> +err_out4:
> + if (fuse_daemonize_active())
> + fuse_daemonize_signal(FUSE_DAEMONIZE_FAILURE);
> err_out3:
> fuse_remove_signal_handlers(se);
> err_out2:
> diff --git a/include/fuse_daemonize.h b/include/fuse_daemonize.h
> new file mode 100644
> index 0000000000000000000000000000000000000000..f49f6aa580a93547198e7dabe65498708e65d024
> --- /dev/null
> +++ b/include/fuse_daemonize.h
> @@ -0,0 +1,71 @@
> +/*
> + * FUSE: Filesystem in Userspace
> + * Copyright (C) 2026 Bernd Schubert <bsbernd.com>
> + *
> + * This program can be distributed under the terms of the GNU LGPLv2.
> + * See the file COPYING.LIB.
> + *
> + */
> +
> +#ifndef FUSE_DAEMONIZE_H_
> +#define FUSE_DAEMONIZE_H_
> +
> +#include <stdint.h>
> +#include <stdbool.h>
> +
> +#ifdef __cplusplus
> +extern "C" {
> +#endif
> +
> +/**
> + * Flags for fuse_daemonize_start()
> + */
> +#define FUSE_DAEMONIZE_NO_CHDIR (1 << 0)
> +#define FUSE_DAEMONIZE_NO_BACKGROUND (1 << 1)
> +
> +/**
> + * Status values for fuse_daemonize_signal()
> + */
> +#define FUSE_DAEMONIZE_SUCCESS 0
> +#define FUSE_DAEMONIZE_FAILURE 1
What if fuse_daemonize_signal() took an exitcode and passed it directly
to the parent process via the death pipe, and the parent process can
pass that to exit()? IOWs, what if fuse_daemonize_signal writes @status
into signal_pipe_wr instead of flattening it to EXIT_SUCCESS/FAILURE?
Or, if you want to constrain the values to binary, then why not use
#define FUSE_DAEMONIZE_SUCCESS (EXIT_SUCCESS) ?
> +
> +/**
> + * Start daemonization process.
> + *
> + * Unless FUSE_DAEMONIZE_NO_BACKGROUND is set, this forks the process.
> + * The parent waits for a signal from the child via fuse_daemonize_signal().
> + * The child returns from this call and continues setup.
> + *
> + * Unless FUSE_DAEMONIZE_NO_CHDIR is set, changes directory to "/".
> + *
> + * Must be called before fuse_session_mount().
> + *
> + * @param flags combination of FUSE_DAEMONIZE_* flags
> + * @return 0 on success, negative errno on error
> + */
> +int fuse_daemonize_start(unsigned int flags);
> +
> +/**
> + * Signal daemonization status to parent and cleanup.
> + *
> + * The child calls this after setup is complete (or failed).
> + * The parent receives the status and exits with it.
> + * Safe to call multiple times or if start failed.
> + *
> + * @param status FUSE_DAEMONIZE_SUCCESS or FUSE_DAEMONIZE_FAILURE
> + */
> +void fuse_daemonize_signal(int status);
> +
> +/**
> + * Check if daemonization is active and waiting for signal.
> + *
> + * @return true if active, false otherwise
> + */
> +bool fuse_daemonize_active(void);
> +
> +#ifdef __cplusplus
> +}
> +#endif
> +
> +#endif /* FUSE_DAEMONIZE_H_ */
> +
> diff --git a/include/meson.build b/include/meson.build
> index bf671977a5a6a9142bd67aceabd8a919e3d968d0..cfbaf52ac5d84369e92948c631e2fcfdd04ac2eb 100644
> --- a/include/meson.build
> +++ b/include/meson.build
> @@ -1,4 +1,5 @@
> libfuse_headers = [ 'fuse.h', 'fuse_common.h', 'fuse_lowlevel.h',
> - 'fuse_opt.h', 'cuse_lowlevel.h', 'fuse_log.h' ]
> + 'fuse_opt.h', 'cuse_lowlevel.h', 'fuse_log.h',
> + 'fuse_daemonize.h' ]
>
> install_headers(libfuse_headers, subdir: 'fuse3')
> diff --git a/lib/fuse_daemonize.c b/lib/fuse_daemonize.c
> new file mode 100644
> index 0000000000000000000000000000000000000000..5d191e7d737d04876d02ef6bd526061c003d2ab7
> --- /dev/null
> +++ b/lib/fuse_daemonize.c
> @@ -0,0 +1,284 @@
> +/*
> + * FUSE: Filesystem in Userspace
> + * Copyright (C) 2026 Bernd Schubert <bsbernd.com>
> + *
> + * This program can be distributed under the terms of the GNU LGPLv2.
> + * See the file COPYING.LIB.
> + */
> +
> +#define _GNU_SOURCE
> +
> +#include "fuse_daemonize.h"
> +
> +#include <fcntl.h>
> +#include <poll.h>
> +#include <pthread.h>
> +#include <stdatomic.h>
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <string.h>
> +#include <sys/types.h>
> +#include <unistd.h>
> +#include <stdbool.h>
> +#include <errno.h>
> +
> +/* Private/internal data */
> +struct fuse_daemonize {
> + unsigned int flags;
> + int signal_pipe_wr; /* write end for signaling parent */
Hrm. Ok, so signal_pipe[2] is the pipe through which the child writes
an int back to the parent, and then the parent can convey that outcome
to whatever started the parent.
> + int death_pipe_rd; /* read end, POLLHUP when parent dies */
death_pipe[2] is a different pipe. The parent closes its end of the
pipe and the child treats POLLHUP as a message that the parent died.
> + int stop_pipe_rd; /* read end for stop signal */
> + int stop_pipe_wr; /* write end for stop signal */
and this third pipe exist so that the child can wake its own "parent
watcher" thread and have it abort.
> + pthread_t watcher;
> + int watcher_started;
Hm. So watcher_started is initialized to 0 in the parent, gets set to 1
in the child's copy of memory when it starts the parent-watcher thread,
and later becomes zero when the child shuts down the parent-watcher
thread.
> + _Atomic bool active;
active is set in the parent process, copied to the child process, and
cleared in fuse_daemonize_signal in the child.
> + _Atomic bool daemonized;
and daemonized is set in the child upon creation of the child; and
read by fuse_daemonize_signal. The parent never accesses its copy of
the variable.
> +};
> +
> +/* Global daemonization object pointer */
> +static _Atomic(struct fuse_daemonize *) daemonize;
> +
> +/* Watcher thread: polls for parent death or stop signal */
> +static void *parent_watcher_thread(void *arg)
> +{
> + struct fuse_daemonize *di = arg;
> + struct pollfd pfd[2];
> +
> + pfd[0].fd = di->death_pipe_rd;
> + pfd[0].events = POLLIN;
> + pfd[1].fd = di->stop_pipe_rd;
> + pfd[1].events = POLLIN;
> +
> + while (1) {
> + int rc = poll(pfd, 2, -1);
> +
> + if (rc < 0)
> + continue;
> +
> + /* Parent died - death pipe write end closed */
> + if (pfd[0].revents & (POLLHUP | POLLERR))
> + _exit(EXIT_FAILURE);
> +
> + /* Stop signal received */
> + if (pfd[1].revents & POLLIN)
> + break;
> + }
> + return NULL;
> +}
> +
> +static int start_parent_watcher(struct fuse_daemonize *daemonize)
> +{
> + int rc;
> +
> + rc = pthread_create(&daemonize->watcher, NULL, parent_watcher_thread,
> + daemonize);
> + if (rc != 0) {
> + perror("fuse_daemonize: pthread_create");
pthread functions return positive error numbers and do not set errno, so
you can't use perror().
> + return -1;
> + }
> + daemonize->watcher_started = 1;
Isn't this a bool value?
FWIW the rest of the logic below looks correct, though I think the
daemonize object itself would need a pthread_mutex_t to coordinate
access if it's possible or desirable for multiple threads to access it.
I think that's not the case, and any fuse server that did need that
could implement the locking on its own.
--D
> + return 0;
> +}
> +
> +static void stop_parent_watcher(struct fuse_daemonize *daemonize)
> +{
> + char byte = 0;
> +
> + if (daemonize && daemonize->watcher_started) {
> + /* Signal watcher to stop */
> + if (write(daemonize->stop_pipe_wr, &byte, 1) != 1)
> + perror("fuse_daemonize: stop write");
> + pthread_join(daemonize->watcher, NULL);
> + daemonize->watcher_started = 0;
> + }
> +}
> +
> +static int daemonize_child(struct fuse_daemonize *daemonize)
> +{
> + int stop_pipe[2];
> +
> + if (pipe(stop_pipe) == -1) {
> + perror("fuse_daemonize_start: stop pipe");
> + return -1;
> + }
> + daemonize->stop_pipe_rd = stop_pipe[0];
> + daemonize->stop_pipe_wr = stop_pipe[1];
> +
> + if (setsid() == -1) {
> + perror("fuse_daemonize_start: setsid");
> + goto err_close_stop;
> + }
> +
> + /* Close stdin immediately */
> + int nullfd = open("/dev/null", O_RDWR, 0);
> +
> + if (nullfd != -1) {
> + (void)dup2(nullfd, 0);
> + if (nullfd > 0)
> + close(nullfd);
> + }
> +
> + /* Start watcher thread to detect parent death */
> + if (start_parent_watcher(daemonize) != 0)
> + goto err_close_stop;
> +
> + daemonize->daemonized = true;
> + return 0;
> +
> +err_close_stop:
> + close(daemonize->stop_pipe_rd);
> + close(daemonize->stop_pipe_wr);
> + return -1;
> +}
> +
> +/* Fork and daemonize. Returns 0 in child, never returns in parent. */
> +static int do_daemonize(struct fuse_daemonize *daemonize)
> +{
> + int signal_pipe[2];
> + int death_pipe[2];
> +
> + if (pipe(signal_pipe) == -1) {
> + perror("fuse_daemonize_start: signal pipe");
> + return -1;
> + }
> +
> + if (pipe(death_pipe) == -1) {
> + perror("fuse_daemonize_start: death pipe");
> + close(signal_pipe[0]);
> + close(signal_pipe[1]);
> + return -1;
> + }
> +
> + switch (fork()) {
> + case -1:
> + perror("fuse_daemonize_start: fork");
> + close(signal_pipe[0]);
> + close(signal_pipe[1]);
> + close(death_pipe[0]);
> + close(death_pipe[1]);
> + return -1;
> +
> + case 0:
> + /* Child: signal write end, death read end */
> + close(signal_pipe[0]);
> + close(death_pipe[1]);
> + daemonize->signal_pipe_wr = signal_pipe[1];
> + daemonize->death_pipe_rd = death_pipe[0];
> + return daemonize_child(daemonize);
> +
> + default: {
> + /* Parent: signal read end, death write end (kept open) */
> + unsigned char status;
> + ssize_t res;
> +
> + close(signal_pipe[1]);
> + close(death_pipe[0]);
> +
> + res = read(signal_pipe[0], &status, sizeof(status));
> + close(signal_pipe[0]);
> + close(death_pipe[1]);
> +
> + if (res != sizeof(status))
> + _exit(EXIT_FAILURE);
> + _exit(status);
> + }
> + }
> +}
> +
> +int fuse_daemonize_start(unsigned int flags)
> +{
> + struct fuse_daemonize *dm;
> + struct fuse_daemonize *expected = NULL;
> +
> + dm = calloc(1, sizeof(*dm));
> + if (dm == NULL) {
> + fprintf(stderr, "%s: calloc failed\n", __func__);
> + return -ENOMEM;
> + }
> +
> + dm->flags = flags;
> + dm->signal_pipe_wr = -1;
> + dm->death_pipe_rd = -1;
> + dm->stop_pipe_rd = -1;
> + dm->stop_pipe_wr = -1;
> + dm->active = true;
> +
> + if (!(flags & FUSE_DAEMONIZE_NO_CHDIR))
> + (void)chdir("/");
> +
> + if (!(flags & FUSE_DAEMONIZE_NO_BACKGROUND)) {
> + if (do_daemonize(dm) != 0) {
> + free(dm);
> + return -errno;
> + }
> + }
> +
> + /* Set global pointer using CAS - fail if already set */
> + if (!atomic_compare_exchange_strong(&daemonize, &expected, dm)) {
> + fprintf(stderr, "%s: already active\n", __func__);
> + free(dm);
> + return -EEXIST;
> + }
> +
> + return 0;
> +}
> +
> +static void close_if_valid(int *fd)
> +{
> + if (*fd != -1) {
> + close(*fd);
> + *fd = -1;
> + }
> +}
> +
> +void fuse_daemonize_signal(int status)
> +{
> + struct fuse_daemonize *dm;
> + unsigned char st;
> +
> + dm = atomic_load(&daemonize);
> + if (dm == NULL || !dm->active)
> + return;
> +
> + dm->active = false;
> +
> + /* Stop watcher before signaling - parent will exit after this */
> + stop_parent_watcher(dm);
> +
> + /* Signal status to parent */
> + if (dm->signal_pipe_wr != -1) {
> + st = (status != 0) ? EXIT_FAILURE : EXIT_SUCCESS;
> + if (write(dm->signal_pipe_wr, &st, sizeof(st)) != sizeof(st))
> + fprintf(stderr, "%s: write failed\n", __func__);
> + }
> +
> + /* Redirect stdout/stderr to /dev/null on success */
> + if (status == 0 && dm->daemonized) {
> + int nullfd = open("/dev/null", O_RDWR, 0);
> +
> + if (nullfd != -1) {
> + (void)dup2(nullfd, 1);
> + (void)dup2(nullfd, 2);
> + if (nullfd > 2)
> + close(nullfd);
> + }
> + }
> +
> + close_if_valid(&dm->signal_pipe_wr);
> + close_if_valid(&dm->death_pipe_rd);
> + close_if_valid(&dm->stop_pipe_rd);
> + close_if_valid(&dm->stop_pipe_wr);
> +
> + /* Clear global pointer using CAS and free */
> + if (atomic_compare_exchange_strong(&daemonize, &dm, NULL))
> + free(dm);
> +}
> +
> +bool fuse_daemonize_active(void)
> +{
> + struct fuse_daemonize *dm = atomic_load(&daemonize);
> +
> + return dm != NULL && (dm->daemonized || dm->active);
> +}
> diff --git a/lib/fuse_i.h b/lib/fuse_i.h
> index 65d2f68f7f30918a3c3ee4d473796cb013428a8f..9e3c5dc5021e210a2778e975a37ab609af324010 100644
> --- a/lib/fuse_i.h
> +++ b/lib/fuse_i.h
> @@ -17,7 +17,6 @@
> #include <semaphore.h>
> #include <stdint.h>
> #include <stdbool.h>
> -#include <errno.h>
> #include <stdatomic.h>
>
> #define MIN(a, b) \
> @@ -110,6 +109,9 @@ struct fuse_session {
> /* true if reading requests from /dev/fuse are handled internally */
> bool buf_reallocable;
>
> + /* synchronous FUSE_INIT support */
> + bool want_sync_init;
> +
> /* io_uring */
> struct fuse_session_uring uring;
>
> diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c
> index 3234f0ce3b246a4c2c40dc0757177de91b6608b2..4a1c7c4c02a05706e077a7ea89fab3c81bfe22cd 100644
> --- a/lib/fuse_lowlevel.c
> +++ b/lib/fuse_lowlevel.c
> @@ -19,6 +19,7 @@
> #include "mount_util.h"
> #include "util.h"
> #include "fuse_uring_i.h"
> +#include "fuse_daemonize.h"
>
> #include <pthread.h>
> #include <stdatomic.h>
> diff --git a/lib/fuse_versionscript b/lib/fuse_versionscript
> index cce09610316f4b0b1d6836dd0e63686342b70037..f1765d39e13bc9b1f53e625b9a091c5fa53f5afd 100644
> --- a/lib/fuse_versionscript
> +++ b/lib/fuse_versionscript
> @@ -227,6 +227,9 @@ FUSE_3.19 {
> fuse_session_start_teardown_watchdog;
> fuse_session_stop_teardown_watchdog;
> fuse_lowlevel_notify_prune;
> + fuse_daemonize_start;
> + fuse_daemonize_signal;
> + fuse_daemonize_active;
> } FUSE_3.18;
>
> # Local Variables:
> diff --git a/lib/helper.c b/lib/helper.c
> index 5c13b93a473181f027eba01e0bfefd78875ede3e..e6ec74364d000a6e091e0596fc74954b11cc51ab 100644
> --- a/lib/helper.c
> +++ b/lib/helper.c
> @@ -15,6 +15,7 @@
> #include "fuse_misc.h"
> #include "fuse_opt.h"
> #include "fuse_lowlevel.h"
> +#include "fuse_daemonize.h"
> #include "mount_util.h"
>
> #include <stdio.h>
> @@ -352,17 +353,19 @@ int fuse_main_real_versioned(int argc, char *argv[],
> goto out1;
> }
>
> + struct fuse_session *se = fuse_get_session(fuse);
> if (fuse_mount(fuse,opts.mountpoint) != 0) {
> res = 4;
> goto out2;
> }
>
> - if (fuse_daemonize(opts.foreground) != 0) {
> - res = 5;
> - goto out3;
> + if (!fuse_daemonize_active()) {
> + /* Avoid daemonizing if we are already daemonized by the newer API */
> + if (fuse_daemonize(opts.foreground) != 0) {
> + res = 5;
> + goto out3;
> + }
> }
> -
> - struct fuse_session *se = fuse_get_session(fuse);
> if (fuse_set_signal_handlers(se) != 0) {
> res = 6;
> goto out3;
> diff --git a/lib/meson.build b/lib/meson.build
> index fcd95741c9d3748fa01d9ec52b417aca66745f26..5bd449ebffe7c9229df904d647d990c6c47f80b5 100644
> --- a/lib/meson.build
> +++ b/lib/meson.build
> @@ -2,7 +2,8 @@ libfuse_sources = ['fuse.c', 'fuse_i.h', 'fuse_loop.c', 'fuse_loop_mt.c',
> 'fuse_lowlevel.c', 'fuse_misc.h', 'fuse_opt.c',
> 'fuse_signals.c', 'buffer.c', 'cuse_lowlevel.c',
> 'helper.c', 'modules/subdir.c', 'mount_util.c',
> - 'fuse_log.c', 'compat.c', 'util.c', 'util.h' ]
> + 'fuse_log.c', 'compat.c', 'util.c', 'util.h',
> + 'fuse_daemonize.c' ]
>
> if host_machine.system().startswith('linux')
> libfuse_sources += [ 'mount.c' ]
> diff --git a/test/test_want_conversion.c b/test/test_want_conversion.c
> index db731edbfe1be8230ae16b422f798603b4a3bb82..48e6dd2dc6084425a0462bba000563c6083160be 100644
> --- a/test/test_want_conversion.c
> +++ b/test/test_want_conversion.c
> @@ -8,6 +8,7 @@
> #include <inttypes.h>
> #include <stdbool.h>
> #include <err.h>
> +#include <errno.h>
>
> static void print_conn_info(const char *prefix, struct fuse_conn_info *conn)
> {
>
> --
> 2.43.0
>
>
^ permalink raw reply [flat|nested] 59+ messages in thread
* Re: [PATCH 06/19] mount.c: Split fuse_mount_sys to prepare privileged sync FUSE_INIT
2026-03-23 17:45 ` [PATCH 06/19] mount.c: Split fuse_mount_sys to prepare privileged sync FUSE_INIT Bernd Schubert
@ 2026-03-23 22:34 ` Darrick J. Wong
0 siblings, 0 replies; 59+ messages in thread
From: Darrick J. Wong @ 2026-03-23 22:34 UTC (permalink / raw)
To: Bernd Schubert
Cc: linux-fsdevel, Miklos Szeredi, Joanne Koong, Bernd Schubert
On Mon, Mar 23, 2026 at 06:45:01PM +0100, Bernd Schubert wrote:
> From: Bernd Schubert <bschubert@ddn.com>
>
> Synchronous FUSE_INIT needs to spawn a worker thread to handle
> FUSE_INIT before starting the mount. For that it needs to have
> the device file descriptor - this is preparation work and
> fuse_mount_sys() is split into fuse_kern_mount_prepare()
> and fuse_kern_do_mount().
>
> Signed-off-by: Bernd Schubert <bschubert@ddn.com>
This looks like a reasonable slice-and-dice, though it's a noisier
change with the whitespace rearranging that goes on in the new
fuse_kern_mount_prepare function. Assuming that's checkpatch and
that you're ok with that,
Reviewed-by: "Darrick J. Wong" <djwong@kernel.org>
--D
> ---
> lib/mount.c | 88 ++++++++++++++++++++++++++++++++++++++++++++++++-------------
> 1 file changed, 69 insertions(+), 19 deletions(-)
>
> diff --git a/lib/mount.c b/lib/mount.c
> index 398bf9e7a86743ac3c99f0cc2975e8db92346013..6e404451cc9edc8e35434cc31f25612cfc4edca1 100644
> --- a/lib/mount.c
> +++ b/lib/mount.c
> @@ -506,13 +506,11 @@ static int fuse_mount_fusermount(const char *mountpoint, struct mount_opts *mo,
> #define O_CLOEXEC 0
> #endif
>
> -static int fuse_mount_sys(const char *mnt, struct mount_opts *mo,
> - const char *mnt_opts)
> +static int fuse_kern_mount_prepare(const char *mnt,
> + struct mount_opts *mo)
> {
> char tmp[128];
> const char *devname = getenv(FUSE_KERN_DEVICE_ENV) ?: "/dev/fuse";
> - char *source = NULL;
> - char *type = NULL;
> struct stat stbuf;
> int fd;
> int res;
> @@ -524,32 +522,58 @@ static int fuse_mount_sys(const char *mnt, struct mount_opts *mo,
>
> res = stat(mnt, &stbuf);
> if (res == -1) {
> - fuse_log(FUSE_LOG_ERR, "fuse: failed to access mountpoint %s: %s\n",
> - mnt, strerror(errno));
> + fuse_log(FUSE_LOG_ERR,
> + "fuse: failed to access mountpoint %s: %s\n", mnt,
> + strerror(errno));
> return -1;
> }
>
> fd = open(devname, O_RDWR | O_CLOEXEC);
> if (fd == -1) {
> if (errno == ENODEV || errno == ENOENT)
> - fuse_log(FUSE_LOG_ERR,
> + fuse_log(
> + FUSE_LOG_ERR,
> "fuse: device %s not found. Kernel module not loaded?\n",
> devname);
> else
> fuse_log(FUSE_LOG_ERR, "fuse: failed to open %s: %s\n",
> - devname, strerror(errno));
> + devname, strerror(errno));
> return -1;
> }
> if (!O_CLOEXEC)
> fcntl(fd, F_SETFD, FD_CLOEXEC);
>
> - snprintf(tmp, sizeof(tmp), "fd=%i,rootmode=%o,user_id=%u,group_id=%u",
> + snprintf(tmp, sizeof(tmp), "fd=%i,rootmode=%o,user_id=%u,group_id=%u",
> fd, stbuf.st_mode & S_IFMT, getuid(), getgid());
>
> res = fuse_opt_add_opt(&mo->kernel_opts, tmp);
> if (res == -1)
> goto out_close;
>
> + return fd;
> +
> +out_close:
> + close(fd);
> + return -1;
> +}
> +
> +/**
> + * Complete the mount operation with an already-opened fd
> + * @mnt: mountpoint
> + * @mo: mount options
> + * @mnt_opts: mount options to pass to the kernel
> + *
> + * Returns: 0 on success, -1 on failure, -2 if fusermount should be used
> + */
> +static int fuse_kern_do_mount(const char *mnt, struct mount_opts *mo,
> + const char *mnt_opts)
> +{
> + const char *devname = getenv(FUSE_KERN_DEVICE_ENV) ?: "/dev/fuse";
> + char *source = NULL;
> + char *type = NULL;
> + int res;
> +
> + res = -ENOMEM;
> source = malloc((mo->fsname ? strlen(mo->fsname) : 0) +
> (mo->subtype ? strlen(mo->subtype) : 0) +
> strlen(devname) + 32);
> @@ -593,10 +617,11 @@ static int fuse_mount_sys(const char *mnt, struct mount_opts *mo,
> if (mo->blkdev && errno == ENODEV &&
> !fuse_mnt_check_fuseblk())
> fuse_log(FUSE_LOG_ERR,
> - "fuse: 'fuseblk' support missing\n");
> + "fuse: 'fuseblk' support missing\n");
> else
> - fuse_log(FUSE_LOG_ERR, "fuse: mount failed: %s\n",
> - strerror(errno_save));
> + fuse_log(FUSE_LOG_ERR,
> + "fuse: mount failed: %s\n",
> + strerror(errno_save));
> }
>
> goto out_close;
> @@ -619,17 +644,35 @@ static int fuse_mount_sys(const char *mnt, struct mount_opts *mo,
> free(type);
> free(source);
>
> - return fd;
> + return 0;
>
> out_umount:
> umount2(mnt, 2); /* lazy umount */
> out_close:
> free(type);
> free(source);
> - close(fd);
> return res;
> }
>
> +static int fuse_mount_sys(const char *mnt, struct mount_opts *mo,
> + const char *mnt_opts)
> +{
> + int fd;
> + int res;
> +
> + fd = fuse_kern_mount_prepare(mnt, mo);
> + if (fd == -1)
> + return -1;
> +
> + res = fuse_kern_do_mount(mnt, mo, mnt_opts);
> + if (res) {
> + close(fd);
> + return res;
> + }
> +
> + return fd;
> +}
> +
> static int get_mnt_flag_opts(char **mnt_optsp, int flags)
> {
> int i;
> @@ -678,6 +721,17 @@ void destroy_mount_opts(struct mount_opts *mo)
> free(mo);
> }
>
> +static int fuse_kern_mount_get_base_mnt_opts(struct mount_opts *mo,
> + char **mnt_optsp)
> +{
> + if (get_mnt_flag_opts(mnt_optsp, mo->flags) == -1)
> + return -1;
> + if (mo->kernel_opts && fuse_opt_add_opt(mnt_optsp, mo->kernel_opts) == -1)
> + return -1;
> + if (mo->mtab_opts && fuse_opt_add_opt(mnt_optsp, mo->mtab_opts) == -1)
> + return -1;
> + return 0;
> +}
>
> int fuse_kern_mount(const char *mountpoint, struct mount_opts *mo)
> {
> @@ -685,11 +739,7 @@ int fuse_kern_mount(const char *mountpoint, struct mount_opts *mo)
> char *mnt_opts = NULL;
>
> res = -1;
> - if (get_mnt_flag_opts(&mnt_opts, mo->flags) == -1)
> - goto out;
> - if (mo->kernel_opts && fuse_opt_add_opt(&mnt_opts, mo->kernel_opts) == -1)
> - goto out;
> - if (mo->mtab_opts && fuse_opt_add_opt(&mnt_opts, mo->mtab_opts) == -1)
> + if (fuse_kern_mount_get_base_mnt_opts(mo, &mnt_opts) == -1)
> goto out;
>
> res = fuse_mount_sys(mountpoint, mo, mnt_opts);
>
> --
> 2.43.0
>
>
^ permalink raw reply [flat|nested] 59+ messages in thread
* Re: [PATCH 07/19] Add FUSE_MOUNT_FALLBACK_NEEDED define for -2 mount errors
2026-03-23 17:45 ` [PATCH 07/19] Add FUSE_MOUNT_FALLBACK_NEEDED define for -2 mount errors Bernd Schubert
@ 2026-03-23 22:36 ` Darrick J. Wong
2026-03-24 18:03 ` Bernd Schubert
0 siblings, 1 reply; 59+ messages in thread
From: Darrick J. Wong @ 2026-03-23 22:36 UTC (permalink / raw)
To: Bernd Schubert
Cc: linux-fsdevel, Miklos Szeredi, Joanne Koong, Bernd Schubert
On Mon, Mar 23, 2026 at 06:45:02PM +0100, Bernd Schubert wrote:
> From: Bernd Schubert <bschubert@ddn.com>
>
> Magic numbers in the code are not good - we better use a define.
>
> Signed-off-by: Bernd Schubert <bschubert@ddn.com>
> ---
> lib/fuse_i.h | 3 +++
> lib/mount.c | 6 +++---
> 2 files changed, 6 insertions(+), 3 deletions(-)
>
> diff --git a/lib/fuse_i.h b/lib/fuse_i.h
> index 9e3c5dc5021e210a2778e975a37ab609af324010..b4c1d3eef41010287f6c9555ec0b2442d904d192 100644
> --- a/lib/fuse_i.h
> +++ b/lib/fuse_i.h
> @@ -217,6 +217,9 @@ struct fuse_chan *fuse_chan_get(struct fuse_chan *ch);
> */
> void fuse_chan_put(struct fuse_chan *ch);
>
> +/* Special return value for mount functions to indicate fallback to fusermount3 is needed */
> +#define FUSE_MOUNT_FALLBACK_NEEDED -2
...and you might want to put that in parentheses to avoid weird
interactions elsewhere. Or maybe just make fuse_kern_do_mount return an
enum so that callers have to handle FUSE_MOUNT_{OK,FAIL,FALLBACK} and
can do so via switch statements?
--D
> +
> struct mount_opts *parse_mount_opts(struct fuse_args *args);
> void destroy_mount_opts(struct mount_opts *mo);
> void fuse_mount_version(void);
> diff --git a/lib/mount.c b/lib/mount.c
> index 6e404451cc9edc8e35434cc31f25612cfc4edca1..dec9d52274c13536648cacef959789f472c5682c 100644
> --- a/lib/mount.c
> +++ b/lib/mount.c
> @@ -563,7 +563,7 @@ out_close:
> * @mo: mount options
> * @mnt_opts: mount options to pass to the kernel
> *
> - * Returns: 0 on success, -1 on failure, -2 if fusermount should be used
> + * Returns: 0 on success, -1 on failure, FUSE_MOUNT_FALLBACK_NEEDED if fusermount should be used
> */
> static int fuse_kern_do_mount(const char *mnt, struct mount_opts *mo,
> const char *mnt_opts)
> @@ -611,7 +611,7 @@ static int fuse_kern_do_mount(const char *mnt, struct mount_opts *mo,
> * case try falling back to fusermount3
> */
> if (errno == EPERM) {
> - res = -2;
> + res = FUSE_MOUNT_FALLBACK_NEEDED;
> } else {
> int errno_save = errno;
> if (mo->blkdev && errno == ENODEV &&
> @@ -749,7 +749,7 @@ int fuse_kern_mount(const char *mountpoint, struct mount_opts *mo)
> umount2(mountpoint, MNT_DETACH); /* lazy umount */
> res = -1;
> }
> - } else if (res == -2) {
> + } else if (res == FUSE_MOUNT_FALLBACK_NEEDED) {
> if (mo->fusermount_opts &&
> fuse_opt_add_opt(&mnt_opts, mo->fusermount_opts) == -1)
> goto out;
>
> --
> 2.43.0
>
>
^ permalink raw reply [flat|nested] 59+ messages in thread
* Re: [PATCH 08/19] Refactor mount code / move common functions to mount_util.c
2026-03-23 17:45 ` [PATCH 08/19] Refactor mount code / move common functions to mount_util.c Bernd Schubert
@ 2026-03-23 22:40 ` Darrick J. Wong
0 siblings, 0 replies; 59+ messages in thread
From: Darrick J. Wong @ 2026-03-23 22:40 UTC (permalink / raw)
To: Bernd Schubert
Cc: linux-fsdevel, Miklos Szeredi, Joanne Koong, Bernd Schubert
On Mon, Mar 23, 2026 at 06:45:03PM +0100, Bernd Schubert wrote:
> From: Bernd Schubert <bschubert@ddn.com>
>
> Also create the new "mount_i.h", which is independent of the
> the rest of libfuse.
>
> This is preparation for the new mount API, which goes into a new file.
>
> Signed-off-by: Bernd Schubert <bschubert@ddn.com>
Hey, I want to use some of these new helpers too! :)
Reviewed-by: "Darrick J. Wong" <djwong@kernel.org>
Can we use asprintf for fuse_mnt_build_{source,type} though?
--D
> ---
> lib/fuse_i.h | 10 ++----
> lib/mount.c | 96 ++++++++++++++++++++++++++++------------------------
> lib/mount_bsd.c | 1 +
> lib/mount_common_i.h | 31 +++++++++++++++++
> lib/mount_i_linux.h | 32 ++++++++++++++++++
> lib/mount_util.c | 24 +++++++++++++
> lib/mount_util.h | 6 ++++
> 7 files changed, 148 insertions(+), 52 deletions(-)
>
> diff --git a/lib/fuse_i.h b/lib/fuse_i.h
> index b4c1d3eef41010287f6c9555ec0b2442d904d192..6d63c9fd2149eb4ae3b0e0170640a4ce2eed4705 100644
> --- a/lib/fuse_i.h
> +++ b/lib/fuse_i.h
> @@ -19,14 +19,15 @@
> #include <stdbool.h>
> #include <stdatomic.h>
>
> +#ifndef MIN
> #define MIN(a, b) \
> ({ \
> typeof(a) _a = (a); \
> typeof(b) _b = (b); \
> _a < _b ? _a : _b; \
> })
> +#endif
>
> -struct mount_opts;
> struct fuse_ring_pool;
>
> struct fuse_req {
> @@ -217,13 +218,8 @@ struct fuse_chan *fuse_chan_get(struct fuse_chan *ch);
> */
> void fuse_chan_put(struct fuse_chan *ch);
>
> -/* Special return value for mount functions to indicate fallback to fusermount3 is needed */
> -#define FUSE_MOUNT_FALLBACK_NEEDED -2
> -
> -struct mount_opts *parse_mount_opts(struct fuse_args *args);
> -void destroy_mount_opts(struct mount_opts *mo);
> +/* Mount-related functions */
> void fuse_mount_version(void);
> -unsigned get_max_read(struct mount_opts *o);
> void fuse_kern_unmount(const char *mountpoint, int fd);
> int fuse_kern_mount(const char *mountpoint, struct mount_opts *mo);
>
> diff --git a/lib/mount.c b/lib/mount.c
> index dec9d52274c13536648cacef959789f472c5682c..fe353e2cc4579adb47473cac5db7d1bae2defb2c 100644
> --- a/lib/mount.c
> +++ b/lib/mount.c
> @@ -16,6 +16,7 @@
> #include "fuse_misc.h"
> #include "fuse_opt.h"
> #include "mount_util.h"
> +#include "mount_i_linux.h"
>
> #include <stdio.h>
> #include <stdlib.h>
> @@ -49,7 +50,6 @@
> #define FUSERMOUNT_PROG "fusermount3"
> #define FUSE_COMMFD_ENV "_FUSE_COMMFD"
> #define FUSE_COMMFD2_ENV "_FUSE_COMMFD2"
> -#define FUSE_KERN_DEVICE_ENV "FUSE_KERN_DEVICE"
>
> #ifndef MS_DIRSYNC
> #define MS_DIRSYNC 128
> @@ -65,20 +65,6 @@ enum {
> KEY_RO,
> };
>
> -struct mount_opts {
> - int allow_other;
> - int flags;
> - int auto_unmount;
> - int blkdev;
> - char *fsname;
> - char *subtype;
> - char *subtype_opt;
> - char *mtab_opts;
> - char *fusermount_opts;
> - char *kernel_opts;
> - unsigned max_read;
> -};
> -
> #define FUSE_MOUNT_OPT(t, p) { t, offsetof(struct mount_opts, p), 1 }
>
> static const struct fuse_opt fuse_mount_opts[] = {
> @@ -197,7 +183,7 @@ static const struct mount_flags mount_flags[] = {
> {NULL, 0, 0}
> };
>
> -unsigned get_max_read(struct mount_opts *o)
> +unsigned int get_max_read(struct mount_opts *o)
> {
> return o->max_read;
> }
> @@ -510,7 +496,7 @@ static int fuse_kern_mount_prepare(const char *mnt,
> struct mount_opts *mo)
> {
> char tmp[128];
> - const char *devname = getenv(FUSE_KERN_DEVICE_ENV) ?: "/dev/fuse";
> + const char *devname = fuse_mnt_get_devname();
> struct stat stbuf;
> int fd;
> int res;
> @@ -563,35 +549,24 @@ out_close:
> * @mo: mount options
> * @mnt_opts: mount options to pass to the kernel
> *
> - * Returns: 0 on success, -1 on failure, FUSE_MOUNT_FALLBACK_NEEDED if fusermount should be used
> + * Returns: 0 on success, -1 on failure,
> + * FUSE_MOUNT_FALLBACK_NEEDED if fusermount should be used
> */
> static int fuse_kern_do_mount(const char *mnt, struct mount_opts *mo,
> const char *mnt_opts)
> {
> - const char *devname = getenv(FUSE_KERN_DEVICE_ENV) ?: "/dev/fuse";
> char *source = NULL;
> char *type = NULL;
> int res;
>
> res = -ENOMEM;
> - source = malloc((mo->fsname ? strlen(mo->fsname) : 0) +
> - (mo->subtype ? strlen(mo->subtype) : 0) +
> - strlen(devname) + 32);
> -
> - type = malloc((mo->subtype ? strlen(mo->subtype) : 0) + 32);
> + source = fuse_mnt_build_source(mo);
> + type = fuse_mnt_build_type(mo);
> if (!type || !source) {
> fuse_log(FUSE_LOG_ERR, "fuse: failed to allocate memory\n");
> goto out_close;
> }
>
> - strcpy(type, mo->blkdev ? "fuseblk" : "fuse");
> - if (mo->subtype) {
> - strcat(type, ".");
> - strcat(type, mo->subtype);
> - }
> - strcpy(source,
> - mo->fsname ? mo->fsname : (mo->subtype ? mo->subtype : devname));
> -
> res = mount(source, mnt, type, mo->flags, mo->kernel_opts);
> if (res == -1 && errno == ENODEV && mo->subtype) {
> /* Probably missing subtype support */
> @@ -627,20 +602,10 @@ static int fuse_kern_do_mount(const char *mnt, struct mount_opts *mo,
> goto out_close;
> }
>
> -#ifndef IGNORE_MTAB
> - if (geteuid() == 0) {
> - char *newmnt = fuse_mnt_resolve_path("fuse", mnt);
> - res = -1;
> - if (!newmnt)
> - goto out_umount;
> + res = fuse_mnt_add_mount_helper(mnt, source, type, mnt_opts);
> + if (res == -1)
> + goto out_umount;
>
> - res = fuse_mnt_add_mount("fuse", source, newmnt, type,
> - mnt_opts);
> - free(newmnt);
> - if (res == -1)
> - goto out_umount;
> - }
> -#endif /* IGNORE_MTAB */
> free(type);
> free(source);
>
> @@ -777,3 +742,44 @@ out:
> free(mnt_opts);
> return res;
> }
> +
> +const char *fuse_mnt_get_devname(void)
> +{
> + const char *devname = getenv(FUSE_KERN_DEVICE_ENV);
> +
> + return devname ? devname : "/dev/fuse";
> +}
> +
> +char *fuse_mnt_build_source(const struct mount_opts *mo)
> +{
> + const char *devname = fuse_mnt_get_devname();
> + char *source;
> +
> + source = malloc((mo->fsname ? strlen(mo->fsname) : 0) +
> + (mo->subtype ? strlen(mo->subtype) : 0) +
> + strlen(devname) + 32);
> + if (!source)
> + return NULL;
> +
> + strcpy(source,
> + mo->fsname ? mo->fsname : (mo->subtype ? mo->subtype : devname));
> +
> + return source;
> +}
> +
> +char *fuse_mnt_build_type(const struct mount_opts *mo)
> +{
> + char *type;
> +
> + type = malloc((mo->subtype ? strlen(mo->subtype) : 0) + 32);
> + if (!type)
> + return NULL;
> +
> + strcpy(type, mo->blkdev ? "fuseblk" : "fuse");
> + if (mo->subtype) {
> + strcat(type, ".");
> + strcat(type, mo->subtype);
> + }
> +
> + return type;
> +}
> diff --git a/lib/mount_bsd.c b/lib/mount_bsd.c
> index c12ab322e7dc86d5c8d2f2abc6b71488e82523cf..c5b831160f826c0e4620626a72c4b93427d18bab 100644
> --- a/lib/mount_bsd.c
> +++ b/lib/mount_bsd.c
> @@ -10,6 +10,7 @@
>
> #include "fuse_config.h"
> #include "fuse_i.h"
> +#include "mount_common_i.h"
> #include "fuse_misc.h"
> #include "fuse_opt.h"
> #include "util.h"
> diff --git a/lib/mount_common_i.h b/lib/mount_common_i.h
> new file mode 100644
> index 0000000000000000000000000000000000000000..d27d2aa624ae3806c61a0fe382c2d024080c9bb3
> --- /dev/null
> +++ b/lib/mount_common_i.h
> @@ -0,0 +1,31 @@
> +/*
> + * FUSE: Filesystem in Userspace
> + * Copyright (C) 2001-2007 Miklos Szeredi <miklos@szeredi.hu>
> + * 2026 Bernd Schubert <bernd@bsbernd.com>
> + *
> + * This program can be distributed under the terms of the GNU LGPLv2.
> + * See the file LGPL2.txt
> + */
> +
> +#ifndef FUSE_MOUNT_COMMON_I_H_
> +#define FUSE_MOUNT_COMMON_I_H_
> +
> +/* Forward declaration for fuse_args */
> +struct fuse_args;
> +struct mount_opts;
> +
> +/* Special return value for mount functions to indicate fallback to fusermount3 is needed */
> +#define FUSE_MOUNT_FALLBACK_NEEDED -2
> +
> +/* Environment variable for FUSE kernel device */
> +#define FUSE_KERN_DEVICE_ENV "FUSE_KERN_DEVICE"
> +
> +/* Mount options management functions */
> +struct mount_opts *parse_mount_opts(struct fuse_args *args);
> +void destroy_mount_opts(struct mount_opts *mo);
> +unsigned int get_max_read(struct mount_opts *o);
> +char *fuse_mnt_build_source(const struct mount_opts *mo);
> +char *fuse_mnt_build_type(const struct mount_opts *mo);
> +
> +
> +#endif /* FUSE_MOUNT_COMMON_I_H_ */
> diff --git a/lib/mount_i_linux.h b/lib/mount_i_linux.h
> new file mode 100644
> index 0000000000000000000000000000000000000000..abcd1b08012feedef6b4c8961b55ac847a27496a
> --- /dev/null
> +++ b/lib/mount_i_linux.h
> @@ -0,0 +1,32 @@
> +/*
> + * FUSE: Filesystem in Userspace
> + * Copyright (C) 2001-2007 Miklos Szeredi <miklos@szeredi.hu>
> + * 2026 Bernd Schubert <bernd@bsbernd.com>
> + *
> + * This program can be distributed under the terms of the GNU LGPLv2.
> + * See the file LGPL2.txt
> + */
> +
> +#ifndef FUSE_MOUNT_I_H_
> +#define FUSE_MOUNT_I_H_
> +
> +/* Forward declaration for fuse_args */
> +struct fuse_args;
> +
> +/* Mount options structure */
> +struct mount_opts {
> + int allow_other;
> + int flags;
> + int auto_unmount;
> + int blkdev;
> + char *fsname;
> + char *subtype;
> + char *subtype_opt;
> + char *mtab_opts;
> + char *fusermount_opts;
> + char *kernel_opts;
> + unsigned int max_read;
> +};
> +
> +
> +#endif /* FUSE_MOUNT_I_H_ */
> diff --git a/lib/mount_util.c b/lib/mount_util.c
> index 8c0cdf72d978da68c95125964416b92668104924..a42a02a0b92f98778abb2c491cdc7a01260f56ee 100644
> --- a/lib/mount_util.c
> +++ b/lib/mount_util.c
> @@ -375,3 +375,27 @@ int fuse_mnt_parse_fuse_fd(const char *mountpoint)
>
> return -1;
> }
> +
> +int fuse_mnt_add_mount_helper(const char *mnt, const char *source,
> + const char *type, const char *mnt_opts)
> +{
> +#ifndef IGNORE_MTAB
> + if (geteuid() == 0) {
> + char *newmnt = fuse_mnt_resolve_path("fuse", mnt);
> + int res;
> +
> + if (!newmnt)
> + return -1;
> +
> + res = fuse_mnt_add_mount("fuse", source, newmnt, type,
> + mnt_opts);
> + free(newmnt);
> + return res;
> + }
> +#endif
> + (void)mnt;
> + (void)source;
> + (void)type;
> + (void)mnt_opts;
> + return 0;
> +}
> diff --git a/lib/mount_util.h b/lib/mount_util.h
> index 9cb9077dd177381d712e34e7118bf73572142c6a..4688d00091f001b3ccd36b1ef3b7e799031ed773 100644
> --- a/lib/mount_util.h
> +++ b/lib/mount_util.h
> @@ -7,6 +7,7 @@
> */
>
> #include <sys/types.h>
> +#include "mount_common_i.h" // IWYU pragma: keep
>
> int fuse_mnt_add_mount(const char *progname, const char *fsname,
> const char *mnt, const char *type, const char *opts);
> @@ -16,3 +17,8 @@ int fuse_mnt_umount(const char *progname, const char *abs_mnt,
> char *fuse_mnt_resolve_path(const char *progname, const char *orig);
> int fuse_mnt_check_fuseblk(void);
> int fuse_mnt_parse_fuse_fd(const char *mountpoint);
> +
> +/* Helper functions for mount operations */
> +const char *fuse_mnt_get_devname(void);
> +int fuse_mnt_add_mount_helper(const char *mnt, const char *source,
> + const char *type, const char *mnt_opts);
>
> --
> 2.43.0
>
>
^ permalink raw reply [flat|nested] 59+ messages in thread
* Re: [PATCH 09/19] Move mount flags to mount_i.h
2026-03-23 17:45 ` [PATCH 09/19] Move mount flags to mount_i.h Bernd Schubert
@ 2026-03-23 22:45 ` Darrick J. Wong
2026-03-24 18:40 ` Bernd Schubert
0 siblings, 1 reply; 59+ messages in thread
From: Darrick J. Wong @ 2026-03-23 22:45 UTC (permalink / raw)
To: Bernd Schubert
Cc: linux-fsdevel, Miklos Szeredi, Joanne Koong, Bernd Schubert
On Mon, Mar 23, 2026 at 06:45:04PM +0100, Bernd Schubert wrote:
> From: Bernd Schubert <bschubert@ddn.com>
>
> This is preparation work for the new mount API, which goes into
> its own file.
>
> Signed-off-by: Bernd Schubert <bschubert@ddn.com>
> ---
> lib/mount.c | 59 ++++++++---------------------------------------------
> lib/mount_i_linux.h | 35 ++++++++++++++++++++++++++++++-
> 2 files changed, 42 insertions(+), 52 deletions(-)
>
> diff --git a/lib/mount.c b/lib/mount.c
> index fe353e2cc4579adb47473cac5db7d1bae2defb2c..b3ee9ba4c0a74c1d0d55f916e7046e064f8468b0 100644
> --- a/lib/mount.c
> +++ b/lib/mount.c
> @@ -34,16 +34,6 @@
> #include "fuse_mount_compat.h"
>
> #ifdef __NetBSD__
Hopefully the rest of the bsd stuff can go in its own files too?
> -#include <perfuse.h>
> -
> -#define MS_RDONLY MNT_RDONLY
> -#define MS_NOSUID MNT_NOSUID
> -#define MS_NODEV MNT_NODEV
> -#define MS_NOEXEC MNT_NOEXEC
> -#define MS_SYNCHRONOUS MNT_SYNCHRONOUS
> -#define MS_NOATIME MNT_NOATIME
> -#define MS_NOSYMFOLLOW MNT_NOSYMFOLLOW
> -
> #define umount2(mnt, flags) unmount(mnt, (flags == 2) ? MNT_FORCE : 0)
> #endif
>
> @@ -51,10 +41,6 @@
> #define FUSE_COMMFD_ENV "_FUSE_COMMFD"
> #define FUSE_COMMFD2_ENV "_FUSE_COMMFD2"
>
> -#ifndef MS_DIRSYNC
> -#define MS_DIRSYNC 128
> -#endif
> -
> enum {
> KEY_KERN_FLAG,
> KEY_KERN_OPT,
> @@ -154,35 +140,6 @@ void fuse_mount_version(void)
> FUSERMOUNT_PROG);
> }
>
> -struct mount_flags {
> - const char *opt;
> - unsigned long flag;
> - int on;
> -};
> -
> -static const struct mount_flags mount_flags[] = {
> - {"rw", MS_RDONLY, 0},
> - {"ro", MS_RDONLY, 1},
> - {"suid", MS_NOSUID, 0},
> - {"nosuid", MS_NOSUID, 1},
> - {"dev", MS_NODEV, 0},
> - {"nodev", MS_NODEV, 1},
> - {"exec", MS_NOEXEC, 0},
> - {"noexec", MS_NOEXEC, 1},
> - {"async", MS_SYNCHRONOUS, 0},
> - {"sync", MS_SYNCHRONOUS, 1},
> - {"noatime", MS_NOATIME, 1},
> - {"nodiratime", MS_NODIRATIME, 1},
> - {"norelatime", MS_RELATIME, 0},
> - {"nostrictatime", MS_STRICTATIME, 0},
> - {"symfollow", MS_NOSYMFOLLOW, 0},
> - {"nosymfollow", MS_NOSYMFOLLOW, 1},
> -#ifndef __NetBSD__
> - {"dirsync", MS_DIRSYNC, 1},
> -#endif
> - {NULL, 0, 0}
> -};
> -
> unsigned int get_max_read(struct mount_opts *o)
> {
> return o->max_read;
> @@ -192,13 +149,13 @@ static void set_mount_flag(const char *s, int *flags)
> {
> int i;
>
> - for (i = 0; mount_flags[i].opt != NULL; i++) {
> - const char *opt = mount_flags[i].opt;
> + for (i = 0; fuse_mount_flags[i].opt != NULL; i++) {
> + const char *opt = fuse_mount_flags[i].opt;
> if (strcmp(opt, s) == 0) {
> - if (mount_flags[i].on)
> - *flags |= mount_flags[i].flag;
> + if (fuse_mount_flags[i].on)
> + *flags |= fuse_mount_flags[i].flag;
> else
> - *flags &= ~mount_flags[i].flag;
> + *flags &= ~fuse_mount_flags[i].flag;
> return;
> }
> }
> @@ -645,9 +602,9 @@ static int get_mnt_flag_opts(char **mnt_optsp, int flags)
> if (!(flags & MS_RDONLY) && fuse_opt_add_opt(mnt_optsp, "rw") == -1)
> return -1;
>
> - for (i = 0; mount_flags[i].opt != NULL; i++) {
> - if (mount_flags[i].on && (flags & mount_flags[i].flag) &&
> - fuse_opt_add_opt(mnt_optsp, mount_flags[i].opt) == -1)
> + for (i = 0; fuse_mount_flags[i].opt != NULL; i++) {
> + if (fuse_mount_flags[i].on && (flags & fuse_mount_flags[i].flag) &&
> + fuse_opt_add_opt(mnt_optsp, fuse_mount_flags[i].opt) == -1)
> return -1;
> }
> return 0;
> diff --git a/lib/mount_i_linux.h b/lib/mount_i_linux.h
> index abcd1b08012feedef6b4c8961b55ac847a27496a..c0de6228fce5a4d9070cc246ec76222b66de56fb 100644
> --- a/lib/mount_i_linux.h
> +++ b/lib/mount_i_linux.h
> @@ -10,7 +10,8 @@
> #ifndef FUSE_MOUNT_I_H_
> #define FUSE_MOUNT_I_H_
>
> -/* Forward declaration for fuse_args */
> +#include <sys/mount.h>
> +
> struct fuse_args;
>
> /* Mount options structure */
> @@ -28,5 +29,37 @@ struct mount_opts {
> unsigned int max_read;
> };
>
> +/* Mount flags mapping structure */
> +struct mount_flags {
> + const char *opt;
> + unsigned long flag;
> + int on;
> +};
> +
> +/* Mount flags table */
> +static const struct mount_flags fuse_mount_flags[] = {
Hmmm, doesn't this create a fuse_mount_flags[] in every single .o file
that #includes this header? I'd have thought you'd want an extern
declaration here and a definition elsewhere?
> + {"rw", MS_RDONLY, 0},
> + {"ro", MS_RDONLY, 1},
> + {"suid", MS_NOSUID, 0},
> + {"nosuid", MS_NOSUID, 1},
> + {"dev", MS_NODEV, 0},
> + {"nodev", MS_NODEV, 1},
> + {"exec", MS_NOEXEC, 0},
> + {"noexec", MS_NOEXEC, 1},
> + {"async", MS_SYNCHRONOUS, 0},
> + {"sync", MS_SYNCHRONOUS, 1},
> + {"noatime", MS_NOATIME, 1},
> + {"nodiratime", MS_NODIRATIME, 1},
> + {"norelatime", MS_RELATIME, 0},
> + {"nostrictatime", MS_STRICTATIME, 0},
> + {"symfollow", MS_NOSYMFOLLOW, 0},
> + {"nosymfollow", MS_NOSYMFOLLOW, 1},
> +#ifndef __NetBSD__
When would we have __NetBSD__ defined on a Linux build?
Though I see no meson.build changes here, so I can't tell if
mount_i_linux.c only gets built on Linux or everywhere or ...?
--D
> + {"dirsync", MS_DIRSYNC, 1},
> +#endif
> + {NULL, 0, 0}
> +};
> +
> +
>
> #endif /* FUSE_MOUNT_I_H_ */
>
> --
> 2.43.0
>
>
^ permalink raw reply [flat|nested] 59+ messages in thread
* Re: [PATCH 11/19] Add support for the new linux mount API
2026-03-23 17:45 ` [PATCH 11/19] Add support for the new linux mount API Bernd Schubert
@ 2026-03-23 23:42 ` Darrick J. Wong
2026-03-24 20:16 ` Bernd Schubert
0 siblings, 1 reply; 59+ messages in thread
From: Darrick J. Wong @ 2026-03-23 23:42 UTC (permalink / raw)
To: Bernd Schubert
Cc: linux-fsdevel, Miklos Szeredi, Joanne Koong, Bernd Schubert
On Mon, Mar 23, 2026 at 06:45:06PM +0100, Bernd Schubert wrote:
> From: Bernd Schubert <bschubert@ddn.com>
>
> So far only supported for fuse_session_mount(), which is called
> from high and low level API, but not yet supported for
> fuse_open_channel(), which used for privilege drop through
> mount.fuse. Main goal for the new API is support for synchronous
> FUSE_INIT and I don't think that is going to work with
> fuse_open_channel(). At least not with io-uring support as long
> as it is started from FUSE_INIT.
>
> Signed-off-by: Bernd Schubert <bschubert@ddn.com>
> ---
> lib/fuse_lowlevel.c | 75 +++++++++-
> lib/meson.build | 3 +
> lib/mount.c | 27 +++-
> lib/mount_fsmount.c | 405 ++++++++++++++++++++++++++++++++++++++++++++++++++++
> lib/mount_i_linux.h | 14 ++
> meson.build | 19 ++-
> 6 files changed, 535 insertions(+), 8 deletions(-)
>
> diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c
> index 4a1c7c4c02a05706e077a7ea89fab3c81bfe22cd..626233df20f49fa89cd9327f94340169d7061f75 100644
> --- a/lib/fuse_lowlevel.c
> +++ b/lib/fuse_lowlevel.c
> @@ -20,6 +20,9 @@
> #include "util.h"
> #include "fuse_uring_i.h"
> #include "fuse_daemonize.h"
> +#if defined(__linux__)
> +#include "mount_i_linux.h"
> +#endif
>
> #include <pthread.h>
> #include <stdatomic.h>
> @@ -4398,6 +4401,64 @@ int fuse_session_custom_io_30(struct fuse_session *se,
> offsetof(struct fuse_custom_io, clone_fd), fd);
> }
>
> +#if defined(HAVE_NEW_MOUNT_API)
> +/* Only linux supports sync FUSE_INIT so far */
What does synchronous FUSE_INIT have to do with fsmount()?
Does it not work with the old mount(2)?
> +static int fuse_session_mount_new_api(struct fuse_session *se,
> + const char *mountpoint)
> +{
> + int fd = -1;
> + int res, err;
> + char *mnt_opts = NULL;
> + char *mnt_opts_with_fd = NULL;
> + char fd_opt[32];
> +
> + res = fuse_kern_mount_get_base_mnt_opts(se->mo, &mnt_opts);
> + if (res == -1) {
> + fuse_log(FUSE_LOG_ERR,
> + "fuse: failed to get base mount options\n");
> + err = -EIO;
> + goto err;
> + }
> +
> + fd = fuse_kern_mount_prepare(mountpoint, se->mo);
> + if (fd == -1) {
> + fuse_log(FUSE_LOG_ERR, "Mount preparation failed.\n");
> + err = -EIO;
> + goto err;
> + }
> +
> + snprintf(fd_opt, sizeof(fd_opt), "fd=%i", fd);
> + if (fuse_opt_add_opt(&mnt_opts_with_fd, mnt_opts) == -1 ||
> + fuse_opt_add_opt(&mnt_opts_with_fd, fd_opt) == -1) {
> + err = -ENOMEM;
> + goto err;
> + }
> +
> + err = fuse_kern_fsmount_mo(mountpoint, se->mo, mnt_opts_with_fd);
> +err:
> + if (err) {
> + if (fd >= 0)
> + close(fd);
> + fd = -1;
> + se->fd = -1;
> + se->error = -errno;
> + }
> +
> + free(mnt_opts);
> + free(mnt_opts_with_fd);
> + return fd;
> +}
> +#else
> +static int fuse_session_mount_new_api(struct fuse_session *se,
> + const char *mountpoint)
> +{
> + (void)se;
> + (void)mountpoint;
> +
> + return -1;
> +}
> +#endif
> +
> int fuse_session_mount(struct fuse_session *se, const char *_mountpoint)
> {
> int fd;
> @@ -4425,6 +4486,8 @@ int fuse_session_mount(struct fuse_session *se, const char *_mountpoint)
> close(fd);
> } while (fd >= 0 && fd <= 2);
>
> + /* Open channel */
> +
> /*
> * To allow FUSE daemons to run without privileges, the caller may open
> * /dev/fuse before launching the file system and pass on the file
> @@ -4443,10 +4506,18 @@ int fuse_session_mount(struct fuse_session *se, const char *_mountpoint)
> return 0;
> }
>
> - /* Open channel */
> + /* new linux mount api */
> + fd = fuse_session_mount_new_api(se, mountpoint);
> + if (fd >= 0)
> + goto out;
> +
> + /* fall back to old API */
> + se->error = 0; /* reset error of new api */
> fd = fuse_kern_mount(mountpoint, se->mo);
> - if (fd == -1)
> + if (fd < 0)
> goto error_out;
> +
> +out:
> se->fd = fd;
>
> /* Save mountpoint */
> diff --git a/lib/meson.build b/lib/meson.build
> index 5bd449ebffe7c9229df904d647d990c6c47f80b5..5fd738a589c5aba97a738d5eedbf0f9962e4adfc 100644
> --- a/lib/meson.build
> +++ b/lib/meson.build
> @@ -7,6 +7,9 @@ libfuse_sources = ['fuse.c', 'fuse_i.h', 'fuse_loop.c', 'fuse_loop_mt.c',
>
> if host_machine.system().startswith('linux')
> libfuse_sources += [ 'mount.c' ]
> + if private_cfg.get('HAVE_NEW_MOUNT_API', false)
> + libfuse_sources += [ 'mount_fsmount.c' ]
> + endif
> else
> libfuse_sources += [ 'mount_bsd.c' ]
> endif
> diff --git a/lib/mount.c b/lib/mount.c
> index b3ee9ba4c0a74c1d0d55f916e7046e064f8468b0..30fd4d2f9bbb84c817b2363b2075456acd1c1255 100644
> --- a/lib/mount.c
> +++ b/lib/mount.c
> @@ -449,8 +449,8 @@ static int fuse_mount_fusermount(const char *mountpoint, struct mount_opts *mo,
> #define O_CLOEXEC 0
> #endif
>
> -static int fuse_kern_mount_prepare(const char *mnt,
> - struct mount_opts *mo)
> +int fuse_kern_mount_prepare(const char *mnt,
> + struct mount_opts *mo)
> {
> char tmp[128];
> const char *devname = fuse_mnt_get_devname();
> @@ -500,6 +500,26 @@ out_close:
> return -1;
> }
>
> +#if defined(HAVE_NEW_MOUNT_API)
> +/**
> + * Wrapper for fuse_kern_fsmount that accepts struct mount_opts
> + * @mnt: mountpoint
> + * @mo: mount options
> + * @mnt_opts: mount options to pass to the kernel
> + *
> + * Returns: 0 on success, -1 on failure with errno set
> + */
> +int fuse_kern_fsmount_mo(const char *mnt, struct mount_opts *mo,
> + const char *mnt_opts)
> +{
> + const char *devname = fuse_mnt_get_devname();
> +
> + return fuse_kern_fsmount(mnt, mo->flags, mo->blkdev, mo->fsname,
> + mo->subtype, devname, mo->kernel_opts,
> + mnt_opts);
> +}
> +#endif
> +
> /**
> * Complete the mount operation with an already-opened fd
> * @mnt: mountpoint
> @@ -643,8 +663,7 @@ void destroy_mount_opts(struct mount_opts *mo)
> free(mo);
> }
>
> -static int fuse_kern_mount_get_base_mnt_opts(struct mount_opts *mo,
> - char **mnt_optsp)
> +int fuse_kern_mount_get_base_mnt_opts(struct mount_opts *mo, char **mnt_optsp)
> {
> if (get_mnt_flag_opts(mnt_optsp, mo->flags) == -1)
> return -1;
> diff --git a/lib/mount_fsmount.c b/lib/mount_fsmount.c
> new file mode 100644
> index 0000000000000000000000000000000000000000..cba998bc60c783a5edc0c16570f7e5512b7f1253
> --- /dev/null
> +++ b/lib/mount_fsmount.c
> @@ -0,0 +1,405 @@
> +/*
> + * FUSE: Filesystem in Userspace
> + * Copyright (C) 2001-2007 Miklos Szeredi <miklos@szeredi.hu>
> + * 2026 Bernd Schubert <bernd@bsbernd.com>
> + *
> + * New Linux mount API (fsopen/fsconfig/fsmount/move_mount) support.
> + *
> + * This program can be distributed under the terms of the GNU LGPLv2.
> + * See the file LGPL2.txt.
> + */
> +
> +#define _GNU_SOURCE
> +
> +#include "fuse_config.h"
> +#include "fuse_misc.h"
> +#include "mount_util.h"
> +#include "mount_i_linux.h"
> +
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <unistd.h>
> +#include <string.h>
> +#include <fcntl.h>
> +#include <errno.h>
> +#include <sys/mount.h>
> +#include <sys/syscall.h>
> +
> +/* Mount attribute flags for fsmount() - from linux/mount.h */
> +#ifndef MOUNT_ATTR_RDONLY
> +#define MOUNT_ATTR_RDONLY 0x00000001
> +#endif
> +#ifndef MOUNT_ATTR_NOSUID
> +#define MOUNT_ATTR_NOSUID 0x00000002
> +#endif
> +#ifndef MOUNT_ATTR_NODEV
> +#define MOUNT_ATTR_NODEV 0x00000004
> +#endif
> +#ifndef MOUNT_ATTR_NOEXEC
> +#define MOUNT_ATTR_NOEXEC 0x00000008
> +#endif
> +#ifndef MOUNT_ATTR__ATIME
> +#define MOUNT_ATTR__ATIME 0x00000070
> +#endif
> +#ifndef MOUNT_ATTR_RELATIME
> +#define MOUNT_ATTR_RELATIME 0x00000000
> +#endif
> +#ifndef MOUNT_ATTR_NOATIME
> +#define MOUNT_ATTR_NOATIME 0x00000010
> +#endif
> +#ifndef MOUNT_ATTR_STRICTATIME
> +#define MOUNT_ATTR_STRICTATIME 0x00000020
> +#endif
> +#ifndef MOUNT_ATTR_NODIRATIME
> +#define MOUNT_ATTR_NODIRATIME 0x00000080
> +#endif
> +#ifndef MOUNT_ATTR_NOSYMFOLLOW
> +#define MOUNT_ATTR_NOSYMFOLLOW 0x00200000
> +#endif
/me notes that all MOUNT_ATTR_ before NOSYMFOLLOW were introduced in the
initial commit so if the meson test thinks the new fsmount api is
present then you can rely on flags being defined.
> +
> +/*
> + * Convert MS_* mount flags to MOUNT_ATTR_* mount attributes.
> + * These flags are passed to fsmount(), not fsconfig().
> + * Mount attributes control mount-point level behavior.
> + */
> +static unsigned long ms_flags_to_mount_attrs(unsigned long flags)
> +{
> + unsigned long attrs = 0;
> +
> + if (flags & MS_NOSUID)
> + attrs |= MOUNT_ATTR_NOSUID;
> + if (flags & MS_NODEV)
> + attrs |= MOUNT_ATTR_NODEV;
> + if (flags & MS_NOEXEC)
> + attrs |= MOUNT_ATTR_NOEXEC;
> + if (flags & MS_NOATIME)
> + attrs |= MOUNT_ATTR_NOATIME;
> + else if (flags & MS_RELATIME)
> + attrs |= MOUNT_ATTR_RELATIME;
> + else if (flags & MS_STRICTATIME)
> + attrs |= MOUNT_ATTR_STRICTATIME;
> + if (flags & MS_NODIRATIME)
> + attrs |= MOUNT_ATTR_NODIRATIME;
> + if (flags & MS_NOSYMFOLLOW)
> + attrs |= MOUNT_ATTR_NOSYMFOLLOW;
> +
> + return attrs;
> +}
> +
> +/*
> + * Apply VFS superblock flags to the filesystem context.
> + * Only handles flags that are filesystem parameters (ro, sync, dirsync).
> + * Mount attributes (nosuid, nodev, etc.) are handled separately via fsmount().
> + */
> +static int apply_mount_flags(int fsfd, unsigned long flags)
> +{
> + int res;
> +
> + /* Handle read-only flag */
> + if (flags & MS_RDONLY) {
> + res = fsconfig(fsfd, FSCONFIG_SET_FLAG, "ro", NULL, 0);
> + if (res == -1) {
> + fprintf(stderr,
> + "fuse: fsconfig SET_FLAG ro failed: %s\n",
> + strerror(errno));
> + return -errno;
> + }
> + }
> +
> + /* Handle sync flag */
> + if (flags & MS_SYNCHRONOUS) {
> + res = fsconfig(fsfd, FSCONFIG_SET_FLAG, "sync", NULL, 0);
> + if (res == -1) {
> + fprintf(stderr,
> + "fuse: fsconfig SET_FLAG sync failed: %s\n",
> + strerror(errno));
> + return -errno;
> + }
> + }
> +
> +#ifndef __NetBSD__
> + /* Handle dirsync flag */
> + if (flags & MS_DIRSYNC) {
> + res = fsconfig(fsfd, FSCONFIG_SET_FLAG, "dirsync", NULL, 0);
> + if (res == -1) {
> + fprintf(stderr,
> + "fuse: fsconfig SET_FLAG dirsync failed: %s\n",
> + strerror(errno));
> + return -errno;
> + }
> + }
> +#endif
> +
> + return 0;
> +}
Oh, nice treatment of the MS_ flags that don't have a corresponding
MOUNT_ATTR_ flag. I should incorporate that into mount_service.c.
> +
> +static int apply_opt_fd(int fsfd, const char *value)
> +{
> + int res;
> +
> + /* The fd parameter is a u32 value, not a file descriptor to pass */
> + res = fsconfig(fsfd, FSCONFIG_SET_STRING, "fd", value, 0);
> + if (res == -1) {
> + fprintf(stderr, "fuse: fsconfig SET_STRING fd=%s failed: %s\n",
> + value, strerror(errno));
> + return -errno;
Did you know that you can read the kernel's error messages from the
fsopen fd?
https://git.kernel.org/pub/scm/linux/kernel/git/djwong/libfuse.git/tree/util/mount_service.c?h=fuse-service-container&id=d02bbab680ab4689bbd5e274735e91bd38e5f47f#n693
> + }
> + return 0;
> +}
> +
> +static int apply_opt_string(int fsfd, const char *key, const char *value)
> +{
> + int res;
> +
> + res = fsconfig(fsfd, FSCONFIG_SET_STRING, key, value, 0);
> + if (res == -1) {
> + fprintf(stderr,
> + "fuse: fsconfig SET_STRING %s=%s failed: %s\n",
> + key, value, strerror(errno));
> + return -errno;
I think you have to save errno explicitly here, because fprintf and
strerror could fail and set errno to something else. For example, if
fsconfig returns EINVAL but stderr points to a file on a completely full
filesystem, the ENOSPC from the fprintf call (or more likely the write()
underneath it) will obliterate the EINVAL.
> + }
> + return 0;
> +}
> +
> +static int apply_opt_flag(int fsfd, const char *opt)
> +{
> + int res;
> +
> + res = fsconfig(fsfd, FSCONFIG_SET_FLAG, opt, NULL, 0);
> + if (res == -1) {
> + fprintf(stderr, "fuse: fsconfig SET_FLAG %s failed: %s\n",
> + opt, strerror(errno));
> + return -errno;
> + }
> + return 0;
> +}
> +
> +static int apply_opt_key_value(int fsfd, char *opt)
> +{
> + char *eq;
> + const char *key;
> + const char *value;
> +
> + eq = strchr(opt, '=');
> + if (!eq)
> + return apply_opt_flag(fsfd, opt);
> +
> + *eq = '\0';
> + key = opt;
> + value = eq + 1;
> +
> + if (strcmp(key, "fd") == 0)
> + return apply_opt_fd(fsfd, value);
> +
> + return apply_opt_string(fsfd, key, value);
> +}
> +
> +/**
> + * Check if an option is a mount attribute (handled by fsmount, not fsconfig)
> + */
> +static int is_mount_attr_opt(const char *opt)
> +{
> + /* These options are mount attributes passed to fsmount(), not fsconfig() */
> + return strcmp(opt, "nosuid") == 0 ||
> + strcmp(opt, "suid") == 0 ||
> + strcmp(opt, "nodev") == 0 ||
> + strcmp(opt, "dev") == 0 ||
> + strcmp(opt, "noexec") == 0 ||
> + strcmp(opt, "exec") == 0 ||
> + strcmp(opt, "noatime") == 0 ||
> + strcmp(opt, "atime") == 0 ||
> + strcmp(opt, "nodiratime") == 0 ||
> + strcmp(opt, "diratime") == 0 ||
> + strcmp(opt, "relatime") == 0 ||
> + strcmp(opt, "norelatime") == 0 ||
> + strcmp(opt, "strictatime") == 0 ||
> + strcmp(opt, "nostrictatime") == 0 ||
> + strcmp(opt, "nosymfollow") == 0 ||
> + strcmp(opt, "symfollow") == 0;
> +}
> +
> +/*
> + * Parse kernel options string and apply via fsconfig
> + * Options are comma-separated key=value pairs
> + */
> +static int apply_mount_opts(int fsfd, const char *opts)
> +{
> + char *opts_copy;
> + char *opt;
> + char *saveptr;
> + int res;
> +
> + if (!opts || !*opts)
> + return 0;
> +
> + opts_copy = strdup(opts);
> + if (!opts_copy) {
> + fprintf(stderr, "fuse: failed to allocate memory\n");
> + return -ENOMEM;
> + }
> +
> + opt = strtok_r(opts_copy, ",", &saveptr);
> + while (opt) {
> + /* Skip mount attributes - they're handled by fsmount(), not fsconfig() */
> + if (!is_mount_attr_opt(opt)) {
> + res = apply_opt_key_value(fsfd, opt);
Does fuse_session_new_versioned convert those magic string flags from
is_mount_attr_opt into their MS_* equivalents such that apply_mount_opts
won't see the string versions?
> + if (res < 0) {
> + free(opts_copy);
> + return res;
> + }
> + }
> + opt = strtok_r(NULL, ",", &saveptr);
> + }
> +
> + free(opts_copy);
> + return 0;
> +}
> +
> +
> +/**
> + * Mount using the new Linux mount API (fsopen/fsconfig/fsmount/move_mount)
> + * @mnt: mountpoint
> + * @flags: mount flags (MS_NOSUID, MS_NODEV, etc.)
> + * @blkdev: 1 for fuseblk, 0 for fuse
> + * @fsname: filesystem name (or NULL)
> + * @subtype: filesystem subtype (or NULL)
> + * @source_dev: device name for building source string
> + * @kernel_opts: kernel mount options string
> + * @mnt_opts: additional mount options to pass to the kernel
> + *
> + * Returns: 0 on success, -1 on failure with errno set
> + */
> +int fuse_kern_fsmount(const char *mnt, unsigned long flags, int blkdev,
> + const char *fsname, const char *subtype,
> + const char *source_dev, const char *kernel_opts,
> + const char *mnt_opts)
> +{
> + const char *type;
> + char *source = NULL;
> + int fsfd = -1;
> + int mntfd = -1;
> + int err, res;
> + unsigned long mount_attrs;
> +
> + /* Determine filesystem type */
> + type = blkdev ? "fuseblk" : "fuse";
> +
> + /* Try to open filesystem context */
> + fsfd = fsopen(type, FSOPEN_CLOEXEC);
> + if (fsfd == -1) {
> + fprintf(stderr, "fuse: fsopen(%s) failed: %s\n", type,
> + strerror(errno));
> + return -1;
> + }
> +
> + /* Build source string */
> + source = malloc((fsname ? strlen(fsname) : 0) +
> + (subtype ? strlen(subtype) : 0) +
> + strlen(source_dev) + 32);
> + err = -ENOMEM;
> + if (!source) {
> + fprintf(stderr, "fuse: failed to allocate memory\n");
> + goto out_close_fsfd;
> + }
> +
> + strcpy(source, fsname ? fsname : (subtype ? subtype : source_dev));
This is almost fuse_mnt_build_source()
--D
> +
> + /* Configure source */
> + res = fsconfig(fsfd, FSCONFIG_SET_STRING, "source", source, 0);
> + if (res == -1) {
> + err = -errno;
> + fprintf(stderr, "fuse: fsconfig source failed: %s\n",
> + strerror(errno));
> + goto out_free;
> + }
> +
> + /* Apply VFS superblock flags (ro, sync, dirsync) */
> + err = apply_mount_flags(fsfd, flags);
> + if (err < 0) {
> + fprintf(stderr, "fuse: failed to apply mount flags\n");
> + goto out_free;
> + }
> +
> + /* Apply kernel options */
> + err = apply_mount_opts(fsfd, kernel_opts);
> + if (err < 0) {
> + fprintf(stderr,
> + "fuse: failed to apply kernel options '%s'\n",
> + kernel_opts);
> + goto out_free;
> + }
> +
> + /* Apply additional mount options */
> + err = apply_mount_opts(fsfd, mnt_opts);
> + if (err < 0) {
> + fprintf(stderr,
> + "fuse: failed to apply additional mount options '%s'\n",
> + mnt_opts);
> + goto out_free;
> + }
> +
> + /* Create the filesystem instance */
> + res = fsconfig(fsfd, FSCONFIG_CMD_CREATE, NULL, NULL, 0);
> + if (res == -1) {
> + err = -errno;
> + fprintf(stderr, "fuse: fsconfig CREATE failed: %s\n",
> + strerror(errno));
> + goto out_free;
> + }
> +
> + /* Convert MS_* flags to MOUNT_ATTR_* for fsmount() */
> + mount_attrs = ms_flags_to_mount_attrs(flags);
> +
> + /* Create mount object with mount attributes */
> + mntfd = fsmount(fsfd, FSMOUNT_CLOEXEC, mount_attrs);
> + if (mntfd == -1) {
> + err = -errno;
> + fprintf(stderr, "fuse: fsmount failed: %s\n",
> + strerror(errno));
> + goto out_free;
> + }
> +
> + close(fsfd);
> + fsfd = -1;
> +
> + /* Attach to mount point */
> + if (move_mount(mntfd, "", AT_FDCWD, mnt, MOVE_MOUNT_F_EMPTY_PATH) ==
> + -1) {
> + err = -errno;
> + fprintf(stderr, "fuse: move_mount failed: %s\n",
> + strerror(errno));
> + goto out_close_mntfd;
> + }
> +
> + err = fuse_mnt_add_mount_helper(mnt, source, type, mnt_opts);
> + if (err == -1)
> + goto out_umount;
> +
> + close(mntfd);
> + free(source);
> + return 0;
> +
> +out_umount:
> + {
> + /* race free umount */
> + char fd_path[64];
> +
> + snprintf(fd_path, sizeof(fd_path), "/proc/self/fd/%d", mntfd);
> + if (umount2(fd_path, MNT_DETACH) == -1 && errno != EINVAL) {
> + fprintf(stderr,
> + "fuse: cleanup umount failed: %s\n",
> + strerror(errno));
> + }
> + }
> +out_close_mntfd:
> + if (mntfd != -1)
> + close(mntfd);
> +out_free:
> + free(source);
> +out_close_fsfd:
> + if (fsfd != -1)
> + close(fsfd);
> + errno = -err;
> + return -1;
> +}
> +
> diff --git a/lib/mount_i_linux.h b/lib/mount_i_linux.h
> index c0de6228fce5a4d9070cc246ec76222b66de56fb..52eab9e650c055142feec329264f82c2b08be0d5 100644
> --- a/lib/mount_i_linux.h
> +++ b/lib/mount_i_linux.h
> @@ -60,6 +60,20 @@ static const struct mount_flags fuse_mount_flags[] = {
> {NULL, 0, 0}
> };
>
> +int fuse_kern_mount_prepare(const char *mnt, struct mount_opts *mo);
> +
> +int fuse_kern_mount_get_base_mnt_opts(struct mount_opts *mo, char **mnt_optsp);
> +
> +int fuse_kern_fsmount(const char *mnt, unsigned long flags, int blkdev,
> + const char *fsname, const char *subtype,
> + const char *source_dev, const char *kernel_opts,
> + const char *mnt_opts);
> +
> +int fuse_kern_fsmount_mo(const char *mnt, struct mount_opts *mo,
> + const char *mnt_opts);
> +
> +int fuse_kern_fsmount_mo(const char *mnt, struct mount_opts *mo,
> + const char *mnt_opts);
>
>
> #endif /* FUSE_MOUNT_I_H_ */
> diff --git a/meson.build b/meson.build
> index 80c5f1dc0bd3565c11ad3084dac08e28dab611dc..465a7bbfd14fe9c45897f197ef37db76079d20ee 100644
> --- a/meson.build
> +++ b/meson.build
> @@ -30,7 +30,7 @@ if platform == 'darwin'
> 'https://www.fuse-t.org/ instead')
> elif platform == 'cygwin' or platform == 'windows'
> error('libfuse does not support Windows.\n' +
> - 'Take a look at http://www.secfs.net/winfsp/ instead')
> + 'Take a look at http://www.secfs.net/winfsp/ instead')
> endif
>
> cc = meson.get_compiler('c')
> @@ -118,6 +118,21 @@ special_funcs = {
> return -1;
> }
> }
> + ''',
> + 'new_mount_api': '''
> + #define _GNU_SOURCE
> + #include <sys/mount.h>
> + #include <linux/mount.h>
> + #include <unistd.h>
> + #include <fcntl.h>
> +
> + int main(void) {
> + int fsfd = fsopen("fuse", FSOPEN_CLOEXEC);
> + int res = fsconfig(fsfd, FSCONFIG_SET_STRING, "source", "test", 0);
> + int mntfd = fsmount(fsfd, FSMOUNT_CLOEXEC, 0);
> + res = move_mount(mntfd, "", AT_FDCWD, "/mnt", MOVE_MOUNT_F_EMPTY_PATH);
> + return 0;
> + }
> '''
> }
>
> @@ -308,7 +323,7 @@ configure_file(output: 'libfuse_config.h',
> include_dirs = include_directories('include', 'lib', '.')
>
> # Common dependencies
> -thread_dep = dependency('threads')
> +thread_dep = dependency('threads')
>
> #
> # Read build files from sub-directories
>
> --
> 2.43.0
>
>
^ permalink raw reply [flat|nested] 59+ messages in thread
* Re: [PATCH 12/19] fuse mount: Support synchronous FUSE_INIT (privileged daemon)
2026-03-23 17:45 ` [PATCH 12/19] fuse mount: Support synchronous FUSE_INIT (privileged daemon) Bernd Schubert
@ 2026-03-24 0:03 ` Darrick J. Wong
2026-03-24 20:42 ` Bernd Schubert
0 siblings, 1 reply; 59+ messages in thread
From: Darrick J. Wong @ 2026-03-24 0:03 UTC (permalink / raw)
To: Bernd Schubert
Cc: linux-fsdevel, Miklos Szeredi, Joanne Koong, Bernd Schubert
On Mon, Mar 23, 2026 at 06:45:07PM +0100, Bernd Schubert wrote:
> From: Bernd Schubert <bschubert@ddn.com>
>
> Add synchronous FUSE_INIT processing during mount() to
> enable early daemonization with proper error reporting
> to the parent process.
>
> A new mount thread is needed that handles FUSE_INIT and
> possible other requests at mount time (like getxattr for selinux).
> The kernel sends FUSE_INIT during the mount() syscall. Without a thread
> to process it, mount() blocks forever.
>
> Mount thread lifetime:
> Created before mount() syscall in fuse_start_sync_init_worker()
> Processes requests until se->mount_finished is set (after mount() returns)
> Joined after successful mount in fuse_wait_sync_init_completion()
> Cancelled if mount fails (direct → fusermount3 fallback)
Hrmm, so the main thread (of the child process after daemonization)
calls mount(), having started a child thread to read and process
FUSE_INIT + any other mount-related fuse requests?
Later I see an eventfd being used to signal this background thread that
it should exit and (presumably) let the real request processing queues
take over. I'm curious why an eventfd here when pipes were used
earlier?
> Key changes:
>
> Add init_thread, init_error, mount_finished to struct fuse_session
> Use FUSE_DEV_IOC_SYNC_INIT ioctl for kernel support
> Fall back to async FUSE_INIT if unsupported
> Auto-enabled when fuse_daemonize_active() or via
> fuse_session_want_sync_init()
> Allows parent to report mount/init failures instead of
> exiting immediately after fork.
>
> Note: For now synchronous FUSE_INIT is only supported for privileged
> mounts.
>
> Signed-off-by: Bernd Schubert <bschubert@ddn.com>
> ---
> include/fuse_lowlevel.h | 12 +++
> lib/fuse_daemonize.c | 8 ++
> lib/fuse_i.h | 16 ++++
> lib/fuse_lowlevel.c | 192 ++++++++++++++++++++++++++++++++++++++++++++++--
> lib/mount.c | 5 +-
> lib/mount_fsmount.c | 5 +-
> 6 files changed, 229 insertions(+), 9 deletions(-)
>
> diff --git a/include/fuse_lowlevel.h b/include/fuse_lowlevel.h
> index ee0bd8d71d95e4d57ebb4873dca0f2b36e22a649..d8626f85bdaf497534cd2835a589e30f1f4e2466 100644
> --- a/include/fuse_lowlevel.h
> +++ b/include/fuse_lowlevel.h
> @@ -2429,6 +2429,18 @@ void fuse_session_process_buf(struct fuse_session *se,
> */
> int fuse_session_receive_buf(struct fuse_session *se, struct fuse_buf *buf);
>
> +/**
> + * Request synchronous FUSE_INIT, i.e. FUSE_INIT is handled by the
> + * kernel before mount is returned.
> + *
> + * As FUSE_INIT also starts io-uring ring threads, fork() must not be
> + * called after this if io-uring is enabled. Also see
> + * fuse_session_daemonize_start().
> + *
> + * This must be called before fuse_session_mount() to have any effect.
> + */
> +void fuse_session_want_sync_init(struct fuse_session *se);
> +
> /**
> * Check if the request is submitted through fuse-io-uring
> */
> diff --git a/lib/fuse_daemonize.c b/lib/fuse_daemonize.c
> index 5d191e7d737d04876d02ef6bd526061c003d2ab7..1a32ef74093f231091b4f541e5b9136bff72024f 100644
> --- a/lib/fuse_daemonize.c
> +++ b/lib/fuse_daemonize.c
> @@ -9,6 +9,7 @@
> #define _GNU_SOURCE
>
> #include "fuse_daemonize.h"
> +#include "fuse_i.h"
>
> #include <fcntl.h>
> #include <poll.h>
> @@ -282,3 +283,10 @@ bool fuse_daemonize_active(void)
>
> return dm != NULL && (dm->daemonized || dm->active);
> }
> +
> +bool fuse_daemonize_set(void)
> +{
> + struct fuse_daemonize *dm = atomic_load(&daemonize);
> +
> + return dm != NULL;
> +}
> diff --git a/lib/fuse_i.h b/lib/fuse_i.h
> index 6d63c9fd2149eb4ae3b0e0170640a4ce2eed4705..93ab7ac2fadf9395af70487c7626cc57c2948d56 100644
> --- a/lib/fuse_i.h
> +++ b/lib/fuse_i.h
> @@ -112,6 +112,10 @@ struct fuse_session {
>
> /* synchronous FUSE_INIT support */
> bool want_sync_init;
> + pthread_t init_thread;
> + int init_error;
> + _Atomic bool terminate_mount_worker;
> + int init_wakeup_fd;
>
> /* io_uring */
> struct fuse_session_uring uring;
> @@ -221,7 +225,11 @@ void fuse_chan_put(struct fuse_chan *ch);
> /* Mount-related functions */
> void fuse_mount_version(void);
> void fuse_kern_unmount(const char *mountpoint, int fd);
> +int fuse_kern_mount_get_base_mnt_opts(struct mount_opts *mo, char **mnt_optsp);
> int fuse_kern_mount(const char *mountpoint, struct mount_opts *mo);
> +int fuse_kern_mount_prepare(const char *mountpoint, struct mount_opts *mo);
> +int fuse_kern_do_mount(const char *mountpoint, struct mount_opts *mo,
> + const char *mnt_opts);
>
> int fuse_send_reply_iov_nofree(fuse_req_t req, int error, struct iovec *iov,
> int count);
> @@ -255,6 +263,14 @@ int fuse_session_loop_mt_312(struct fuse_session *se, struct fuse_loop_config *c
> */
> int fuse_loop_cfg_verify(struct fuse_loop_config *config);
>
> +/**
> + * Check if daemonization is set.
> + *
> + * @return true if set, false otherwise
> + */
> +bool fuse_daemonize_set(void);
> +
> +
>
> /*
> * This can be changed dynamically on recent kernels through the
> diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c
> index 626233df20f49fa89cd9327f94340169d7061f75..b10def03f3666757d312f87f177a560483691d6f 100644
> --- a/lib/fuse_lowlevel.c
> +++ b/lib/fuse_lowlevel.c
> @@ -4230,6 +4230,7 @@ fuse_session_new_versioned(struct fuse_args *args,
> goto out1;
> }
> se->fd = -1;
> + se->init_wakeup_fd = -1;
> se->conn.max_write = FUSE_DEFAULT_MAX_PAGES_LIMIT * getpagesize();
> se->bufsize = se->conn.max_write + FUSE_BUFFER_HEADER_SIZE;
> se->conn.max_readahead = UINT_MAX;
> @@ -4402,6 +4403,167 @@ int fuse_session_custom_io_30(struct fuse_session *se,
> }
>
> #if defined(HAVE_NEW_MOUNT_API)
> +
> +/* Worker thread for synchronous FUSE_INIT */
> +static void *session_sync_init_worker(void *data)
> +{
> + struct fuse_session *se = (struct fuse_session *)data;
> + struct fuse_buf fbuf = {
> + .mem = NULL,
> + };
> + struct pollfd pfds[2];
> + int res;
> +
> + pfds[0].fd = se->fd;
> + pfds[0].events = POLLIN;
> + pfds[0].revents = 0;
> + pfds[1].fd = se->init_wakeup_fd;
> + pfds[1].events = POLLIN;
> + pfds[1].revents = 0;
> +
> + /*
> + * Process requests until mount completes. With SELinux there may be
> + * additional requests (like getattr) after FUSE_INIT before mount
> + * returns.
> + */
> + while (!atomic_load(&se->terminate_mount_worker)) {
We don't ever *read* the init_wakeup_fd event, so I wonder if the
_Atomic flag here is really needed? I gather we need acquire semantics
or something?
> + res = poll(pfds, 2, -1);
> + if (res == -1) {
> + if (errno == EINTR)
> + continue;
> + se->init_error = -errno;
> + break;
> + }
> +
> + if (pfds[1].revents & POLLIN)
> + break;
> +
> + if (pfds[0].revents & POLLIN) {
> + res = fuse_session_receive_buf_internal(se, &fbuf, NULL);
> + if (res == -EINTR)
> + continue;
> + if (res <= 0) {
> + se->init_error = res < 0 ? res : -EINVAL;
> + break;
> + }
> +
> + fuse_session_process_buf_internal(se, &fbuf, NULL);
> + }
> + }
> +
> + fuse_buf_free(&fbuf);
> + return NULL;
> +}
> +
> +/* Enable synchronous FUSE_INIT and start worker thread */
> +static int session_start_sync_init(struct fuse_session *se, int fd)
> +{
> + int err;
> + int res;
> +
> + if (!se->want_sync_init &&
> + (se->uring.enable && !fuse_daemonize_set())) {
> + if (se->debug)
> + fuse_log(FUSE_LOG_DEBUG,
> + "fuse: sync init not enabled\n");
> + return 0;
> + }
> +
> + /* Try to enable synchronous FUSE_INIT */
> + res = ioctl(fd, FUSE_DEV_IOC_SYNC_INIT);
> + if (res) {
> + /* ENOTTY means kernel doesn't support sync init - not an error */
> + if (errno != ENOTTY) {
> + fuse_log(
> + FUSE_LOG_ERR,
> + "fuse: failed to enable sync init: %s\n",
> + strerror(errno));
> + } else if (se->debug) {
> + fuse_log(
> + FUSE_LOG_DEBUG,
> + "fuse: kernel doesn't support sync init\n");
> + }
> + return -ENOTTY;
> + }
> +
> + if (se->debug)
> + fuse_log(FUSE_LOG_DEBUG,
> + "fuse: synchronous FUSE_INIT enabled\n");
> +
> + se->init_error = 0;
> + se->terminate_mount_worker = false;
> +
> + se->init_wakeup_fd = eventfd(0, EFD_CLOEXEC);
> + if (se->init_wakeup_fd == -1) {
> + fuse_log(
> + FUSE_LOG_ERR,
> + "fuse: failed to create eventfd for init worker: %s\n",
> + strerror(errno));
> + return -EIO;
> + }
> +
> + err = pthread_create(&se->init_thread, NULL,
> + session_sync_init_worker, se);
> + if (err != 0) {
> + fuse_log(
> + FUSE_LOG_ERR,
> + "fuse: failed to create init worker thread: %s\n",
> + strerror(err));
> + close(se->init_wakeup_fd);
> + se->init_wakeup_fd = -1;
> + return -EIO;
> + }
> +
> + return 0;
> +}
> +
> +/* Wait for synchronous FUSE_INIT to complete */
> +static int session_wait_sync_init_completion(struct fuse_session *se)
> +{
> + void *retval;
> + int err;
> + uint64_t val = 1;
> +
> + if (se->init_thread == 0)
pthread_t is supposed to be an opaque datatype, so "0" could be a valid
value for a running thread. For that matter, it could be a struct. I
think you have to have a separate flag (or just use init_wakeup_fd >= 0)
to determine if we're doing a synchronous init.
> + return 0;
> +
> + se->terminate_mount_worker = true;
> +
> + if (se->init_wakeup_fd != -1) {
> + ssize_t res = write(se->init_wakeup_fd, &val, sizeof(val));
> +
> + if (res != sizeof(val)) {
> + fuse_log(FUSE_LOG_ERR,
> + "fuse: failed to signal init worker: %s\n",
> + strerror(errno));
> + }
> + }
> +
> + err = pthread_join(se->init_thread, &retval);
> + if (err != 0) {
> + fuse_log(FUSE_LOG_ERR, "fuse: failed to join init worker thread: %s\n",
> + strerror(err));
> + return -1;
> + }
> +
> + if (se->init_wakeup_fd != -1) {
> + close(se->init_wakeup_fd);
> + se->init_wakeup_fd = -1;
> + }
> +
> + if (se->init_error != 0) {
> + fuse_log(FUSE_LOG_ERR, "fuse: init worker failed: %d\n", se->init_error);
Could you strerror(-se->init_error) to give the user a real error
message?
> + return -1;
> + }
> +
> + if (fuse_session_exited(se)) {
> + fuse_log(FUSE_LOG_ERR, "FUSE_INIT failed: session exited\n");
> + return -1;
> + }
> +
> + return 0;
> +}
> +
> /* Only linux supports sync FUSE_INIT so far */
> static int fuse_session_mount_new_api(struct fuse_session *se,
> const char *mountpoint)
> @@ -4414,8 +4576,7 @@ static int fuse_session_mount_new_api(struct fuse_session *se,
>
> res = fuse_kern_mount_get_base_mnt_opts(se->mo, &mnt_opts);
> if (res == -1) {
> - fuse_log(FUSE_LOG_ERR,
> - "fuse: failed to get base mount options\n");
> + fuse_log(FUSE_LOG_ERR, "fuse: failed to get base mount options\n");
Unrelated change?
> err = -EIO;
> goto err;
> }
> @@ -4427,6 +4588,17 @@ static int fuse_session_mount_new_api(struct fuse_session *se,
> goto err;
> }
>
> + /*
> + * Enable synchronous FUSE_INIT and start worker thread, sync init
> + * failure is not an error
> + */
> + se->fd = fd;
> + err = session_start_sync_init(se, fd);
> + if (err) {
> + /* ENOTTY means kernel doesn't support sync init - not an error */
> + if (err != -ENOTTY)
> + goto err;
> + }
Perhaps session_start_sync_init should not return ENOTTY, since you
already check in session_wait_sync_init_completion if the synchronous
init worker has been started?
> snprintf(fd_opt, sizeof(fd_opt), "fd=%i", fd);
> if (fuse_opt_add_opt(&mnt_opts_with_fd, mnt_opts) == -1 ||
> fuse_opt_add_opt(&mnt_opts_with_fd, fd_opt) == -1) {
> @@ -4436,13 +4608,16 @@ static int fuse_session_mount_new_api(struct fuse_session *se,
>
> err = fuse_kern_fsmount_mo(mountpoint, se->mo, mnt_opts_with_fd);
> err:
> - if (err) {
> + if (err < 0) {
> if (fd >= 0)
> close(fd);
> fd = -1;
> se->fd = -1;
> se->error = -errno;
> }
> + /* Wait for synchronous FUSE_INIT to complete */
> + if (session_wait_sync_init_completion(se) < 0)
> + fuse_log(FUSE_LOG_ERR, "fuse: sync init completion failed\n");
>
> free(mnt_opts);
> free(mnt_opts_with_fd);
> @@ -4452,8 +4627,8 @@ err:
> static int fuse_session_mount_new_api(struct fuse_session *se,
> const char *mountpoint)
> {
> - (void)se;
> - (void)mountpoint;
> + (void) se;
> + (void) mountpoint;
>
> return -1;
> }
> @@ -4825,3 +5000,10 @@ void fuse_session_stop_teardown_watchdog(void *data)
> pthread_join(tt->thread_id, NULL);
> fuse_tt_destruct(tt);
> }
> +
> +void fuse_session_want_sync_init(struct fuse_session *se)
> +{
> + if (se == NULL)
> + return;
> + se->want_sync_init = true;
> +}
> diff --git a/lib/mount.c b/lib/mount.c
> index 30fd4d2f9bbb84c817b2363b2075456acd1c1255..12df49d9109cf918cc41aa75c5fdf84231d4d5ff 100644
> --- a/lib/mount.c
> +++ b/lib/mount.c
> @@ -30,6 +30,7 @@
> #include <sys/socket.h>
> #include <sys/un.h>
> #include <sys/wait.h>
> +#include <sys/ioctl.h>
>
> #include "fuse_mount_compat.h"
>
> @@ -529,8 +530,8 @@ int fuse_kern_fsmount_mo(const char *mnt, struct mount_opts *mo,
> * Returns: 0 on success, -1 on failure,
> * FUSE_MOUNT_FALLBACK_NEEDED if fusermount should be used
> */
> -static int fuse_kern_do_mount(const char *mnt, struct mount_opts *mo,
> - const char *mnt_opts)
> +int fuse_kern_do_mount(const char *mnt, struct mount_opts *mo,
> + const char *mnt_opts)
> {
> char *source = NULL;
> char *type = NULL;
> diff --git a/lib/mount_fsmount.c b/lib/mount_fsmount.c
> index cba998bc60c783a5edc0c16570f7e5512b7f1253..f1fec790bb80f8815d485a068dc7efdff1746309 100644
> --- a/lib/mount_fsmount.c
> +++ b/lib/mount_fsmount.c
> @@ -287,8 +287,9 @@ int fuse_kern_fsmount(const char *mnt, unsigned long flags, int blkdev,
> /* Try to open filesystem context */
> fsfd = fsopen(type, FSOPEN_CLOEXEC);
> if (fsfd == -1) {
> - fprintf(stderr, "fuse: fsopen(%s) failed: %s\n", type,
> - strerror(errno));
> + if (errno != EPERM)
> + fprintf(stderr, "fuse: fsopen(%s) failed: %s\n", type,
> + strerror(errno));
More unrelated changes?
--D
> return -1;
> }
>
>
> --
> 2.43.0
>
>
^ permalink raw reply [flat|nested] 59+ messages in thread
* Re: [PATCH 13/19] Add fuse_session_set_debug() to enable debug output without foreground
2026-03-23 17:45 ` [PATCH 13/19] Add fuse_session_set_debug() to enable debug output without foreground Bernd Schubert
@ 2026-03-24 0:04 ` Darrick J. Wong
0 siblings, 0 replies; 59+ messages in thread
From: Darrick J. Wong @ 2026-03-24 0:04 UTC (permalink / raw)
To: Bernd Schubert
Cc: linux-fsdevel, Miklos Szeredi, Joanne Koong, Bernd Schubert
On Mon, Mar 23, 2026 at 06:45:08PM +0100, Bernd Schubert wrote:
> From: Bernd Schubert <bschubert@ddn.com>
>
> One might want to print debug out in background mode, which the command
> line parameter does not easily allow. Or one might want to enable and
> disable at run time.
>
> Signed-off-by: Bernd Schubert <bschubert@ddn.com>
Woot! Have been wanting this for fuse4fs for a while :)
Reviewed-by: "Darrick J. Wong" <djwong@kernel.org>
--D
> ---
> include/fuse_lowlevel.h | 12 ++++++++++++
> lib/fuse_lowlevel.c | 5 +++++
> lib/fuse_versionscript | 1 +
> 3 files changed, 18 insertions(+)
>
> diff --git a/include/fuse_lowlevel.h b/include/fuse_lowlevel.h
> index d8626f85bdaf497534cd2835a589e30f1f4e2466..d85929e291a77de8caad7d6b3d9ac5b092ce0e62 100644
> --- a/include/fuse_lowlevel.h
> +++ b/include/fuse_lowlevel.h
> @@ -2441,6 +2441,18 @@ int fuse_session_receive_buf(struct fuse_session *se, struct fuse_buf *buf);
> */
> void fuse_session_want_sync_init(struct fuse_session *se);
>
> +/**
> + * Enable debug output
> + *
> + * This allows to enable debug output without a command line parameter and
> + * without the enforcement of the command line parameter to run in foreground.
> + * The daemon needs to handle either fuse_log output via stderr, or
> + * redirection to its own logs or via syslog.
> + *
> + * @param se the session
> + */
> +void fuse_session_set_debug(struct fuse_session *se);
> +
> /**
> * Check if the request is submitted through fuse-io-uring
> */
> diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c
> index b10def03f3666757d312f87f177a560483691d6f..a7293a3898c37c3877eadf965d310ae2aa5cc2d1 100644
> --- a/lib/fuse_lowlevel.c
> +++ b/lib/fuse_lowlevel.c
> @@ -5007,3 +5007,8 @@ void fuse_session_want_sync_init(struct fuse_session *se)
> return;
> se->want_sync_init = true;
> }
> +
> +void fuse_session_set_debug(struct fuse_session *se)
> +{
> + se->debug = 1;
> +}
> diff --git a/lib/fuse_versionscript b/lib/fuse_versionscript
> index f1765d39e13bc9b1f53e625b9a091c5fa53f5afd..64bf75ac2cb252d066ac301be8fc024b59f903ac 100644
> --- a/lib/fuse_versionscript
> +++ b/lib/fuse_versionscript
> @@ -230,6 +230,7 @@ FUSE_3.19 {
> fuse_daemonize_start;
> fuse_daemonize_signal;
> fuse_daemonize_active;
> + fuse_session_set_debug;
> } FUSE_3.18;
>
> # Local Variables:
>
> --
> 2.43.0
>
>
^ permalink raw reply [flat|nested] 59+ messages in thread
* Re: [PATCH 14/19] Move more generic mount code to mount_util.{c,h}
2026-03-23 17:45 ` [PATCH 14/19] Move more generic mount code to mount_util.{c,h} Bernd Schubert
@ 2026-03-24 0:06 ` Darrick J. Wong
2026-03-24 20:57 ` Bernd Schubert
0 siblings, 1 reply; 59+ messages in thread
From: Darrick J. Wong @ 2026-03-24 0:06 UTC (permalink / raw)
To: Bernd Schubert
Cc: linux-fsdevel, Miklos Szeredi, Joanne Koong, Bernd Schubert
On Mon, Mar 23, 2026 at 06:45:09PM +0100, Bernd Schubert wrote:
> From: Bernd Schubert <bschubert@ddn.com>
>
> This is to allow fusermount to use the code from mount_fsmount.c.
> I.e. avoid code dup and add just re-use the new linux api mount
> functions from that file for fusermount.
>
> Signed-off-by: Bernd Schubert <bschubert@ddn.com>
> ---
> lib/mount.c | 7 -------
> lib/mount_common_i.h | 2 --
> lib/mount_i_linux.h | 3 ++-
> lib/mount_util.c | 10 ++++++++++
> lib/mount_util.h | 5 +++++
> 5 files changed, 17 insertions(+), 10 deletions(-)
>
> diff --git a/lib/mount.c b/lib/mount.c
> index 12df49d9109cf918cc41aa75c5fdf84231d4d5ff..263b05051c236458b830c40181bce7f494803800 100644
> --- a/lib/mount.c
> +++ b/lib/mount.c
> @@ -720,13 +720,6 @@ out:
> return res;
> }
>
> -const char *fuse_mnt_get_devname(void)
> -{
> - const char *devname = getenv(FUSE_KERN_DEVICE_ENV);
> -
> - return devname ? devname : "/dev/fuse";
> -}
> -
> char *fuse_mnt_build_source(const struct mount_opts *mo)
> {
> const char *devname = fuse_mnt_get_devname();
> diff --git a/lib/mount_common_i.h b/lib/mount_common_i.h
> index d27d2aa624ae3806c61a0fe382c2d024080c9bb3..b015e8f98d46980891216e93358aae9f7836d156 100644
> --- a/lib/mount_common_i.h
> +++ b/lib/mount_common_i.h
> @@ -24,8 +24,6 @@ struct mount_opts;
> struct mount_opts *parse_mount_opts(struct fuse_args *args);
> void destroy_mount_opts(struct mount_opts *mo);
> unsigned int get_max_read(struct mount_opts *o);
> -char *fuse_mnt_build_source(const struct mount_opts *mo);
> -char *fuse_mnt_build_type(const struct mount_opts *mo);
>
>
> #endif /* FUSE_MOUNT_COMMON_I_H_ */
> diff --git a/lib/mount_i_linux.h b/lib/mount_i_linux.h
> index 52eab9e650c055142feec329264f82c2b08be0d5..867105019fa57576682091d1a650302f31e450b3 100644
> --- a/lib/mount_i_linux.h
> +++ b/lib/mount_i_linux.h
> @@ -74,6 +74,7 @@ int fuse_kern_fsmount_mo(const char *mnt, struct mount_opts *mo,
>
> int fuse_kern_fsmount_mo(const char *mnt, struct mount_opts *mo,
> const char *mnt_opts);
> -
> +char *fuse_mnt_build_source(const struct mount_opts *mo);
> +char *fuse_mnt_build_type(const struct mount_opts *mo);
These are linux-only things? I thought they applied to BSD too and
should go in mount_util.h...
> #endif /* FUSE_MOUNT_I_H_ */
...but I'm now terribly confused, is mount_i_linux.h just the symbols
needed to build on Linux, or to build anywhere?
> diff --git a/lib/mount_util.c b/lib/mount_util.c
> index a42a02a0b92f98778abb2c491cdc7a01260f56ee..6a7908cb74b429a1ffd811678cfd39c35093b265 100644
> --- a/lib/mount_util.c
> +++ b/lib/mount_util.c
> @@ -10,6 +10,9 @@
>
> #include "fuse_config.h"
> #include "mount_util.h"
> +#ifdef __linux__
> +#include "mount_i_linux.h"
> +#endif
>
> #include <stdio.h>
> #include <unistd.h>
> @@ -399,3 +402,10 @@ int fuse_mnt_add_mount_helper(const char *mnt, const char *source,
> (void)mnt_opts;
> return 0;
> }
> +
> +const char *fuse_mnt_get_devname(void)
> +{
> + const char *devname = getenv(FUSE_KERN_DEVICE_ENV);
> +
> + return devname ? devname : "/dev/fuse";
> +}
> diff --git a/lib/mount_util.h b/lib/mount_util.h
> index 4688d00091f001b3ccd36b1ef3b7e799031ed773..00397943a46a58e640d15b0c0531e5bc6e76006b 100644
> --- a/lib/mount_util.h
> +++ b/lib/mount_util.h
> @@ -6,6 +6,9 @@
> See the file LGPL2.txt.
> */
>
> +#ifndef FUSE_MOUNT_UTIL_H_
> +#define FUSE_MOUNT_UTIL_H_
This is a positive development, at least :)
--D
> +
> #include <sys/types.h>
> #include "mount_common_i.h" // IWYU pragma: keep
>
> @@ -22,3 +25,5 @@ int fuse_mnt_parse_fuse_fd(const char *mountpoint);
> const char *fuse_mnt_get_devname(void);
> int fuse_mnt_add_mount_helper(const char *mnt, const char *source,
> const char *type, const char *mnt_opts);
> +
> +#endif /* FUSE_MOUNT_UTIL_H_ */
>
> --
> 2.43.0
>
>
^ permalink raw reply [flat|nested] 59+ messages in thread
* Re: [PATCH 15/19] Split the fusermount do_mount function
2026-03-23 17:45 ` [PATCH 15/19] Split the fusermount do_mount function Bernd Schubert
@ 2026-03-24 0:14 ` Darrick J. Wong
2026-03-24 21:05 ` Bernd Schubert
0 siblings, 1 reply; 59+ messages in thread
From: Darrick J. Wong @ 2026-03-24 0:14 UTC (permalink / raw)
To: Bernd Schubert
Cc: linux-fsdevel, Miklos Szeredi, Joanne Koong, Bernd Schubert
On Mon, Mar 23, 2026 at 06:45:10PM +0100, Bernd Schubert wrote:
> From: Bernd Schubert <bschubert@ddn.com>
>
> We will need new API and old API and need to pass the options to
> two different functions - factor out the option parsing.
>
> Signed-off-by: Bernd Schubert <bschubert@ddn.com>
> ---
> util/fusermount.c | 298 +++++++++++++++++++++++++++++++++++++-----------------
> 1 file changed, 205 insertions(+), 93 deletions(-)
>
> diff --git a/util/fusermount.c b/util/fusermount.c
> index f17b44f51142c682b339d0ce2287f7c00d644454..ecf509bb80d5cd129f6e582f1ec666502c55603a 100644
> --- a/util/fusermount.c
> +++ b/util/fusermount.c
> @@ -917,30 +917,132 @@ static int mount_notrunc(const char *source, const char *target,
> return mount(source, target, filesystemtype, mountflags, data);
> }
>
> +struct mount_params {
> + /* Input parameters */
> + int fd; /* /dev/fuse file descriptor */
> + mode_t rootmode; /* Root mode from stat */
> + const char *dev; /* Device path (/dev/fuse) */
>
> -static int do_mount(const char *mnt, const char **typep, mode_t rootmode,
> - int fd, const char *opts, const char *dev, char **sourcep,
> - char **mnt_optsp)
> + /* Parsed mount options */
> + unsigned long flags; /* Mount flags (MS_NOSUID, etc.) */
> + char *optbuf; /* Kernel mount options buffer */
> + char *fsname; /* Filesystem name from options */
> + char *subtype; /* Subtype from options */
> + int blkdev; /* Block device flag */
> +
> + /* Generated mount parameters */
> + char *source; /* Mount source string */
> + char *type; /* Filesystem type string */
> + char *mnt_opts; /* Mount table options */
> +
> + /* Pointer for optbuf manipulation */
> + char *optbuf_end; /* Points to end of optbuf for sprintf */
> +};
> +
> +static void free_mount_params(struct mount_params *mp)
> +{
> + free(mp->optbuf);
> + free(mp->fsname);
> + free(mp->subtype);
> + free(mp->source);
> + free(mp->type);
> + free(mp->mnt_opts);
> +}
> +
> +/*
> + * Check if option is deprecated large_read.
> + *
> + * Returns true if the option should be skipped (large_read on kernel > 2.4),
> + * false otherwise (all other options or large_read on old kernels).
> + */
> +static bool check_large_read(const char *opt, unsigned int len)
> +{
> + struct utsname utsname;
> + unsigned int kmaj, kmin;
> + int res;
> +
> + if (!opt_eq(opt, len, "large_read"))
> + return false;
> +
> + res = uname(&utsname);
> + if (res == 0 &&
> + sscanf(utsname.release, "%u.%u", &kmaj, &kmin) == 2 &&
> + (kmaj > 2 || (kmaj == 2 && kmin > 4))) {
> + fprintf(stderr,
> + "%s: note: 'large_read' mount option is deprecated for %i.%i kernels\n",
> + progname, kmaj, kmin);
> + return true;
Ugh, you can drop the uname and all that. Nobody's running 2.4 kernels
anymore, right?
> + }
> + return false;
> +}
> +
> +/*
> + * Check if user has permission to use allow_other or allow_root options.
> + *
> + * Returns -1 if permission denied, 0 if allowed or option is not
> + * allow_other/allow_root.
> + */
> +static int check_allow_permission(const char *opt, unsigned int len)
> +{
> + if (getuid() != 0 && !user_allow_other &&
> + (opt_eq(opt, len, "allow_other") || opt_eq(opt, len, "allow_root"))) {
> + fprintf(stderr, "%s: option %.*s only allowed if 'user_allow_other' is set in %s\n",
> + progname, len, opt, FUSE_CONF);
> + return -1;
> + }
> + return 0;
> +}
> +
> +/*
> + * Process generic mount option.
> + *
> + * Handles mount flags (ro, rw, suid, etc.), kernel options
> + * (default_permissions, allow_other, max_read, blksize), or exits on
> + * unknown options.
> + */
> +static int process_generic_option(const char *opt, unsigned int len,
> + unsigned long *flags, char **dest)
> +{
> + int on;
> + int flag;
> +
> + if (find_mount_flag(opt, len, &on, &flag)) {
> + if (on)
> + *flags |= flag;
> + else
> + *flags &= ~flag;
> + return 0;
> + }
> +
> + if (opt_eq(opt, len, "default_permissions") ||
> + opt_eq(opt, len, "allow_other") ||
> + begins_with(opt, "max_read=") ||
> + begins_with(opt, "blksize=")) {
> + memcpy(*dest, opt, len);
> + *dest += len;
> + **dest = ',';
> + (*dest)++;
> + return 0;
> + }
> +
> + fprintf(stderr, "%s: unknown option '%.*s'\n", progname, len, opt);
> + exit(1);
> +}
> +
> +static int prepare_mount(const char *opts, struct mount_params *mp)
> {
> int res;
> - int flags = MS_NOSUID | MS_NODEV;
> - char *optbuf;
> - char *mnt_opts = NULL;
> const char *s;
> char *d;
> - char *fsname = NULL;
> - char *subtype = NULL;
> - char *source = NULL;
> - char *type = NULL;
> - int blkdev = 0;
>
> - optbuf = (char *) malloc(strlen(opts) + 128);
> - if (!optbuf) {
> + mp->flags = MS_NOSUID | MS_NODEV;
> + mp->optbuf = (char *) malloc(strlen(opts) + 128);
> + if (!mp->optbuf) {
> fprintf(stderr, "%s: failed to allocate memory\n", progname);
> return -1;
> }
>
> - for (s = opts, d = optbuf; *s;) {
> + for (s = opts, d = mp->optbuf; *s;) {
> unsigned len;
> const char *fsname_str = "fsname=";
> const char *subtype_str = "subtype=";
> @@ -953,10 +1055,10 @@ static int do_mount(const char *mnt, const char **typep, mode_t rootmode,
> break;
> }
> if (begins_with(s, fsname_str)) {
> - if (!get_string_opt(s, len, fsname_str, &fsname))
> + if (!get_string_opt(s, len, fsname_str, &mp->fsname))
> goto err;
> } else if (begins_with(s, subtype_str)) {
> - if (!get_string_opt(s, len, subtype_str, &subtype))
> + if (!get_string_opt(s, len, subtype_str, &mp->subtype))
> goto err;
> } else if (opt_eq(s, len, "blkdev")) {
> if (getuid() != 0) {
> @@ -965,7 +1067,7 @@ static int do_mount(const char *mnt, const char **typep, mode_t rootmode,
> progname);
> goto err;
> }
> - blkdev = 1;
> + mp->blkdev = 1;
> } else if (opt_eq(s, len, "auto_unmount")) {
> auto_unmount = 1;
> } else if (!opt_eq(s, len, "nonempty") &&
> @@ -973,122 +1075,132 @@ static int do_mount(const char *mnt, const char **typep, mode_t rootmode,
> !begins_with(s, "rootmode=") &&
> !begins_with(s, "user_id=") &&
> !begins_with(s, "group_id=")) {
> - int on;
> - int flag;
> - int skip_option = 0;
> - if (opt_eq(s, len, "large_read")) {
> - struct utsname utsname;
> - unsigned kmaj, kmin;
> - res = uname(&utsname);
> - if (res == 0 &&
> - sscanf(utsname.release, "%u.%u",
> - &kmaj, &kmin) == 2 &&
> - (kmaj > 2 || (kmaj == 2 && kmin > 4))) {
> - fprintf(stderr, "%s: note: 'large_read' mount option is deprecated for %i.%i kernels\n", progname, kmaj, kmin);
> - skip_option = 1;
> - }
> - }
> - if (getuid() != 0 && !user_allow_other &&
> - (opt_eq(s, len, "allow_other") ||
> - opt_eq(s, len, "allow_root"))) {
> - fprintf(stderr, "%s: option %.*s only allowed if 'user_allow_other' is set in %s\n", progname, len, s, FUSE_CONF);
> + bool skip;
> +
> + if (check_allow_permission(s, len) == -1)
> goto err;
> - }
> - if (!skip_option) {
> - if (find_mount_flag(s, len, &on, &flag)) {
> - if (on)
> - flags |= flag;
> - else
> - flags &= ~flag;
> - } else if (opt_eq(s, len, "default_permissions") ||
> - opt_eq(s, len, "allow_other") ||
> - begins_with(s, "max_read=") ||
> - begins_with(s, "blksize=")) {
> - memcpy(d, s, len);
> - d += len;
> - *d++ = ',';
> - } else {
> - fprintf(stderr, "%s: unknown option '%.*s'\n", progname, len, s);
> - exit(1);
> - }
> - }
> +
> + skip = check_large_read(s, len);
> +
> + /*
> + * Skip deprecated large_read to avoid passing it to
> + * kernel which would reject it as unknown option.
> + */
> + if (!skip)
> + process_generic_option(s, len, &mp->flags, &d);
> }
> s += len;
> if (*s)
> s++;
> }
> *d = '\0';
> - res = get_mnt_opts(flags, optbuf, &mnt_opts);
> + res = get_mnt_opts(mp->flags, mp->optbuf, &mp->mnt_opts);
> if (res == -1)
> goto err;
>
> + mp->optbuf_end = d;
> +
> sprintf(d, "fd=%i,rootmode=%o,user_id=%u,group_id=%u",
> - fd, rootmode, getuid(), getgid());
> + mp->fd, mp->rootmode, getuid(), getgid());
>
> - source = malloc((fsname ? strlen(fsname) : 0) +
> - (subtype ? strlen(subtype) : 0) + strlen(dev) + 32);
> + mp->source = malloc((mp->fsname ? strlen(mp->fsname) : 0) +
> + (mp->subtype ? strlen(mp->subtype) : 0) + strlen(mp->dev) + 32);
>
> - type = malloc((subtype ? strlen(subtype) : 0) + 32);
> - if (!type || !source) {
> + mp->type = malloc((mp->subtype ? strlen(mp->subtype) : 0) + 32);
> + if (!mp->type || !mp->source) {
> fprintf(stderr, "%s: failed to allocate memory\n", progname);
> goto err;
> }
>
> - if (subtype)
> - sprintf(type, "%s.%s", blkdev ? "fuseblk" : "fuse", subtype);
> + if (mp->subtype)
> + sprintf(mp->type, "%s.%s", mp->blkdev ? "fuseblk" : "fuse", mp->subtype);
> else
> - strcpy(type, blkdev ? "fuseblk" : "fuse");
> + strcpy(mp->type, mp->blkdev ? "fuseblk" : "fuse");
Hrm, maybe the patch adding fuse_mnt_build_source / fuse_mnt_build_type
should have done something about this code too.
The straight-up conversion looks ok though.
--D
> - if (fsname)
> - strcpy(source, fsname);
> + if (mp->fsname)
> + strcpy(mp->source, mp->fsname);
> else
> - strcpy(source, subtype ? subtype : dev);
> + strcpy(mp->source, mp->subtype ? mp->subtype : mp->dev);
>
> - res = mount_notrunc(source, mnt, type, flags, optbuf);
> - if (res == -1 && errno == ENODEV && subtype) {
> + return 0;
> +
> +err:
> + free_mount_params(mp);
> + return -1;
> +}
> +
> +/*
> + * Perform the actual mount operation using prepared parameters.
> + *
> + * Returns 0 on success, -1 on failure.
> + */
> +static int perform_mount(const char *mnt, struct mount_params *mp)
> +{
> + int res;
> +
> + res = mount_notrunc(mp->source, mnt, mp->type, mp->flags, mp->optbuf);
> + if (res == -1 && errno == ENODEV && mp->subtype) {
> /* Probably missing subtype support */
> - strcpy(type, blkdev ? "fuseblk" : "fuse");
> - if (fsname) {
> - if (!blkdev)
> - sprintf(source, "%s#%s", subtype, fsname);
> + strcpy(mp->type, mp->blkdev ? "fuseblk" : "fuse");
> + if (mp->fsname) {
> + if (!mp->blkdev)
> + sprintf(mp->source, "%s#%s", mp->subtype, mp->fsname);
> } else {
> - strcpy(source, type);
> + strcpy(mp->source, mp->type);
> }
>
> - res = mount_notrunc(source, mnt, type, flags, optbuf);
> + res = mount_notrunc(mp->source, mnt, mp->type, mp->flags, mp->optbuf);
> }
> if (res == -1 && errno == EINVAL) {
> /* It could be an old version not supporting group_id */
> - sprintf(d, "fd=%i,rootmode=%o,user_id=%u",
> - fd, rootmode, getuid());
> - res = mount_notrunc(source, mnt, type, flags, optbuf);
> + sprintf(mp->optbuf_end, "fd=%i,rootmode=%o,user_id=%u",
> + mp->fd, mp->rootmode, getuid());
> + res = mount_notrunc(mp->source, mnt, mp->type, mp->flags, mp->optbuf);
> }
> if (res == -1) {
> int errno_save = errno;
> - if (blkdev && errno == ENODEV && !fuse_mnt_check_fuseblk())
> + if (mp->blkdev && errno == ENODEV && !fuse_mnt_check_fuseblk())
> fprintf(stderr, "%s: 'fuseblk' support missing\n",
> progname);
> else
> fprintf(stderr, "%s: mount failed: %s\n", progname,
> strerror(errno_save));
> - goto err;
> + return -1;
> }
> - *sourcep = source;
> - *typep = type;
> - *mnt_optsp = mnt_opts;
> - free(fsname);
> - free(optbuf);
>
> return 0;
> +}
>
> -err:
> - free(fsname);
> - free(subtype);
> - free(source);
> - free(type);
> - free(mnt_opts);
> - free(optbuf);
> - return -1;
> +static int do_mount(const char *mnt, const char **typep, mode_t rootmode,
> + int fd, const char *opts, const char *dev, char **sourcep,
> + char **mnt_optsp)
> +{
> + struct mount_params mp = { .fd = fd }; /* implicit zero of other params */
> + int res;
> +
> + mp.rootmode = rootmode;
> + mp.dev = dev;
> +
> + res = prepare_mount(opts, &mp);
> + if (res == -1)
> + return -1;
> +
> + res = perform_mount(mnt, &mp);
> + if (res == -1) {
> + free_mount_params(&mp);
> + return -1;
> + }
> +
> + *sourcep = mp.source;
> + *typep = mp.type;
> + *mnt_optsp = mp.mnt_opts;
> +
> + /* Free only the intermediate allocations, not the returned ones */
> + free(mp.fsname);
> + free(mp.subtype);
> + free(mp.optbuf);
> +
> + return 0;
> }
>
> static int check_perm(const char **mntp, struct stat *stbuf, int *mountpoint_fd)
>
> --
> 2.43.0
>
>
^ permalink raw reply [flat|nested] 59+ messages in thread
* Re: [PATCH 16/19] fusermount: Refactor extract_x_options
2026-03-23 17:45 ` [PATCH 16/19] fusermount: Refactor extract_x_options Bernd Schubert
@ 2026-03-24 0:18 ` Darrick J. Wong
0 siblings, 0 replies; 59+ messages in thread
From: Darrick J. Wong @ 2026-03-24 0:18 UTC (permalink / raw)
To: Bernd Schubert, h
Cc: linux-fsdevel, Miklos Szeredi, Joanne Koong, Bernd Schubert
On Mon, Mar 23, 2026 at 06:45:11PM +0100, Bernd Schubert wrote:
> From: Bernd Schubert <bschubert@ddn.com>
>
> Just to make it better readable.
>
> Also add a NULL check for *regular_opts and *x_prefixed_opts,
> there will be multiple callers in an upcoming commit and
> it becomes harder to track that no caller has an error.
>
> Signed-off-by: Bernd Schubert <bschubert@ddn.com>
Looks fine to me. I would've added the NULL checks as a separate patch,
but this series is already plenty long...
Reviewed-by: "Darrick J. Wong" <djwong@kernel.org>
--D
> ---
> util/fusermount.c | 58 ++++++++++++++++++++++++++++++++-----------------------
> 1 file changed, 34 insertions(+), 24 deletions(-)
>
> diff --git a/util/fusermount.c b/util/fusermount.c
> index ecf509bb80d5cd129f6e582f1ec666502c55603a..80b42a594e89cdc2f43824f5e274892522fd8cce 100644
> --- a/util/fusermount.c
> +++ b/util/fusermount.c
> @@ -112,22 +112,31 @@ static struct mntent *GETMNTENT(FILE *stream)
>
> /*
> * Take a ',' separated option string and extract "x-" options
> + * @original: The original option string
> + * @regular_opts: The regular options
> + * @x_prefixed_opts: The "x-" options
> */
> -static int extract_x_options(const char *original, char **non_x_opts,
> - char **x_opts)
> +static int extract_x_options(const char *original, char **regular_opts,
> + char **x_prefixed_opts)
> {
> size_t orig_len;
> const char *opt, *opt_end;
>
> orig_len = strlen(original) + 1;
>
> - *non_x_opts = calloc(1, orig_len);
> - *x_opts = calloc(1, orig_len);
> + if (*regular_opts != NULL || *x_prefixed_opts != NULL) {
> + fprintf(stderr, "%s: regular_opts or x_prefixed_opts not NULL\n",
> + __func__);
> + return -EINVAL;
> + }
>
> - size_t non_x_opts_len = orig_len;
> - size_t x_opts_len = orig_len;
> + *regular_opts = calloc(1, orig_len);
> + *x_prefixed_opts = calloc(1, orig_len);
>
> - if (*non_x_opts == NULL || *x_opts == NULL) {
> + size_t regular_opts_len = orig_len;
> + size_t x_prefixed_opts_len = orig_len;
> +
> + if (*regular_opts == NULL || *x_prefixed_opts == NULL) {
> fprintf(stderr, "%s: Failed to allocate %zuB.\n",
> __func__, orig_len);
> return -ENOMEM;
> @@ -143,16 +152,16 @@ static int extract_x_options(const char *original, char **non_x_opts,
> size_t opt_len = opt_end - opt;
> size_t opt_len_left = orig_len - (opt - original);
> size_t buf_len;
> - bool is_x_opts;
> + bool is_x_prefixed_opts;
>
> if (strncmp(opt, "x-", MIN(2, opt_len_left)) == 0) {
> - buf_len = x_opts_len;
> - is_x_opts = true;
> - opt_buf = *x_opts;
> + buf_len = x_prefixed_opts_len;
> + is_x_prefixed_opts = true;
> + opt_buf = *x_prefixed_opts;
> } else {
> - buf_len = non_x_opts_len;
> - is_x_opts = false;
> - opt_buf = *non_x_opts;
> + buf_len = regular_opts_len;
> + is_x_prefixed_opts = false;
> + opt_buf = *regular_opts;
> }
>
> if (buf_len < orig_len) {
> @@ -163,7 +172,8 @@ static int extract_x_options(const char *original, char **non_x_opts,
> /* omits ',' */
> if ((ssize_t)(buf_len - opt_len) < 0) {
> /* This would be a bug */
> - fprintf(stderr, "%s: no buf space left in copy, orig='%s'\n",
> + fprintf(stderr,
> + "%s: no buf space left in copy, orig='%s'\n",
> __func__, original);
> return -EIO;
> }
> @@ -171,10 +181,10 @@ static int extract_x_options(const char *original, char **non_x_opts,
> strncat(opt_buf, opt, opt_end - opt);
> buf_len -= opt_len;
>
> - if (is_x_opts)
> - x_opts_len = buf_len;
> + if (is_x_prefixed_opts)
> + x_prefixed_opts_len = buf_len;
> else
> - non_x_opts_len = buf_len;
> + regular_opts_len = buf_len;
> }
>
> return 0;
> @@ -1379,7 +1389,7 @@ static int mount_fuse(const char *mnt, const char *opts, const char **type)
> const char *real_mnt = mnt;
> int mountpoint_fd = -1;
> char *do_mount_opts = NULL;
> - char *x_opts = NULL;
> + char *x_prefixed_opts = NULL;
>
> fd = open_fuse_device(dev);
> if (fd == -1)
> @@ -1397,7 +1407,7 @@ static int mount_fuse(const char *mnt, const char *opts, const char **type)
> }
>
> // Extract any options starting with "x-"
> - res= extract_x_options(opts, &do_mount_opts, &x_opts);
> + res = extract_x_options(opts, &do_mount_opts, &x_prefixed_opts);
> if (res)
> goto fail_close_fd;
>
> @@ -1420,14 +1430,14 @@ static int mount_fuse(const char *mnt, const char *opts, const char **type)
> }
>
> if (geteuid() == 0) {
> - if (x_opts && strlen(x_opts) > 0) {
> + if (x_prefixed_opts && strlen(x_prefixed_opts) > 0) {
> /*
> * Add back the options starting with "x-" to opts from
> * do_mount. +2 for ',' and '\0'
> */
> size_t mnt_opts_len = strlen(mnt_opts);
> size_t x_mnt_opts_len = mnt_opts_len+
> - strlen(x_opts) + 2;
> + strlen(x_prefixed_opts) + 2;
> char *x_mnt_opts = calloc(1, x_mnt_opts_len);
>
> if (mnt_opts_len) {
> @@ -1435,7 +1445,7 @@ static int mount_fuse(const char *mnt, const char *opts, const char **type)
> strncat(x_mnt_opts, ",", 2);
> }
>
> - strncat(x_mnt_opts, x_opts,
> + strncat(x_mnt_opts, x_prefixed_opts,
> x_mnt_opts_len - mnt_opts_len - 2);
>
> free(mnt_opts);
> @@ -1452,7 +1462,7 @@ static int mount_fuse(const char *mnt, const char *opts, const char **type)
> out_free:
> free(source);
> free(mnt_opts);
> - free(x_opts);
> + free(x_prefixed_opts);
> free(do_mount_opts);
>
> return fd;
>
> --
> 2.43.0
>
>
^ permalink raw reply [flat|nested] 59+ messages in thread
* Re: [PATCH 00/19] libfuse: Add support for synchronous init
2026-03-23 17:44 [PATCH 00/19] libfuse: Add support for synchronous init Bernd Schubert
` (18 preceding siblings ...)
2026-03-23 17:45 ` [PATCH 19/19] Add support for sync-init of unprivileged daemons Bernd Schubert
@ 2026-03-24 0:19 ` Darrick J. Wong
19 siblings, 0 replies; 59+ messages in thread
From: Darrick J. Wong @ 2026-03-24 0:19 UTC (permalink / raw)
To: Bernd Schubert
Cc: linux-fsdevel, Miklos Szeredi, Joanne Koong, Bernd Schubert
On Mon, Mar 23, 2026 at 06:44:55PM +0100, Bernd Schubert wrote:
> I'm taking Darricks example here and posting API changing libfuse
> changes to linux-fsdevel. We should consider to create a fuse
O^o
> specific list if that is too much. The existing
> fuse-devel@lists.sourceforge.net is rather hopeless due to lack
> of spam filtering.
Yeah, it'd be nice to have a separate list, fsdevel is drinking from the
firehose.
> The first few patches in this series are just preparation, after that
> follow the important parts:
>
> 1) New daemonize API, see that commit for details. In short, the existing
> fuse_daemonize() was not sufficient for complex daemons and is impossible
> to use with sync-init and the current way to start fuse-io-uring ring
> threads.
>
> 2) Support for privileged daemons, still rather straight forward, but
> requires the startup of a worker thread that handles requests until
> mount is complete.
>
> 3) Privileged daemons - requirement is to update the API with fusermount,
> because fuse_session_mount_new_api() needs to obtain the /dev/fuse file
> descriptor, then start the worker thread with that fd and then continue
> the actual mount through fusermount.
Heh, I'll get back to the last part tomorrow.
--D
> To: linux-fsdevel@vger.kernel.org
> Cc: Miklos Szeredi <miklos@szeredi.hu>
> Cc: Joanne Koong <joannelkoong@gmail.com>
> Cc: Darrick J. Wong <djwong@kernel.org>
> Joanne Koong <joannelkoong@gmail.com>
>
> Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
> ---
> Bernd Schubert (19):
> ci-build: Add environment logging
> Add 'STRCPY' to the checkpatch ignore option
> checkpatch.pl: Add _Atomic to $Attribute patttern
> Add a new daemonize API
> Sync fuse_kernel.h with linux-6.18
> mount.c: Split fuse_mount_sys to prepare privileged sync FUSE_INIT
> Add FUSE_MOUNT_FALLBACK_NEEDED define for -2 mount errors
> Refactor mount code / move common functions to mount_util.c
> Move mount flags to mount_i.h
> conftest.py: Add more valgrind filter patterns
> Add support for the new linux mount API
> fuse mount: Support synchronous FUSE_INIT (privileged daemon)
> Add fuse_session_set_debug() to enable debug output without foreground
> Move more generic mount code to mount_util.{c,h}
> Split the fusermount do_mount function
> fusermount: Refactor extract_x_options
> Make fusermount work bidirectional for sync init
> New mount API: Filter out "user="
> Add support for sync-init of unprivileged daemons
>
> .github/workflows/checkpatch.yml | 2 +-
> checkpatch.pl | 3 +-
> doc/README.daemonize | 186 +++++++++++
> doc/README.fusermount | 359 +++++++++++++++++++++
> doc/README.mount | 86 +++++
> doc/README.sync-init | 184 +++++++++++
> example/passthrough_hp.cc | 18 +-
> include/fuse_daemonize.h | 71 +++++
> include/fuse_kernel.h | 1 +
> include/fuse_lowlevel.h | 24 ++
> include/meson.build | 3 +-
> lib/fuse_daemonize.c | 292 +++++++++++++++++
> lib/fuse_i.h | 27 +-
> lib/fuse_lowlevel.c | 342 +++++++++++++++++++-
> lib/fuse_versionscript | 4 +
> lib/helper.c | 13 +-
> lib/meson.build | 6 +-
> lib/mount.c | 380 +++++++++++++++-------
> lib/mount_bsd.c | 1 +
> lib/mount_common_i.h | 29 ++
> lib/mount_fsmount.c | 417 ++++++++++++++++++++++++
> lib/mount_i_linux.h | 87 +++++
> lib/mount_util.c | 34 ++
> lib/mount_util.h | 11 +
> meson.build | 19 +-
> test/ci-build.sh | 16 +
> test/conftest.py | 7 +-
> test/test_want_conversion.c | 1 +
> util/fusermount.c | 671 +++++++++++++++++++++++++++++++--------
> util/meson.build | 2 +-
> 30 files changed, 3024 insertions(+), 272 deletions(-)
> ---
> base-commit: 9eba0f3c9e8b5af7b252093bb6f81f086bb35563
> change-id: 20260323-fuse-init-before-mount-8f5b09a1acf1
>
> Best regards,
> --
> Bernd Schubert <bernd@bsbernd.com>
>
>
^ permalink raw reply [flat|nested] 59+ messages in thread
* Re: [PATCH 04/19] Add a new daemonize API
2026-03-23 22:28 ` Darrick J. Wong
@ 2026-03-24 17:36 ` Bernd Schubert
2026-03-24 22:20 ` Darrick J. Wong
0 siblings, 1 reply; 59+ messages in thread
From: Bernd Schubert @ 2026-03-24 17:36 UTC (permalink / raw)
To: Darrick J. Wong; +Cc: linux-fsdevel, Miklos Szeredi, Joanne Koong
On 3/23/26 23:28, Darrick J. Wong wrote:
> On Mon, Mar 23, 2026 at 06:44:59PM +0100, Bernd Schubert wrote:
>> In complex fuse file systems one often wants
>> a) fork
>> b) start extra threads and system initialization (like network
>> connection and RDMA memory registration).
>> c) Start the fuse session
>>
>> With fuse_daemonize() there is no way to return a notification
>> if step b) or c) failed. Therefore exising examples do
>> the fuse_daemonize() after fuse_session_mount().
>> Step, i.e. starting extra threads and possible failures are
>> do not exist in these examples - unhandled use case.
>
> Er, this sentence is a little garbled. Is this supposed to say "The
> example servers do not cover step b. This is an un-demonstrated use
> case"?
Oops sorry. How about
Existing example/ file systems do the fuse_daemonize() after
fuse_session_mount() - i.e. after the mount point is already
established. Though, these example/ daemons do not start
extra threads and do not need network initialization either.
fuse_daemonize() also does not allow to return notification
from the forked child to the parent.
Complex fuse file system daemons often want the order of
1) fork - parent watches, child does the work
Child:
2) start extra threads and system initialization (like network
connection and RDMA memory registration) from the fork child.
3) Start the fuse session after everything else succeeded
Parent:
Report child initialization success or failure
>
>> With the FUSE_SYNC_INIT feature, which does FUSE_INIT at mount time
>> and not after the mount anymore, it becomes even more complex
>> as FUSE_INIT triggers startup of fuse_io_uring threads. That
>> means for FUSE_SYNC_INIT forking/daemonization has to be done
>> _before_ the fuse_session_mount().
>
> Hey, that /is/ a neat trick!
>
>> A new API is introduced to overcome the limitations of
>> fuse_daemonize()
>>
>> fuse_daemonize_start() - fork, but foreground process does not
>> terminate yet and watches the background.
>>
>> fuse_daemonize_signal() - background daemon signals to
>> the foreground process success or failure.
>>
>> fuse_daemonize_active() - helper function for the high level
>> interface, which needs to handle both APIs. fuse_daemonize()
>> is called within fuse_main(), which now needs to know if the caller
>> actually already used the new API itself.
>>
>> The object 'struct fuse_daemonize *' is allocated dynamically
>> and stored a global variable in fuse_daemonize.c, because
>> - high level fuse_main_real_versioned() needs to know
>> if already daemonized
>> - high level daemons do not have access to struct fuse_session
>> - FUSE_SYNC_INIT in later commits can only be done if the new
>> API (or a file system internal) daemonization is used.
>>
>> Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
>> ---
>> doc/README.daemonize | 186 +++++++++++++++++++++++++++++
>> example/passthrough_hp.cc | 18 ++-
>> include/fuse_daemonize.h | 71 +++++++++++
>> include/meson.build | 3 +-
>> lib/fuse_daemonize.c | 284 ++++++++++++++++++++++++++++++++++++++++++++
>> lib/fuse_i.h | 4 +-
>> lib/fuse_lowlevel.c | 1 +
>> lib/fuse_versionscript | 3 +
>> lib/helper.c | 13 +-
>> lib/meson.build | 3 +-
>> test/test_want_conversion.c | 1 +
>> 11 files changed, 576 insertions(+), 11 deletions(-)
>>
>> diff --git a/doc/README.daemonize b/doc/README.daemonize
>> new file mode 100644
>> index 0000000000000000000000000000000000000000..f7473361b3a39369291e9401dfc5f7928ff4160b
>> --- /dev/null
>> +++ b/doc/README.daemonize
>> @@ -0,0 +1,186 @@
>> +FUSE Daemonization API
>> +======================
>> +
>> +This document describes the FUSE daemonization APIs, including the legacy
>> +fuse_daemonize() function and the new fuse_daemonize_start()/signal() API
>> +introduced in libfuse 3.19.
>> +
>> +
>> +Overview
>> +--------
>> +
>> +FUSE filesystems often need to run as background daemons. Daemonization
>> +involves forking the process, creating a new session, and redirecting
>> +standard file descriptors. The challenge is properly reporting initialization
>> +failures to the parent process.
>
> Yeah, I found this part pretty murky when writing fuse[24]fs -- the
> internal library failures are fuse_log()'d, but then what if the fuse
> server has its own logging library? libext2fs has its own, and there's
> rather a lot of shenanigans that go on to make sure that libfuse and
> libext2fs print to the same streams.
Did you find fuse_set_log_func()?
>
> If you want to run as a systemd service, stdout/stderr are usually
> routed to journald so there's no need to mess with stdout/stderr. But
> then if you're running as a system service you don't want daemonize
> anyway.
>
> Ok enough rambling about my thing. I appreciate you documenting the old
> fuse_daemonize in detail:
>
>> +Old API: fuse_daemonize()
>> +--------------------------
>> +
>> +Function signature:
>> + int fuse_daemonize(int foreground);
>> +
>> +Location: lib/helper.c
>> +
>> +This is the legacy daemonization API, primarily used with the high-level
>> +fuse_main() interface.
>> +
>> +Behavior:
>> +- If foreground=0: forks the process, creates new session, redirects stdio
>
> It creates a new session? I see that a pipe gets created between parent
> and child, but I don't see a new fuse_session or ... oh, you meant a new
> *Unix* session via setsid(). Can you say "creates new Unix session"?
>
>> +- If foreground=1: only changes directory to "/"
>> +- Parent waits for a single byte on a pipe before exiting
>> +- Child writes completion byte immediately after redirecting stdio
>> +- Always changes directory to "/"
>> +
>> +Limitations:
>> +1. No failure reporting: The parent receives notification immediately after
>> + fork/setsid, before any meaningful initialization (like mounting the
>> + filesystem or starting threads).
>
> <nod> and I gather that's what fuse_daemonize_signal() is for.
>
>> +2. Timing constraint: Must be called AFTER fuse_session_mount() in existing
>> + examples, because there's no way to report mount failures to the parent.
>> +
>> +3. Thread initialization: Cannot report failures from complex initialization
>> + steps like:
>> + - Starting worker threads
>
> Where do those errors go? It looks like by default they go to stderr,
> which is /dev/null after daemonization, right?
Yeah, one way for fuse_log() is fuse_log_enable_syslog().
>
>> + - Network connection setup
>> + - RDMA memory registration
>> + - Resource allocation
>> +
>> +4. FUSE_SYNC_INIT incompatibility: With the FUSE_SYNC_INIT feature, FUSE_INIT
>> + happens at mount time and may start io_uring threads. This requires
>> + daemonization BEFORE mount, which the old API cannot handle properly.
>
> (For anyone following at home, fuse-containers cannot use synchronous
> init because the new fuservicemount helper opens /dev/fuse and calls
> mount() at the behest of the fuse server, so I'm mostly interested in
> this as an exercise in getting to know libfuse.)
Hmm, that is a pity. And many many thanks for your reviews!
>
>> +
>> +Example usage (old API):
>> + fuse = fuse_new(...);
>> + fuse_mount(fuse, mountpoint);
>> + fuse_daemonize(opts.foreground); // After mount, can't report mount failure
>> + fuse_set_signal_handlers(se);
>> + fuse_session_loop(se);
>> +
>> +
>> +New API: fuse_daemonize_start() / fuse_daemonize_signal()
>> +----------------------------------------------------------
>> +
>> +Functions:
>> + int fuse_daemonize_start(unsigned int flags);
>> + void fuse_daemonize_signal(int status);
>> + bool fuse_daemonize_active(void);
>> +
>> +Location: lib/fuse_daemonize.c, include/fuse_daemonize.h
>> +Available since: libfuse 3.19
>> +
>> +This new API solves the limitations of fuse_daemonize() by splitting
>> +daemonization into two phases:
>> +
>> +1. fuse_daemonize_start() - Fork and setup, but parent waits
>> +2. fuse_daemonize_signal() - Signal success/failure to parent
>> +
>> +
>> +fuse_daemonize_start()
>> +----------------------
>> +
>> +Flags:
>> +- FUSE_DAEMONIZE_NO_CHDIR: Don't change directory to "/"
>> +- FUSE_DAEMONIZE_NO_BACKGROUND: Don't fork (foreground mode)
>> +
>> +Behavior:
>> +- Unless NO_BACKGROUND: forks the process
>> +- Parent waits for status signal from child
>> +- Child creates new session and continues
>> +- Unless NO_CHDIR: changes directory to "/"
>> +- Closes stdin immediately in child
>> +- Starts a watcher thread to detect parent death
>> +- Returns 0 in child on success, negative errno on error
>> +
>> +Parent death detection:
>> +- Uses a "death pipe" - parent keeps write end open
>> +- Child's watcher thread polls the read end
>> +- When parent dies, pipe gets POLLHUP and child exits
>> +- Prevents orphaned daemons if parent is killed
>> +
>> +
>> +fuse_daemonize_signal()
>> +-----------------------
>> +
>> +Status values:
>> +- FUSE_DAEMONIZE_SUCCESS (0): Initialization succeeded
>> +- FUSE_DAEMONIZE_FAILURE (1): Initialization failed
>> +
>> +Behavior:
>> +- Signals the parent process with success or failure status
>
> Does this get written back through the death pipe? Or does it use
> process signals?
/* Private/internal data */
struct fuse_daemonize {
unsigned int flags;
int signal_pipe_wr; /* write end for signaling parent */
int death_pipe_rd; /* read end, POLLHUP when parent dies */
I.e. it writes through signal_pipe_wr. I probably could simplify it and
just use a single pipe. Although from my point of view, these are
details that can be improved later. I'm quite under time pressure...
>
>> +- Parent exits with EXIT_SUCCESS or EXIT_FAILURE accordingly
>> +- On success: redirects stdout/stderr to /dev/null
>> +- Stops the parent watcher thread
>> +- Cleans up pipes and internal state
>
> I think these might be better called fuse_daemonize_success() and
> fuse_daemonize_fail() since one of them does more than just signal the
> parent process.
Absolutely and thanks a lot! The API functions are the main reason why I
sent it to the list - once the API is in place I cann change it anymore
after a libfuse
release.
>
>> +- Safe to call multiple times
>> +- Safe to call even if fuse_daemonize_start() failed
>> +
>> +
>> +fuse_daemonize_active()
>> +-----------------------
>> +
>> +Returns true if daemonization is active and waiting for signal.
>> +
>> +Used by the high-level fuse_main() to detect if the application already
>> +called the new API, avoiding double-daemonization.
>> +
>> +
>> +Example usage (new API):
>> +-------------------------
>> +
>> + // Start daemonization BEFORE mount
>> + unsigned int daemon_flags = 0;
>> + if (foreground)
>> + daemon_flags |= FUSE_DAEMONIZE_NO_BACKGROUND;
>> +
>> + if (fuse_daemonize_start(daemon_flags) != 0)
>> + goto error;
>> +
>> + // Mount can now fail and be reported to parent
>> + if (fuse_session_mount(se, mountpoint) != 0)
>> + goto error_signal;
>> +
>> + // Complex initialization can fail and be reported
>> + if (setup_threads() != 0)
>> + goto error_signal;
>> +
>> + if (setup_network() != 0)
>> + goto error_signal;
>
> Hrmm, interesting, this solves the problem of fuse2fs having to wait
> until FUSE_INIT to create background threads.
So exactly what the new API for.
I turned the order around in the example above, though. I think one
first wants to set up threads and network and then mount.
>
>> +
>> + // Signal success - parent exits with EXIT_SUCCESS
>> + // This is typically done in the init() callback after FUSE_INIT
>> + fuse_daemonize_signal(FUSE_DAEMONIZE_SUCCESS);
>> +
>> + // Run main loop
>> + fuse_session_loop(se);
>> +
>> + return 0;
>> +
>> +error_signal:
>> + // Signal failure - parent exits with EXIT_FAILURE
>> + fuse_daemonize_signal(FUSE_DAEMONIZE_FAILURE);
>> +error:
>> + return 1;
>> +
>> +
>> +When to signal success
>> +----------------------
>> +
>> +The success signal should be sent after all critical initialization is
>> +complete. For FUSE filesystems, this is typically in the init() callback,
>> +after FUSE_INIT has been processed successfully.
>> +
>> +Example (from passthrough_hp.cc):
>> + static void sfs_init(void *userdata, fuse_conn_info *conn) {
>> + // ... initialization ...
>> + fuse_daemonize_signal(FUSE_DAEMONIZE_SUCCESS);
>> + }
>> +
>> +This ensures the parent only exits after:
>> +- Mount succeeded
>> +- FUSE_INIT completed
>> +- All threads started
>> +- Filesystem is ready to serve requests
>
> Very nice! Does this new daemonization work for async-init servers?
Sure. See passthrough_hp.cc - it does the fuse_daemonize_success() in
sfs_init(). With async-init that is long after the mount, with sync-init
that is as part of the mount.
>
>> diff --git a/example/passthrough_hp.cc b/example/passthrough_hp.cc
>> index 9f795c5546ee8312e1393c5b8fcebfd77724fb49..a0da5c7e6ab07f921df65db9c700e7de77fe1598 100644
>> --- a/example/passthrough_hp.cc
>> +++ b/example/passthrough_hp.cc
>> @@ -55,6 +55,7 @@
>> #include <errno.h>
>> #include <ftw.h>
>> #include <fuse_lowlevel.h>
>> +#include <fuse_daemonize.h>
>> #include <inttypes.h>
>> #include <string.h>
>> #include <sys/file.h>
>> @@ -243,6 +244,9 @@ static void sfs_init(void *userdata, fuse_conn_info *conn)
>>
>> /* Try a large IO by default */
>> conn->max_write = 4 * 1024 * 1024;
>> +
>> + /* Signal successful init to parent */
>> + fuse_daemonize_signal(FUSE_DAEMONIZE_SUCCESS);
>> }
>>
>> static void sfs_getattr(fuse_req_t req, fuse_ino_t ino, fuse_file_info *fi)
>> @@ -1580,6 +1584,7 @@ int main(int argc, char *argv[])
>> {
>> struct fuse_loop_config *loop_config = NULL;
>> void *teardown_watchog = NULL;
>> + unsigned int daemon_flags = 0;
>>
>> // Parse command line options
>> auto options{ parse_options(argc, argv) };
>> @@ -1638,10 +1643,14 @@ int main(int argc, char *argv[])
>>
>> fuse_loop_cfg_set_clone_fd(loop_config, fs.clone_fd);
>>
>> - if (fuse_session_mount(se, argv[2]) != 0)
>> + /* Start daemonization before mount so parent can report mount failure */
>> + if (fs.foreground)
>> + daemon_flags |= FUSE_DAEMONIZE_NO_BACKGROUND;
>> + if (fuse_daemonize_start(daemon_flags) != 0)
>> goto err_out3;
>>
>> - fuse_daemonize(fs.foreground);
>> + if (fuse_session_mount(se, argv[2]) != 0)
>> + goto err_out4;
>>
>> if (!fs.foreground)
>> fuse_log_enable_syslog("passthrough-hp", LOG_PID | LOG_CONS,
>> @@ -1650,7 +1659,7 @@ int main(int argc, char *argv[])
>> teardown_watchog = fuse_session_start_teardown_watchdog(
>> se, fs.root.stop_timeout_secs, NULL, NULL);
>> if (teardown_watchog == NULL)
>> - goto err_out3;
>> + goto err_out4;
>>
>> if (options.count("single"))
>> ret = fuse_session_loop(se);
>> @@ -1659,6 +1668,9 @@ int main(int argc, char *argv[])
>>
>> fuse_session_unmount(se);
>>
>> +err_out4:
>> + if (fuse_daemonize_active())
>> + fuse_daemonize_signal(FUSE_DAEMONIZE_FAILURE);
>> err_out3:
>> fuse_remove_signal_handlers(se);
>> err_out2:
>> diff --git a/include/fuse_daemonize.h b/include/fuse_daemonize.h
>> new file mode 100644
>> index 0000000000000000000000000000000000000000..f49f6aa580a93547198e7dabe65498708e65d024
>> --- /dev/null
>> +++ b/include/fuse_daemonize.h
>> @@ -0,0 +1,71 @@
>> +/*
>> + * FUSE: Filesystem in Userspace
>> + * Copyright (C) 2026 Bernd Schubert <bsbernd.com>
>> + *
>> + * This program can be distributed under the terms of the GNU LGPLv2.
>> + * See the file COPYING.LIB.
>> + *
>> + */
>> +
>> +#ifndef FUSE_DAEMONIZE_H_
>> +#define FUSE_DAEMONIZE_H_
>> +
>> +#include <stdint.h>
>> +#include <stdbool.h>
>> +
>> +#ifdef __cplusplus
>> +extern "C" {
>> +#endif
>> +
>> +/**
>> + * Flags for fuse_daemonize_start()
>> + */
>> +#define FUSE_DAEMONIZE_NO_CHDIR (1 << 0)
>> +#define FUSE_DAEMONIZE_NO_BACKGROUND (1 << 1)
>> +
>> +/**
>> + * Status values for fuse_daemonize_signal()
>> + */
>> +#define FUSE_DAEMONIZE_SUCCESS 0
>> +#define FUSE_DAEMONIZE_FAILURE 1
>
> What if fuse_daemonize_signal() took an exitcode and passed it directly
> to the parent process via the death pipe, and the parent process can
> pass that to exit()? IOWs, what if fuse_daemonize_signal writes @status
> into signal_pipe_wr instead of flattening it to EXIT_SUCCESS/FAILURE?
>
> Or, if you want to constrain the values to binary, then why not use
>
> #define FUSE_DAEMONIZE_SUCCESS (EXIT_SUCCESS) ?
Good idea. Updated to
/**
* Signal daemonization failure to parent and cleanup.
*
* @param err error code to pass to parent
*/
void fuse_daemonize_fail(int err);
>
>> +
>> +/**
>> + * Start daemonization process.
>> + *
>> + * Unless FUSE_DAEMONIZE_NO_BACKGROUND is set, this forks the process.
>> + * The parent waits for a signal from the child via fuse_daemonize_signal().
>> + * The child returns from this call and continues setup.
>> + *
>> + * Unless FUSE_DAEMONIZE_NO_CHDIR is set, changes directory to "/".
>> + *
>> + * Must be called before fuse_session_mount().
>> + *
>> + * @param flags combination of FUSE_DAEMONIZE_* flags
>> + * @return 0 on success, negative errno on error
>> + */
>> +int fuse_daemonize_start(unsigned int flags);
>> +
>> +/**
>> + * Signal daemonization status to parent and cleanup.
>> + *
>> + * The child calls this after setup is complete (or failed).
>> + * The parent receives the status and exits with it.
>> + * Safe to call multiple times or if start failed.
>> + *
>> + * @param status FUSE_DAEMONIZE_SUCCESS or FUSE_DAEMONIZE_FAILURE
>> + */
>> +void fuse_daemonize_signal(int status);
>> +
>> +/**
>> + * Check if daemonization is active and waiting for signal.
>> + *
>> + * @return true if active, false otherwise
>> + */
>> +bool fuse_daemonize_active(void);
>> +
>> +#ifdef __cplusplus
>> +}
>> +#endif
>> +
>> +#endif /* FUSE_DAEMONIZE_H_ */
>> +
>> diff --git a/include/meson.build b/include/meson.build
>> index bf671977a5a6a9142bd67aceabd8a919e3d968d0..cfbaf52ac5d84369e92948c631e2fcfdd04ac2eb 100644
>> --- a/include/meson.build
>> +++ b/include/meson.build
>> @@ -1,4 +1,5 @@
>> libfuse_headers = [ 'fuse.h', 'fuse_common.h', 'fuse_lowlevel.h',
>> - 'fuse_opt.h', 'cuse_lowlevel.h', 'fuse_log.h' ]
>> + 'fuse_opt.h', 'cuse_lowlevel.h', 'fuse_log.h',
>> + 'fuse_daemonize.h' ]
>>
>> install_headers(libfuse_headers, subdir: 'fuse3')
>> diff --git a/lib/fuse_daemonize.c b/lib/fuse_daemonize.c
>> new file mode 100644
>> index 0000000000000000000000000000000000000000..5d191e7d737d04876d02ef6bd526061c003d2ab7
>> --- /dev/null
>> +++ b/lib/fuse_daemonize.c
>> @@ -0,0 +1,284 @@
>> +/*
>> + * FUSE: Filesystem in Userspace
>> + * Copyright (C) 2026 Bernd Schubert <bsbernd.com>
>> + *
>> + * This program can be distributed under the terms of the GNU LGPLv2.
>> + * See the file COPYING.LIB.
>> + */
>> +
>> +#define _GNU_SOURCE
>> +
>> +#include "fuse_daemonize.h"
>> +
>> +#include <fcntl.h>
>> +#include <poll.h>
>> +#include <pthread.h>
>> +#include <stdatomic.h>
>> +#include <stdio.h>
>> +#include <stdlib.h>
>> +#include <string.h>
>> +#include <sys/types.h>
>> +#include <unistd.h>
>> +#include <stdbool.h>
>> +#include <errno.h>
>> +
>> +/* Private/internal data */
>> +struct fuse_daemonize {
>> + unsigned int flags;
>> + int signal_pipe_wr; /* write end for signaling parent */
>
> Hrm. Ok, so signal_pipe[2] is the pipe through which the child writes
> an int back to the parent, and then the parent can convey that outcome
> to whatever started the parent.
>
>> + int death_pipe_rd; /* read end, POLLHUP when parent dies */
>
> death_pipe[2] is a different pipe. The parent closes its end of the
> pipe and the child treats POLLHUP as a message that the parent died.
Yeah, let's say network initialization takes a long time or never
succeeds, the user wants to abort the mount and kills the parent. In
that case client should not continue to run in the background/
>
>> + int stop_pipe_rd; /* read end for stop signal */
>> + int stop_pipe_wr; /* write end for stop signal */
>
> and this third pipe exist so that the child can wake its own "parent
> watcher" thread and have it abort.
>
>> + pthread_t watcher;
>> + int watcher_started;
>
> Hm. So watcher_started is initialized to 0 in the parent, gets set to 1
> in the child's copy of memory when it starts the parent-watcher thread,
> and later becomes zero when the child shuts down the parent-watcher
> thread.
>
>> + _Atomic bool active;
>
> active is set in the parent process, copied to the child process, and
> cleared in fuse_daemonize_signal in the child.
>
>> + _Atomic bool daemonized;
>
> and daemonized is set in the child upon creation of the child; and
> read by fuse_daemonize_signal. The parent never accesses its copy of
> the variable.
>
>> +};
>> +
>> +/* Global daemonization object pointer */
>> +static _Atomic(struct fuse_daemonize *) daemonize;
>> +
>> +/* Watcher thread: polls for parent death or stop signal */
>> +static void *parent_watcher_thread(void *arg)
>> +{
>> + struct fuse_daemonize *di = arg;
>> + struct pollfd pfd[2];
>> +
>> + pfd[0].fd = di->death_pipe_rd;
>> + pfd[0].events = POLLIN;
>> + pfd[1].fd = di->stop_pipe_rd;
>> + pfd[1].events = POLLIN;
>> +
>> + while (1) {
>> + int rc = poll(pfd, 2, -1);
>> +
>> + if (rc < 0)
>> + continue;
>> +
>> + /* Parent died - death pipe write end closed */
>> + if (pfd[0].revents & (POLLHUP | POLLERR))
>> + _exit(EXIT_FAILURE);
>> +
>> + /* Stop signal received */
>> + if (pfd[1].revents & POLLIN)
>> + break;
>> + }
>> + return NULL;
>> +}
>> +
>> +static int start_parent_watcher(struct fuse_daemonize *daemonize)
>> +{
>> + int rc;
>> +
>> + rc = pthread_create(&daemonize->watcher, NULL, parent_watcher_thread,
>> + daemonize);
>> + if (rc != 0) {
>> + perror("fuse_daemonize: pthread_create");
>
> pthread functions return positive error numbers and do not set errno, so
> you can't use perror().
Thanks, updated.
>
>> + return -1;
>> + }
>> + daemonize->watcher_started = 1;
>
> Isn't this a bool value?
Already just updated it before even reading this :)
>
> FWIW the rest of the logic below looks correct, though I think the
> daemonize object itself would need a pthread_mutex_t to coordinate
> access if it's possible or desirable for multiple threads to access it.
>
> I think that's not the case, and any fuse server that did need that
> could implement the locking on its own.
Yeah, I had thought about it, but I can't find a real use case yet.
Maybe with sync-init fuse_daemonize_success() needs to be called twice,
although I still assume that calling it in the FUSE_INIT handler should
be ok, because the mount then succeeded. I need to think about it
another night.
Hmm, actually ->init() needs to know if sync or async init used, i.e.
the ->init() function should only call fuse_daemonize_success() with
async-init. With sync-init that should be done after
fuse_session_mount(). I'm going to add another new patch at the end of
the series in the next patch version.
Thanks,
Bernd
^ permalink raw reply [flat|nested] 59+ messages in thread
* Re: [PATCH 07/19] Add FUSE_MOUNT_FALLBACK_NEEDED define for -2 mount errors
2026-03-23 22:36 ` Darrick J. Wong
@ 2026-03-24 18:03 ` Bernd Schubert
0 siblings, 0 replies; 59+ messages in thread
From: Bernd Schubert @ 2026-03-24 18:03 UTC (permalink / raw)
To: Darrick J. Wong
Cc: linux-fsdevel, Miklos Szeredi, Joanne Koong, Bernd Schubert
On 3/23/26 23:36, Darrick J. Wong wrote:
> On Mon, Mar 23, 2026 at 06:45:02PM +0100, Bernd Schubert wrote:
>> From: Bernd Schubert <bschubert@ddn.com>
>>
>> Magic numbers in the code are not good - we better use a define.
>>
>> Signed-off-by: Bernd Schubert <bschubert@ddn.com>
>> ---
>> lib/fuse_i.h | 3 +++
>> lib/mount.c | 6 +++---
>> 2 files changed, 6 insertions(+), 3 deletions(-)
>>
>> diff --git a/lib/fuse_i.h b/lib/fuse_i.h
>> index 9e3c5dc5021e210a2778e975a37ab609af324010..b4c1d3eef41010287f6c9555ec0b2442d904d192 100644
>> --- a/lib/fuse_i.h
>> +++ b/lib/fuse_i.h
>> @@ -217,6 +217,9 @@ struct fuse_chan *fuse_chan_get(struct fuse_chan *ch);
>> */
>> void fuse_chan_put(struct fuse_chan *ch);
>>
>> +/* Special return value for mount functions to indicate fallback to fusermount3 is needed */
>> +#define FUSE_MOUNT_FALLBACK_NEEDED -2
>
> ...and you might want to put that in parentheses to avoid weird
> interactions elsewhere. Or maybe just make fuse_kern_do_mount return an
> enum so that callers have to handle FUSE_MOUNT_{OK,FAIL,FALLBACK} and
> can do so via switch statements?
I'm going the simple way for now and put it in parentheses. I think the
old API needs further clean up later on.
Thanks,
Bernd
^ permalink raw reply [flat|nested] 59+ messages in thread
* Re: [PATCH 09/19] Move mount flags to mount_i.h
2026-03-23 22:45 ` Darrick J. Wong
@ 2026-03-24 18:40 ` Bernd Schubert
0 siblings, 0 replies; 59+ messages in thread
From: Bernd Schubert @ 2026-03-24 18:40 UTC (permalink / raw)
To: Darrick J. Wong
Cc: linux-fsdevel, Miklos Szeredi, Joanne Koong, Bernd Schubert
On 3/23/26 23:45, Darrick J. Wong wrote:
> On Mon, Mar 23, 2026 at 06:45:04PM +0100, Bernd Schubert wrote:
>> From: Bernd Schubert <bschubert@ddn.com>
>>
>> This is preparation work for the new mount API, which goes into
>> its own file.
>>
>> Signed-off-by: Bernd Schubert <bschubert@ddn.com>
>> ---
>> lib/mount.c | 59 ++++++++---------------------------------------------
>> lib/mount_i_linux.h | 35 ++++++++++++++++++++++++++++++-
>> 2 files changed, 42 insertions(+), 52 deletions(-)
>>
>> diff --git a/lib/mount.c b/lib/mount.c
>> index fe353e2cc4579adb47473cac5db7d1bae2defb2c..b3ee9ba4c0a74c1d0d55f916e7046e064f8468b0 100644
>> --- a/lib/mount.c
>> +++ b/lib/mount.c
>> @@ -34,16 +34,6 @@
>> #include "fuse_mount_compat.h"
>>
>> #ifdef __NetBSD__
>
> Hopefully the rest of the bsd stuff can go in its own files too?
Just created another patch for that.
>
>> -#include <perfuse.h>
>> -
>> -#define MS_RDONLY MNT_RDONLY
>> -#define MS_NOSUID MNT_NOSUID
>> -#define MS_NODEV MNT_NODEV
>> -#define MS_NOEXEC MNT_NOEXEC
>> -#define MS_SYNCHRONOUS MNT_SYNCHRONOUS
>> -#define MS_NOATIME MNT_NOATIME
>> -#define MS_NOSYMFOLLOW MNT_NOSYMFOLLOW
>> -
>> #define umount2(mnt, flags) unmount(mnt, (flags == 2) ? MNT_FORCE : 0)
>> #endif
>>
>> @@ -51,10 +41,6 @@
>> #define FUSE_COMMFD_ENV "_FUSE_COMMFD"
>> #define FUSE_COMMFD2_ENV "_FUSE_COMMFD2"
>>
>> -#ifndef MS_DIRSYNC
>> -#define MS_DIRSYNC 128
>> -#endif
>> -
>> enum {
>> KEY_KERN_FLAG,
>> KEY_KERN_OPT,
>> @@ -154,35 +140,6 @@ void fuse_mount_version(void)
>> FUSERMOUNT_PROG);
>> }
>>
>> -struct mount_flags {
>> - const char *opt;
>> - unsigned long flag;
>> - int on;
>> -};
>> -
>> -static const struct mount_flags mount_flags[] = {
>> - {"rw", MS_RDONLY, 0},
>> - {"ro", MS_RDONLY, 1},
>> - {"suid", MS_NOSUID, 0},
>> - {"nosuid", MS_NOSUID, 1},
>> - {"dev", MS_NODEV, 0},
>> - {"nodev", MS_NODEV, 1},
>> - {"exec", MS_NOEXEC, 0},
>> - {"noexec", MS_NOEXEC, 1},
>> - {"async", MS_SYNCHRONOUS, 0},
>> - {"sync", MS_SYNCHRONOUS, 1},
>> - {"noatime", MS_NOATIME, 1},
>> - {"nodiratime", MS_NODIRATIME, 1},
>> - {"norelatime", MS_RELATIME, 0},
>> - {"nostrictatime", MS_STRICTATIME, 0},
>> - {"symfollow", MS_NOSYMFOLLOW, 0},
>> - {"nosymfollow", MS_NOSYMFOLLOW, 1},
>> -#ifndef __NetBSD__
>> - {"dirsync", MS_DIRSYNC, 1},
>> -#endif
>> - {NULL, 0, 0}
>> -};
>> -
>> unsigned int get_max_read(struct mount_opts *o)
>> {
>> return o->max_read;
>> @@ -192,13 +149,13 @@ static void set_mount_flag(const char *s, int *flags)
>> {
>> int i;
>>
>> - for (i = 0; mount_flags[i].opt != NULL; i++) {
>> - const char *opt = mount_flags[i].opt;
>> + for (i = 0; fuse_mount_flags[i].opt != NULL; i++) {
>> + const char *opt = fuse_mount_flags[i].opt;
>> if (strcmp(opt, s) == 0) {
>> - if (mount_flags[i].on)
>> - *flags |= mount_flags[i].flag;
>> + if (fuse_mount_flags[i].on)
>> + *flags |= fuse_mount_flags[i].flag;
>> else
>> - *flags &= ~mount_flags[i].flag;
>> + *flags &= ~fuse_mount_flags[i].flag;
>> return;
>> }
>> }
>> @@ -645,9 +602,9 @@ static int get_mnt_flag_opts(char **mnt_optsp, int flags)
>> if (!(flags & MS_RDONLY) && fuse_opt_add_opt(mnt_optsp, "rw") == -1)
>> return -1;
>>
>> - for (i = 0; mount_flags[i].opt != NULL; i++) {
>> - if (mount_flags[i].on && (flags & mount_flags[i].flag) &&
>> - fuse_opt_add_opt(mnt_optsp, mount_flags[i].opt) == -1)
>> + for (i = 0; fuse_mount_flags[i].opt != NULL; i++) {
>> + if (fuse_mount_flags[i].on && (flags & fuse_mount_flags[i].flag) &&
>> + fuse_opt_add_opt(mnt_optsp, fuse_mount_flags[i].opt) == -1)
>> return -1;
>> }
>> return 0;
>> diff --git a/lib/mount_i_linux.h b/lib/mount_i_linux.h
>> index abcd1b08012feedef6b4c8961b55ac847a27496a..c0de6228fce5a4d9070cc246ec76222b66de56fb 100644
>> --- a/lib/mount_i_linux.h
>> +++ b/lib/mount_i_linux.h
>> @@ -10,7 +10,8 @@
>> #ifndef FUSE_MOUNT_I_H_
>> #define FUSE_MOUNT_I_H_
>>
>> -/* Forward declaration for fuse_args */
>> +#include <sys/mount.h>
>> +
>> struct fuse_args;
>>
>> /* Mount options structure */
>> @@ -28,5 +29,37 @@ struct mount_opts {
>> unsigned int max_read;
>> };
>>
>> +/* Mount flags mapping structure */
>> +struct mount_flags {
>> + const char *opt;
>> + unsigned long flag;
>> + int on;
>> +};
>> +
>> +/* Mount flags table */
>> +static const struct mount_flags fuse_mount_flags[] = {
>
> Hmmm, doesn't this create a fuse_mount_flags[] in every single .o file
> that #includes this header? I'd have thought you'd want an extern
> declaration here and a definition elsewhere?
Uh oh, thanks for spotting that. Fixed.
>
>> + {"rw", MS_RDONLY, 0},
>> + {"ro", MS_RDONLY, 1},
>> + {"suid", MS_NOSUID, 0},
>> + {"nosuid", MS_NOSUID, 1},
>> + {"dev", MS_NODEV, 0},
>> + {"nodev", MS_NODEV, 1},
>> + {"exec", MS_NOEXEC, 0},
>> + {"noexec", MS_NOEXEC, 1},
>> + {"async", MS_SYNCHRONOUS, 0},
>> + {"sync", MS_SYNCHRONOUS, 1},
>> + {"noatime", MS_NOATIME, 1},
>> + {"nodiratime", MS_NODIRATIME, 1},
>> + {"norelatime", MS_RELATIME, 0},
>> + {"nostrictatime", MS_STRICTATIME, 0},
>> + {"symfollow", MS_NOSYMFOLLOW, 0},
>> + {"nosymfollow", MS_NOSYMFOLLOW, 1},
>> +#ifndef __NetBSD__
>
> When would we have __NetBSD__ defined on a Linux build?
>
> Though I see no meson.build changes here, so I can't tell if
> mount_i_linux.c only gets built on Linux or everywhere or ...?
mount_i_linux.h should be included in files used by linux only. Same
as mount.c. Next patch removes this BSD condition.
Thanks,
Bernd
>
> --D
>
>> + {"dirsync", MS_DIRSYNC, 1},
>> +#endif
>> + {NULL, 0, 0}
>> +};
>> +
>> +
>>
>> #endif /* FUSE_MOUNT_I_H_ */
>>
>> --
>> 2.43.0
>>
>>
^ permalink raw reply [flat|nested] 59+ messages in thread
* Re: [PATCH 17/19] Make fusermount work bidirectional for sync init
2026-03-23 17:45 ` [PATCH 17/19] Make fusermount work bidirectional for sync init Bernd Schubert
@ 2026-03-24 19:35 ` Darrick J. Wong
2026-03-24 21:24 ` Bernd Schubert
0 siblings, 1 reply; 59+ messages in thread
From: Darrick J. Wong @ 2026-03-24 19:35 UTC (permalink / raw)
To: Bernd Schubert
Cc: linux-fsdevel, Miklos Szeredi, Joanne Koong, Bernd Schubert
On Mon, Mar 23, 2026 at 06:45:12PM +0100, Bernd Schubert wrote:
> From: Bernd Schubert <bschubert@ddn.com>
>
> Signed-off-by: Bernd Schubert <bschubert@ddn.com>
> ---
> doc/README.fusermount | 359 ++++++++++++++++++++++++++++++++++++++++++++++++++
> util/fusermount.c | 317 ++++++++++++++++++++++++++++++++++++++++++--
> util/meson.build | 2 +-
> 3 files changed, 665 insertions(+), 13 deletions(-)
>
> diff --git a/doc/README.fusermount b/doc/README.fusermount
> new file mode 100644
> index 0000000000000000000000000000000000000000..54a3bac4f58964a4ed312d6f6bc15606fed1e647
> --- /dev/null
> +++ b/doc/README.fusermount
> @@ -0,0 +1,359 @@
> +Synchronous FUSE_INIT Protocol
> +================================
> +
> +Overview
> +--------
> +
> +The sync-init feature enables the FUSE library to start worker threads and
> +perform initialization ioctl calls BEFORE the actual mount() syscall happens.
> +This is required for the kernel's synchronous FUSE_INIT feature, where the
> +mount() syscall blocks until the FUSE daemon processes the INIT request.
> +
> +Without this feature, there would be a deadlock:
> +- mount() blocks waiting for INIT response
> +- Worker threads can't start because mount() hasn't returned
> +- INIT request can't be processed because worker threads aren't running
> +
> +
> +Protocol Flow
> +-------------
> +
> +Traditional mount flow:
> + 1. Library calls fusermount3
Heh. I haven't looked much at fusermount until recently. I gather that
fuservicemount has somewhat similar goals to fusermount3? fusermount3
seems to be a helper subprocess that libfuse can invoke on behalf of an
unprivileged fuse server. The helper is responsible for:
1) opening /dev/fuse
2) sending it to the fuse server via the FUSE_COMMFD_ENV fd which is
supposed to be an AF_UNIX socket
3) calling mount()
4) waiting for the parent to die
5) maybe calling unmount()
and it's really 1, 3, and 5 that need to be privileged, so that's why
it's a setuid program.
> + 2. fusermount3 opens /dev/fuse
> + 3. fusermount3 performs mount() syscall
> + 4. fusermount3 sends fd to library
> + 5. Library starts worker threads
> + 6. Worker threads process FUSE requests
Ah, yes. Thanks for adding this description! fuservicemount is I think
an upside-down version of fusermount -- fuservicemount runs in the
user's mount namespace, so it
1) connects to a named AF_UNIX socket to start an instance of the fuse
server
2) opens /dev/fuse and a memfd to pass cli arguments
3) passes those to the fuse server
4) the fuse server asks fuservicemount to open resources and pass them
over the socket
5) the fuse server passes source/type/mount options to fuservicemount
6) fuservicemount mounts the fs and exits
7) at some point the user unmounts, so the fuse server exits
> +Sync-init mount flow:
> + 1. Library calls fusermount3 with --sync-init flag
> + 2. fusermount3 opens /dev/fuse
> + 3. fusermount3 sends fd to library
> + 4. Library receives fd
> + 5. Library performs FUSE_DEV_IOC_SYNC_INIT ioctl
> + 6. Library starts worker threads
> + 7. Library sends "proceed" signal to fusermount3
> + 8. fusermount3 performs mount() syscall (blocks until INIT completes)
> + 9. Worker threads process INIT request
> + 10. mount() syscall completes
> + 11. fusermount3 exits
> +
> +
> +Implementation Details
> +----------------------
> +
> +Bidirectional Communication:
> + - Uses the existing unix socket (_FUSE_COMMFD environment variable)
> + - Simple 1-byte protocol for signaling
> + - Library signals fusermount3 when ready to proceed with mount
> +
> +fusermount3 Changes:
> + - New --sync-init command-line option
> + - Split mount operation into two phases:
> + * mount_fuse_prepare(): Opens device, prepares parameters
> + * mount_fuse_finish_fsmount(): Performs actual mount() syscall
> + - wait_for_signal(): Waits for library to signal readiness
> + - struct mount_context: Preserves state between phases
> +
> +Library Changes:
> + - fuse_session_mount_new_api(): Uses new protocol when available
> + - Sends "proceed" signal after worker thread is ready
> + - Handles both old and new mount protocols for compatibility
> +
> +
> +Backward Compatibility
> +----------------------
> +
> +The implementation maintains full backward compatibility:
> + - Old library + new fusermount3: Works (uses traditional flow)
> + - New library + old fusermount3: Falls back to traditional flow
> + - New library + new fusermount3: Uses sync-init flow when appropriate
> +
> +
> +Error Handling
> +--------------
> +
> +If any step fails during the sync-init flow:
> + - fusermount3 closes the fd and exits with error
> + - Library detects failure and cleans up
> + - No mount is left in inconsistent state
> +
> +Connection closure:
> + - If library closes socket before signaling, fusermount3 detects and exits
> + - If fusermount3 crashes, library detects closed socket
> +
> +
> +Security Considerations
> +-----------------------
> +
> +The sync-init protocol does not introduce new security concerns:
> + - Uses the same privilege separation as traditional mount
> + - Socket communication is already established and trusted
> + - No new privileged operations are added
> + - File descriptor passing uses existing SCM_RIGHTS mechanism
> +
> +
> +Performance Impact
> +------------------
> +
> +Minimal performance impact:
> + - One additional recv() call in fusermount3
> + - One additional send() call in library
> + - Total overhead: ~2 context switches
> + - Only affects mount time, not runtime performance
> +
> +
> +Future Enhancements
> +-------------------
> +
> +Potential improvements:
> + - Extended protocol for more complex initialization sequences
> + - Support for multiple worker threads coordination
> + - Enhanced error reporting through the socket
> + - Timeout mechanisms for detecting hung initialization
> +
> +
> +ASCII Workflow Diagrams
> +========================
> +
> +1. Traditional Mount Flow (without --sync-init, async INIT)
> +------------------------------------------------------------
> +
> +Library fusermount3 Kernel
> + | | |
> + |--- spawn fusermount3 ---->| |
> + | | |
> + | [open /dev/fuse] |
> + | |------- open -------->|
> + | |<------ fd ---------- |
> + | | |
> + | [mount() syscall] |
> + | |------ mount -------->|
> + | |<----- success ------ | [mount returns immediately]
> + | | | [INIT queued in kernel]
> + | [send_fd(fd)] |
> + |<------- fd --------------| |
> + | | |
> + | [fusermount3 exits] |
> + | |
> + | [start worker thread] |
> + | [worker reads /dev/fuse] |
> + |---------------------------------------- read -->|
> + |<--------------------------------------- INIT ---| [dequeued from kernel]
> + | |
> + | OK: INIT was queued, worker reads it later |
> + | Works fine for async INIT |
Hmm, looking at this, perhaps it /is/ possible for fuservicemount to
employ synchronous init. The fuse server would start that background
init-only request handler thread before telling fuservicemount to call
mount(). That blocks while the kernel sends FUSE_INIT to the fuse
server, it processes everything up to the init request, and returns.
> +
> +
> +1b. Problem: Synchronous INIT without --sync-init
> +--------------------------------------------------
> +
> +Library fusermount3 Kernel
> + | | |
> + |--- spawn fusermount3 ---->| |
> + | | |
> + | [open /dev/fuse] |
> + | |------- open -------->|
> + | |<------ fd ---------- |
> + | | |
> + | [mount() syscall] |
> + | |------ mount -------->|
> + | | | [mount BLOCKS waiting for INIT]
> + | | (BLOCKED) | [needs worker to process INIT]
> + | | |
> + | [waiting for fd...] | |
> + | | |
> + | | |
> + | DEADLOCK: mount() waits for INIT response |
> + | but worker thread not started yet |
> + | because we're waiting for fd |
> +
> +
> +2. Sync-Init Mount Flow (with --sync-init)
> +-------------------------------------------
> +
> +Library fusermount3 Kernel
> + | | |
> + |--- spawn fusermount3 ---->| |
> + | with --sync-init | |
> + | | |
> + | [open /dev/fuse] |
> + | |------- open -------->|
> + | |<------ fd ---------- |
> + | | |
> + | [send_fd(fd)] |
> + |<------- fd --------------| |
> + | | |
> + | [wait_for_signal()] |
> + | | (BLOCKED) |
> + | | |
> + | [ioctl SYNC_INIT] | |
> + |---------------------------------------- ioctl -->|
> + | |
> + | [start worker thread] |
> + | [worker ready] |
> + | | |
> + |--- "proceed" signal ----->| |
> + | [signal received] |
> + | | |
> + | [mount() syscall] |
> + | |------ mount -------->|
> + | | | [mount blocks]
> + | | | [sends INIT]
> + |<------------------------------------------------ |
> + | | |
> + | [worker processes INIT] | |
> + |------------------------------------------------->|
> + | | | [mount unblocks]
> + | |<----- success ------ |
> + | | |
> + | [fusermount3 exits] |
> + | |
> + | SUCCESS: Worker ready before mount() |
> + | INIT processed synchronously |
> +
> +
> +3. Error Scenario: Library Crashes Before Signaling
> +----------------------------------------------------
> +
> +Library fusermount3 Kernel
> + | | |
> + |--- spawn fusermount3 ---->| |
> + | with --sync-init | |
> + | | |
> + | [open /dev/fuse] |
> + | |------- open -------->|
> + | |<------ fd ---------- |
> + | | |
> + | [send_fd(fd)] |
> + |<------- fd --------------| |
> + | | |
> + | [wait_for_signal()] |
> + | | (BLOCKED) |
> + | | |
> + X [library crashes] | |
> + | | |
> + | [recv() returns 0] |
> + | [socket closed] |
> + | | |
> + | [cleanup and exit] |
> + | X |
> + | |
> + | RESULT: Clean failure, no mount performed |
> +
> +
> +4. Detailed Function Call Flow
> +-------------------------------
> +
> +Library (lib/fuse_lowlevel.c):
> +fuse_session_mount_new_api()
> + |
> + +-- fuse_kern_mount_prepare() [lib/mount.c]
> + | |
> + | +-- fuse_mount_fusermount() [lib/mount_util.c]
> + | |
> + | +-- socketpair() [create comm socket]
> + | |
> + | +-- fork()
> + | |
> + | +-- [child] execl("fusermount3", "--sync-init", ...)
> + | |
> + | +-- [parent] receive_fd() <--- BLOCKS until fd arrives
> + | |
> + | +-- recvmsg(SCM_RIGHTS)
> + | |
> + | +-- return fd
> + |
> + +-- session_start_sync_init() [lib/fuse_lowlevel.c]
> + | |
> + | +-- ioctl(fd, FUSE_DEV_IOC_SYNC_INIT)
> + | |
> + | +-- pthread_create(worker_thread)
> + | |
> + | +-- return
> + |
> + +-- fuse_fusermount_proceed_mnt(socket) [lib/mount.c] <--- NEW: Bidirectional handshake
> + |
> + +-- send(socket, "proceed", 1) <--- Signal fusermount3 to proceed
> + |
> + +-- recv(socket, &status, 1) <--- BLOCKS until mount result arrives
> + | |
> + | +-- [fusermount3 performs mount and sends status byte]
> + |
> + +-- if (status != 0) return -1 <--- Mount failed
> + |
> + +-- return 0 <--- Mount succeeded
> +
> +
> +Utility (util/fusermount.c):
> +fusermount3 main() with --sync-init
> + |
> + +-- mount_fuse_sync_init() [util/fusermount.c]
> + |
> + +-- mount_fuse_prepare() [util/fusermount.c]
> + | |
> + | +-- open("/dev/fuse")
> + | |
> + | +-- check_perm() [util/fusermount.c]
> + | |
> + | +-- return fd
> + |
> + +-- send_fd(socket, fd) [util/fusermount.c]
> + | |
> + | +-- sendmsg(SCM_RIGHTS)
> + |
> + +-- wait_for_signal(socket) [util/fusermount.c] <--- BLOCKS until library signals
> + | |
> + | +-- recv(socket, buf, 1)
> + | |
> + | +-- return 0
> + |
> + +-- mount_fuse_finish_fsmount() [util/fusermount.c]
> + | |
> + | +-- fuse_kern_fsmount() [lib/mount_fsmount.c]
> + | | |
> + | | +-- fsopen("fuse", FSOPEN_CLOEXEC)
> + | | | |
> + | | | +-- [kernel creates filesystem context]
> + | | |
> + | | +-- fsconfig(fsfd, SET_STRING, "source", ...)
> + | | +-- fsconfig(fsfd, SET_STRING, "fd", fd_value, ...)
> + | | +-- fsconfig(fsfd, ...) [apply mount options]
> + | | +-- fsconfig(fsfd, CMD_CREATE, ...)
> + | | |
> + | | +-- fsmount(fsfd, FSMOUNT_CLOEXEC, mount_attrs)
> + | | | |
> + | | | +-- [kernel sends FUSE_INIT here]
> + | | | |
> + | | | +-- [worker thread processes INIT]
> + | | | |
> + | | | +-- [fsmount returns mntfd]
> + | | |
> + | | +-- move_mount(mntfd, "", AT_FDCWD, target, ...)
> + | | | |
> + | | | +-- [attach mount to target directory]
> + | | | |
> + | | | +-- [no blocking - INIT already processed]
> + | | |
> + | | +-- add_mount() [lib/mount_fsmount.c - update /etc/mtab]
> + | | |
> + | | +-- return 0 on success, -1 on failure
> + | |
> + | +-- if mount failed: return -1
> + | +-- if mount succeeded: continue
> + |
> + +-- send_status_byte(socket) [util/fusermount.c] <--- NEW: Send result to library
> + | |
> + | +-- status = (mount_result == 0) ? 0 : 1
> + | +-- send(socket, &status, 1)
> + | |
> + | +-- return
> + |
> + +-- return 0
> +
> +
> +Note: The new mount API (fsopen/fsconfig/fsmount/move_mount) is REQUIRED
> + for sync-init because fsmount() triggers FUSE_INIT before the mount
> + is attached. This allows the worker thread to process INIT before
> + move_mount() completes, preventing deadlock.
...and so we don't expose the directory tree to the mountns until we
know that FUSE_INIT didn't crash the server.
> diff --git a/util/fusermount.c b/util/fusermount.c
> index 80b42a594e89cdc2f43824f5e274892522fd8cce..808b4afd89ceb49273c944d43bffe5033e27549b 100644
> --- a/util/fusermount.c
> +++ b/util/fusermount.c
> @@ -957,6 +957,7 @@ static void free_mount_params(struct mount_params *mp)
> free(mp->source);
> free(mp->type);
> free(mp->mnt_opts);
> + memset(mp, 0, sizeof(*mp));
> }
>
> /*
> @@ -1378,6 +1379,179 @@ static int open_fuse_device(const char *dev)
> return fd;
> }
>
> +#ifdef HAVE_NEW_MOUNT_API
> +/* Forward declaration from lib/mount_fsmount.c */
> +int fuse_kern_fsmount(const char *mnt, unsigned long flags, int blkdev,
> + const char *fsname, const char *subtype,
> + const char *source_dev, const char *kernel_opts,
> + const char *mnt_opts);
> +#endif
Shouldn't this be included from a header file somewhere?
> +
> +/*
> + * Context for split mount operation (sync-init mode)
> + */
> +struct mount_context {
> + int fd;
> + const char *dev;
> + struct stat stbuf;
> + char *source;
> + char *mnt_opts;
> + char *x_opts;
> + const char *type;
> +};
> +
> +/*
> + * Phase 1: Open device and prepare for mount (sync-init mode)
> + * Returns fd on success, -1 on failure
> + */
> +static int mount_fuse_prepare(const char *mnt, const char *opts,
> + struct mount_context *ctx)
> +{
> + int res;
> + int mountpoint_fd = -1;
> + char *do_mount_opts = NULL;
> + const char *real_mnt = mnt;
> +
> + memset(ctx, 0, sizeof(*ctx));
> + ctx->dev = getenv(FUSE_KERN_DEVICE_ENV) ?: FUSE_DEV;
> +
> + ctx->fd = open_fuse_device(ctx->dev);
> + if (ctx->fd == -1)
> + return -1;
> +
> + drop_privs();
> + read_conf();
> +
> + if (getuid() != 0 && mount_max != -1) {
> + int mount_count = count_fuse_fs();
> +
> + if (mount_count >= mount_max) {
> + fprintf(stderr,
> + "%s: too many FUSE filesystems mounted; mount_max=N can be set in %s\n",
> + progname, FUSE_CONF);
> + goto fail_close_fd;
> + }
> + }
/me notes that he's refactored this configuration file related function
into fuser_conf.c though that's in the fuse-services v4 that I'll send
you soon.
> +
> + res = extract_x_options(opts, &do_mount_opts, &ctx->x_opts);
> + if (res)
> + goto fail_close_fd;
> +
> + res = check_perm(&real_mnt, &ctx->stbuf, &mountpoint_fd);
> + restore_privs();
> +
> + if (mountpoint_fd != -1)
> + close(mountpoint_fd);
> +
> + if (res == -1)
> + goto fail_close_fd;
> +
> + free(do_mount_opts);
> + return ctx->fd;
> +
> +fail_close_fd:
> + close(ctx->fd);
> + free(do_mount_opts);
> + free(ctx->x_opts);
> + ctx->fd = -1;
> + return -1;
> +}
> +
> +#ifdef HAVE_NEW_MOUNT_API
> +/*
> + * Phase 2: Perform the actual mount using new mount API (sync-init mode)
> + * Returns 0 on success, -1 on failure
> + */
> +static int mount_fuse_finish_fsmount(const char *mnt, const char *opts,
> + struct mount_context *ctx,
> + const char **type)
> +{
> + int res;
> + char *do_mount_opts = NULL;
> + char *x_prefixed_opts = NULL;
> + struct mount_params mp = { .fd = ctx->fd };
> + char *final_mnt_opts = NULL;
> +
> + /* Extract x-options */
> + res = extract_x_options(opts, &do_mount_opts, &x_prefixed_opts);
> + if (res)
> + goto fail;
> +
> + /* Prepare mount parameters */
> + mp.rootmode = ctx->stbuf.st_mode & S_IFMT;
> + mp.dev = ctx->dev;
I think those could be set in the mp variable definition?
> +
> + res = prepare_mount(do_mount_opts, &mp);
> + if (res == -1)
> + goto fail;
> +
> + /* Merge x-options if running as root */
> + final_mnt_opts = mp.mnt_opts;
> + if (geteuid() == 0 && ctx->x_opts && strlen(ctx->x_opts) > 0) {
> + size_t mnt_opts_len = strlen(mp.mnt_opts);
> + size_t x_mnt_opts_len = mnt_opts_len + strlen(ctx->x_opts) + 2;
> + char *x_mnt_opts = calloc(1, x_mnt_opts_len);
> +
> + if (!x_mnt_opts)
> + goto fail_free_params;
> +
> + if (mnt_opts_len) {
> + strcpy(x_mnt_opts, mp.mnt_opts);
> + strncat(x_mnt_opts, ",", 2);
> + }
> + strncat(x_mnt_opts, ctx->x_opts,
> + x_mnt_opts_len - mnt_opts_len - 2);
> +
> + final_mnt_opts = x_mnt_opts;
> + }
Curious, I thought the x- options were edited out by /sbin/mount so fuse
would never see them? Does the x- option handling in fusermount.c exist
to handle the case where someone passes them directly to the fuse
server, aka
$ sshfs <whatever> /mnt -o x-systemd-hahaha=1
and now you need to ensure that x-systemd-hahaha doesn't get sent to the
kernel but does get seen by the fuse server?
> +
> + /* Use new mount API */
> + res = fuse_kern_fsmount(mnt, mp.flags, mp.blkdev,
> + mp.fsname, mp.subtype, ctx->dev,
> + mp.optbuf, final_mnt_opts);
> + if (res == -1)
> + goto fail_free_merged;
> +
> + /* Change to root directory */
> + res = chdir("/");
> + if (res == -1) {
> + fprintf(stderr, "%s: failed to chdir to '/'\n", progname);
> + goto fail_free_merged;
> + }
> +
> + /* Store results in context */
> + ctx->source = mp.source;
> + ctx->type = mp.type;
> + ctx->mnt_opts = final_mnt_opts;
> + *type = mp.type;
> +
> + res = 0;
> +
> + /* Only free what is not assigned to ctx */
> + free(mp.fsname);
> + free(mp.subtype);
> + free(mp.optbuf);
> + if (final_mnt_opts != mp.mnt_opts)
> + free(mp.mnt_opts);
> +
> +out:
> + free(do_mount_opts);
> + free(x_prefixed_opts);
> +
> + return res;
> +
> +fail_free_merged:
> + if (final_mnt_opts != mp.mnt_opts)
> + free(final_mnt_opts);
> +fail_free_params:
> + free_mount_params(&mp);
> +fail:
> + res = -1;
> + goto out;
> +}
> +#endif /* HAVE_NEW_MOUNT_API */
> +
> +
> static int mount_fuse(const char *mnt, const char *opts, const char **type)
> {
> int res;
> @@ -1473,6 +1647,75 @@ fail_close_fd:
> goto out_free;
> }
>
> +/* Forward declarations for helper functions */
> +static int send_fd(int sock_fd, int fd);
> +static int wait_for_signal(int sock_fd);
> +
> +#ifdef HAVE_NEW_MOUNT_API
> +/*
> + * Perform sync-init mount using new mount API
> + * Returns 0 on success, -1 on failure
> + */
> +static int mount_fuse_sync_init(const char *mnt, const char *opts,
> + int cfd, const char **type)
> +{
> + struct mount_context ctx;
> + int fd, res;
> + int32_t status, send_res;
> +
> + /* Phase 1: Open device and prepare */
> + fd = mount_fuse_prepare(mnt, opts, &ctx);
> + if (fd == -1)
> + return -1;
> +
> + /* Send fd to caller so it can start worker thread */
> + res = send_fd(cfd, fd);
> + if (res != 0) {
> + close(fd);
> + free(ctx.x_opts);
> + return -1;
> + }
> +
> + /* Wait for caller to signal that worker thread is ready */
> + res = wait_for_signal(cfd);
> + if (res != 0) {
> + close(fd);
> + free(ctx.x_opts);
> + return -1;
> + }
> +
> + /* Phase 2: Perform the actual mount using new API */
> + res = mount_fuse_finish_fsmount(mnt, opts, &ctx, type);
> +
> + /* Send mount result back to caller (4-byte error code) */
> + status = (res == 0) ? 0 : -(int32_t)errno;
> + do {
> + send_res = send(cfd, &status, sizeof(status), 0);
> + } while (send_res == -1 && errno == EINTR);
> + if (send_res != sizeof(status)) {
> + fprintf(stderr, "%s: failed to send mount status: %s\n",
> + progname, strerror(errno));
> + }
> +
> + if (res == -1) {
> + close(fd);
> + free(ctx.source);
> + free(ctx.mnt_opts);
> + free(ctx.x_opts);
> + return -1;
> + }
> +
> + close(fd);
> +
> + /* Cleanup */
> + free(ctx.source);
> + free(ctx.mnt_opts);
> + free(ctx.x_opts);
> +
> + return 0;
Err... these cleanups are identical, so I think you can get rid of the
"if (res == -1)" chunk and just return res at the end.
--D
> +}
> +#endif /* HAVE_NEW_MOUNT_API */
> +
> static int send_fd(int sock_fd, int fd)
> {
> int retval;
> @@ -1509,6 +1752,30 @@ static int send_fd(int sock_fd, int fd)
> return 0;
> }
>
> +/*
> + * Wait for a signal byte from the caller.
> + * Returns 0 on success, -1 on error.
> + */
> +static int wait_for_signal(int sock_fd)
> +{
> + char buf[1];
> + int res;
> +
> + do {
> + res = recv(sock_fd, buf, sizeof(buf), 0);
> + } while (res == -1 && errno == EINTR);
> + if (res != 1) {
> + if (res == 0)
> + fprintf(stderr, "%s: connection closed while waiting for signal\n",
> + progname);
> + else
> + fprintf(stderr, "%s: error receiving signal: %s\n",
> + progname, strerror(errno));
> + return -1;
> + }
> + return 0;
> +}
> +
> /* Helper for should_auto_unmount
> *
> * fusermount typically has the s-bit set - initial open of `mnt` was as root
> @@ -1700,6 +1967,7 @@ int main(int argc, char *argv[])
> const char *opts = "";
> const char *type = NULL;
> int setup_auto_unmount_only = 0;
> + int sync_init_mode = 0;
>
> static const struct option long_opts[] = {
> {"unmount", no_argument, NULL, 'u'},
> @@ -1712,6 +1980,7 @@ int main(int argc, char *argv[])
> // They'ne meant for internal use by mount.c
> {"auto-unmount", no_argument, NULL, 'U'},
> {"comm-fd", required_argument, NULL, 'c'},
> + {"sync-init", no_argument, NULL, 'S'},
> {0, 0, 0, 0}};
>
> progname = strdup(argc > 0 ? argv[0] : "fusermount");
> @@ -1746,6 +2015,9 @@ int main(int argc, char *argv[])
> case 'c':
> commfd = optarg;
> break;
> + case 'S':
> + sync_init_mode = 1;
> + break;
> case 'z':
> lazy = 1;
> break;
> @@ -1823,21 +2095,42 @@ int main(int argc, char *argv[])
> if (setup_auto_unmount_only)
> goto wait_for_auto_unmount;
>
> - fd = mount_fuse(mnt, opts, &type);
> - if (fd == -1)
> - goto err_out;
> + if (sync_init_mode) {
> +#ifdef HAVE_NEW_MOUNT_API
> + res = mount_fuse_sync_init(mnt, opts, cfd, &type);
> + if (res == -1)
> + goto err_out;
>
> - res = send_fd(cfd, fd);
> - if (res != 0) {
> - umount2(mnt, MNT_DETACH); /* lazy umount */
> + if (!auto_unmount) {
> + free(mnt);
> + free((void *) type);
> + return 0;
> + }
> + /* Continue to auto_unmount handling below */
> +#else
> + fprintf(stderr, "%s: sync-init mode requires new mount API support\n",
> + progname);
> + fprintf(stderr, "%s: kernel or headers too old (need fsopen/fsmount)\n",
> + progname);
> goto err_out;
> - }
> - close(fd);
> +#endif
> + } else {
> + fd = mount_fuse(mnt, opts, &type);
> + if (fd == -1)
> + goto err_out;
>
> - if (!auto_unmount) {
> - free(mnt);
> - free((void*) type);
> - return 0;
> + res = send_fd(cfd, fd);
> + if (res != 0) {
> + umount2(mnt, MNT_DETACH); /* lazy umount */
> + goto err_out;
> + }
> + close(fd);
> +
> + if (!auto_unmount) {
> + free(mnt);
> + free((void *) type);
> + return 0;
> + }
> }
>
> wait_for_auto_unmount:
> diff --git a/util/meson.build b/util/meson.build
> index 0e4b1cce95377e73af7dc45655a7088315497ddb..731ef95488461ac21c21b1972a96d58b1187dc5a 100644
> --- a/util/meson.build
> +++ b/util/meson.build
> @@ -1,6 +1,6 @@
> fuseconf_path = join_paths(get_option('prefix'), get_option('sysconfdir'), 'fuse.conf')
>
> -executable('fusermount3', ['fusermount.c', '../lib/mount_util.c', '../lib/util.c'],
> +executable('fusermount3', ['fusermount.c', '../lib/mount_util.c', '../lib/mount_fsmount.c', '../lib/util.c'],
> include_directories: include_dirs,
> install: true,
> install_dir: get_option('bindir'),
>
> --
> 2.43.0
>
>
^ permalink raw reply [flat|nested] 59+ messages in thread
* Re: [PATCH 18/19] New mount API: Filter out "user="
2026-03-23 17:45 ` [PATCH 18/19] New mount API: Filter out "user=" Bernd Schubert
@ 2026-03-24 19:51 ` Darrick J. Wong
2026-03-24 20:01 ` Bernd Schubert
0 siblings, 1 reply; 59+ messages in thread
From: Darrick J. Wong @ 2026-03-24 19:51 UTC (permalink / raw)
To: Bernd Schubert
Cc: linux-fsdevel, Miklos Szeredi, Joanne Koong, Bernd Schubert
On Mon, Mar 23, 2026 at 06:45:13PM +0100, Bernd Schubert wrote:
> From: Bernd Schubert <bschubert@ddn.com>
>
> This gets added in the fusermount process and kernel then fails
> the mount.
>
> Signed-off-by: Bernd Schubert <bschubert@ddn.com>
> ---
> lib/mount_fsmount.c | 13 ++++++++++++-
> 1 file changed, 12 insertions(+), 1 deletion(-)
>
> diff --git a/lib/mount_fsmount.c b/lib/mount_fsmount.c
> index f1fec790bb80f8815d485a068dc7efdff1746309..76c14cf9a22465160fc6b206ca9b6c9e7300adba 100644
> --- a/lib/mount_fsmount.c
> +++ b/lib/mount_fsmount.c
> @@ -218,6 +218,16 @@ static int is_mount_attr_opt(const char *opt)
> strcmp(opt, "symfollow") == 0;
> }
>
> +/**
> + * Check if an option is a mount table option (not passed to fsconfig)
> + */
> +static int is_mtab_only_opt(const char *opt)
> +{
> + /* These options are for /run/mount/utab only, not for the kernel */
> + return strncmp(opt, "user=", 5) == 0 ||
> + strcmp(opt, "rw") == 0;
Oh, so these are added to the mount options string by get_mnt_opts
and now they need to be pulled out so they aren't passed to fsconfig?
How'd they get into the argument list in the first place?
--D
> +}
> +
> /*
> * Parse kernel options string and apply via fsconfig
> * Options are comma-separated key=value pairs
> @@ -241,7 +251,8 @@ static int apply_mount_opts(int fsfd, const char *opts)
> opt = strtok_r(opts_copy, ",", &saveptr);
> while (opt) {
> /* Skip mount attributes - they're handled by fsmount(), not fsconfig() */
> - if (!is_mount_attr_opt(opt)) {
> + /* Skip mtab-only options - they're for /run/mount/utab, not kernel */
> + if (!is_mount_attr_opt(opt) && !is_mtab_only_opt(opt)) {
> res = apply_opt_key_value(fsfd, opt);
> if (res < 0) {
> free(opts_copy);
>
> --
> 2.43.0
>
>
^ permalink raw reply [flat|nested] 59+ messages in thread
* Re: [PATCH 18/19] New mount API: Filter out "user="
2026-03-24 19:51 ` Darrick J. Wong
@ 2026-03-24 20:01 ` Bernd Schubert
2026-03-24 23:02 ` Darrick J. Wong
0 siblings, 1 reply; 59+ messages in thread
From: Bernd Schubert @ 2026-03-24 20:01 UTC (permalink / raw)
To: Darrick J. Wong
Cc: linux-fsdevel, Miklos Szeredi, Joanne Koong, Bernd Schubert
On 3/24/26 20:51, Darrick J. Wong wrote:
> On Mon, Mar 23, 2026 at 06:45:13PM +0100, Bernd Schubert wrote:
>> From: Bernd Schubert <bschubert@ddn.com>
>>
>> This gets added in the fusermount process and kernel then fails
>> the mount.
>>
>> Signed-off-by: Bernd Schubert <bschubert@ddn.com>
>> ---
>> lib/mount_fsmount.c | 13 ++++++++++++-
>> 1 file changed, 12 insertions(+), 1 deletion(-)
>>
>> diff --git a/lib/mount_fsmount.c b/lib/mount_fsmount.c
>> index f1fec790bb80f8815d485a068dc7efdff1746309..76c14cf9a22465160fc6b206ca9b6c9e7300adba 100644
>> --- a/lib/mount_fsmount.c
>> +++ b/lib/mount_fsmount.c
>> @@ -218,6 +218,16 @@ static int is_mount_attr_opt(const char *opt)
>> strcmp(opt, "symfollow") == 0;
>> }
>>
>> +/**
>> + * Check if an option is a mount table option (not passed to fsconfig)
>> + */
>> +static int is_mtab_only_opt(const char *opt)
>> +{
>> + /* These options are for /run/mount/utab only, not for the kernel */
>> + return strncmp(opt, "user=", 5) == 0 ||
>> + strcmp(opt, "rw") == 0;
>
> Oh, so these are added to the mount options string by get_mnt_opts
> and now they need to be pulled out so they aren't passed to fsconfig?
> How'd they get into the argument list in the first place?
See the commit message ;) And then check fusermount.c in get_mnt_opts()
Cheers,
Bernd
^ permalink raw reply [flat|nested] 59+ messages in thread
* Re: [PATCH 11/19] Add support for the new linux mount API
2026-03-23 23:42 ` Darrick J. Wong
@ 2026-03-24 20:16 ` Bernd Schubert
2026-03-24 22:46 ` Darrick J. Wong
0 siblings, 1 reply; 59+ messages in thread
From: Bernd Schubert @ 2026-03-24 20:16 UTC (permalink / raw)
To: Darrick J. Wong
Cc: linux-fsdevel, Miklos Szeredi, Joanne Koong, Bernd Schubert
On 3/24/26 00:42, Darrick J. Wong wrote:
> On Mon, Mar 23, 2026 at 06:45:06PM +0100, Bernd Schubert wrote:
>> From: Bernd Schubert <bschubert@ddn.com>
>>
>> So far only supported for fuse_session_mount(), which is called
>> from high and low level API, but not yet supported for
>> fuse_open_channel(), which used for privilege drop through
>> mount.fuse. Main goal for the new API is support for synchronous
>> FUSE_INIT and I don't think that is going to work with
>> fuse_open_channel(). At least not with io-uring support as long
>> as it is started from FUSE_INIT.
>>
>> Signed-off-by: Bernd Schubert <bschubert@ddn.com>
>> ---
>> lib/fuse_lowlevel.c | 75 +++++++++-
>> lib/meson.build | 3 +
>> lib/mount.c | 27 +++-
>> lib/mount_fsmount.c | 405 ++++++++++++++++++++++++++++++++++++++++++++++++++++
>> lib/mount_i_linux.h | 14 ++
>> meson.build | 19 ++-
>> 6 files changed, 535 insertions(+), 8 deletions(-)
>>
>> diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c
>> index 4a1c7c4c02a05706e077a7ea89fab3c81bfe22cd..626233df20f49fa89cd9327f94340169d7061f75 100644
>> --- a/lib/fuse_lowlevel.c
>> +++ b/lib/fuse_lowlevel.c
>> @@ -20,6 +20,9 @@
>> #include "util.h"
>> #include "fuse_uring_i.h"
>> #include "fuse_daemonize.h"
>> +#if defined(__linux__)
>> +#include "mount_i_linux.h"
>> +#endif
>>
>> #include <pthread.h>
>> #include <stdatomic.h>
>> @@ -4398,6 +4401,64 @@ int fuse_session_custom_io_30(struct fuse_session *se,
>> offsetof(struct fuse_custom_io, clone_fd), fd);
>> }
>>
>> +#if defined(HAVE_NEW_MOUNT_API)
>> +/* Only linux supports sync FUSE_INIT so far */
>
> What does synchronous FUSE_INIT have to do with fsmount()?
> Does it not work with the old mount(2)?
So we had this discussion with Miklos in the earlier version of the
patches here https://github.com/libfuse/libfuse/pull/1367
where Miklos has noticed the possible deadlock and suggested the new
mount API. I think Miklos kernel patch solves that, without the new
mount API, but then I'm now hesitating to make the code even more
complex by supported old and new API. Until BSD gains that sync-fuse
init, this is a Linux only feature and was added after new the new mount
api.
The comment is not right for this commit, though.
>
>> +static int fuse_session_mount_new_api(struct fuse_session *se,
>> + const char *mountpoint)
>> +{
>> + int fd = -1;
>> + int res, err;
>> + char *mnt_opts = NULL;
>> + char *mnt_opts_with_fd = NULL;
>> + char fd_opt[32];
>> +
>> + res = fuse_kern_mount_get_base_mnt_opts(se->mo, &mnt_opts);
>> + if (res == -1) {
>> + fuse_log(FUSE_LOG_ERR,
>> + "fuse: failed to get base mount options\n");
>> + err = -EIO;
>> + goto err;
>> + }
>> +
>> + fd = fuse_kern_mount_prepare(mountpoint, se->mo);
>> + if (fd == -1) {
>> + fuse_log(FUSE_LOG_ERR, "Mount preparation failed.\n");
>> + err = -EIO;
>> + goto err;
>> + }
>> +
>> + snprintf(fd_opt, sizeof(fd_opt), "fd=%i", fd);
>> + if (fuse_opt_add_opt(&mnt_opts_with_fd, mnt_opts) == -1 ||
>> + fuse_opt_add_opt(&mnt_opts_with_fd, fd_opt) == -1) {
>> + err = -ENOMEM;
>> + goto err;
>> + }
>> +
>> + err = fuse_kern_fsmount_mo(mountpoint, se->mo, mnt_opts_with_fd);
>> +err:
>> + if (err) {
>> + if (fd >= 0)
>> + close(fd);
>> + fd = -1;
>> + se->fd = -1;
>> + se->error = -errno;
>> + }
>> +
>> + free(mnt_opts);
>> + free(mnt_opts_with_fd);
>> + return fd;
>> +}
>> +#else
>> +static int fuse_session_mount_new_api(struct fuse_session *se,
>> + const char *mountpoint)
>> +{
>> + (void)se;
>> + (void)mountpoint;
>> +
>> + return -1;
>> +}
>> +#endif
>> +
>> int fuse_session_mount(struct fuse_session *se, const char *_mountpoint)
>> {
>> int fd;
>> @@ -4425,6 +4486,8 @@ int fuse_session_mount(struct fuse_session *se, const char *_mountpoint)
>> close(fd);
>> } while (fd >= 0 && fd <= 2);
>>
>> + /* Open channel */
>> +
>> /*
>> * To allow FUSE daemons to run without privileges, the caller may open
>> * /dev/fuse before launching the file system and pass on the file
>> @@ -4443,10 +4506,18 @@ int fuse_session_mount(struct fuse_session *se, const char *_mountpoint)
>> return 0;
>> }
>>
>> - /* Open channel */
>> + /* new linux mount api */
>> + fd = fuse_session_mount_new_api(se, mountpoint);
>> + if (fd >= 0)
>> + goto out;
>> +
>> + /* fall back to old API */
>> + se->error = 0; /* reset error of new api */
>> fd = fuse_kern_mount(mountpoint, se->mo);
>> - if (fd == -1)
>> + if (fd < 0)
>> goto error_out;
>> +
>> +out:
>> se->fd = fd;
>>
>> /* Save mountpoint */
>> diff --git a/lib/meson.build b/lib/meson.build
>> index 5bd449ebffe7c9229df904d647d990c6c47f80b5..5fd738a589c5aba97a738d5eedbf0f9962e4adfc 100644
>> --- a/lib/meson.build
>> +++ b/lib/meson.build
>> @@ -7,6 +7,9 @@ libfuse_sources = ['fuse.c', 'fuse_i.h', 'fuse_loop.c', 'fuse_loop_mt.c',
>>
>> if host_machine.system().startswith('linux')
>> libfuse_sources += [ 'mount.c' ]
>> + if private_cfg.get('HAVE_NEW_MOUNT_API', false)
>> + libfuse_sources += [ 'mount_fsmount.c' ]
>> + endif
>> else
>> libfuse_sources += [ 'mount_bsd.c' ]
>> endif
>> diff --git a/lib/mount.c b/lib/mount.c
>> index b3ee9ba4c0a74c1d0d55f916e7046e064f8468b0..30fd4d2f9bbb84c817b2363b2075456acd1c1255 100644
>> --- a/lib/mount.c
>> +++ b/lib/mount.c
>> @@ -449,8 +449,8 @@ static int fuse_mount_fusermount(const char *mountpoint, struct mount_opts *mo,
>> #define O_CLOEXEC 0
>> #endif
>>
>> -static int fuse_kern_mount_prepare(const char *mnt,
>> - struct mount_opts *mo)
>> +int fuse_kern_mount_prepare(const char *mnt,
>> + struct mount_opts *mo)
>> {
>> char tmp[128];
>> const char *devname = fuse_mnt_get_devname();
>> @@ -500,6 +500,26 @@ out_close:
>> return -1;
>> }
>>
>> +#if defined(HAVE_NEW_MOUNT_API)
>> +/**
>> + * Wrapper for fuse_kern_fsmount that accepts struct mount_opts
>> + * @mnt: mountpoint
>> + * @mo: mount options
>> + * @mnt_opts: mount options to pass to the kernel
>> + *
>> + * Returns: 0 on success, -1 on failure with errno set
>> + */
>> +int fuse_kern_fsmount_mo(const char *mnt, struct mount_opts *mo,
>> + const char *mnt_opts)
>> +{
>> + const char *devname = fuse_mnt_get_devname();
>> +
>> + return fuse_kern_fsmount(mnt, mo->flags, mo->blkdev, mo->fsname,
>> + mo->subtype, devname, mo->kernel_opts,
>> + mnt_opts);
>> +}
>> +#endif
>> +
>> /**
>> * Complete the mount operation with an already-opened fd
>> * @mnt: mountpoint
>> @@ -643,8 +663,7 @@ void destroy_mount_opts(struct mount_opts *mo)
>> free(mo);
>> }
>>
>> -static int fuse_kern_mount_get_base_mnt_opts(struct mount_opts *mo,
>> - char **mnt_optsp)
>> +int fuse_kern_mount_get_base_mnt_opts(struct mount_opts *mo, char **mnt_optsp)
>> {
>> if (get_mnt_flag_opts(mnt_optsp, mo->flags) == -1)
>> return -1;
>> diff --git a/lib/mount_fsmount.c b/lib/mount_fsmount.c
>> new file mode 100644
>> index 0000000000000000000000000000000000000000..cba998bc60c783a5edc0c16570f7e5512b7f1253
>> --- /dev/null
>> +++ b/lib/mount_fsmount.c
>> @@ -0,0 +1,405 @@
>> +/*
>> + * FUSE: Filesystem in Userspace
>> + * Copyright (C) 2001-2007 Miklos Szeredi <miklos@szeredi.hu>
>> + * 2026 Bernd Schubert <bernd@bsbernd.com>
>> + *
>> + * New Linux mount API (fsopen/fsconfig/fsmount/move_mount) support.
>> + *
>> + * This program can be distributed under the terms of the GNU LGPLv2.
>> + * See the file LGPL2.txt.
>> + */
>> +
>> +#define _GNU_SOURCE
>> +
>> +#include "fuse_config.h"
>> +#include "fuse_misc.h"
>> +#include "mount_util.h"
>> +#include "mount_i_linux.h"
>> +
>> +#include <stdio.h>
>> +#include <stdlib.h>
>> +#include <unistd.h>
>> +#include <string.h>
>> +#include <fcntl.h>
>> +#include <errno.h>
>> +#include <sys/mount.h>
>> +#include <sys/syscall.h>
>> +
>> +/* Mount attribute flags for fsmount() - from linux/mount.h */
>> +#ifndef MOUNT_ATTR_RDONLY
>> +#define MOUNT_ATTR_RDONLY 0x00000001
>> +#endif
>> +#ifndef MOUNT_ATTR_NOSUID
>> +#define MOUNT_ATTR_NOSUID 0x00000002
>> +#endif
>> +#ifndef MOUNT_ATTR_NODEV
>> +#define MOUNT_ATTR_NODEV 0x00000004
>> +#endif
>> +#ifndef MOUNT_ATTR_NOEXEC
>> +#define MOUNT_ATTR_NOEXEC 0x00000008
>> +#endif
>> +#ifndef MOUNT_ATTR__ATIME
>> +#define MOUNT_ATTR__ATIME 0x00000070
>> +#endif
>> +#ifndef MOUNT_ATTR_RELATIME
>> +#define MOUNT_ATTR_RELATIME 0x00000000
>> +#endif
>> +#ifndef MOUNT_ATTR_NOATIME
>> +#define MOUNT_ATTR_NOATIME 0x00000010
>> +#endif
>> +#ifndef MOUNT_ATTR_STRICTATIME
>> +#define MOUNT_ATTR_STRICTATIME 0x00000020
>> +#endif
>> +#ifndef MOUNT_ATTR_NODIRATIME
>> +#define MOUNT_ATTR_NODIRATIME 0x00000080
>> +#endif
>> +#ifndef MOUNT_ATTR_NOSYMFOLLOW
>> +#define MOUNT_ATTR_NOSYMFOLLOW 0x00200000
>> +#endif
>
> /me notes that all MOUNT_ATTR_ before NOSYMFOLLOW were introduced in the
> initial commit so if the meson test thinks the new fsmount api is
> present then you can rely on flags being defined.
Thanks, removed those flags and added a comment.
>
>> +
>> +/*
>> + * Convert MS_* mount flags to MOUNT_ATTR_* mount attributes.
>> + * These flags are passed to fsmount(), not fsconfig().
>> + * Mount attributes control mount-point level behavior.
>> + */
>> +static unsigned long ms_flags_to_mount_attrs(unsigned long flags)
>> +{
>> + unsigned long attrs = 0;
>> +
>> + if (flags & MS_NOSUID)
>> + attrs |= MOUNT_ATTR_NOSUID;
>> + if (flags & MS_NODEV)
>> + attrs |= MOUNT_ATTR_NODEV;
>> + if (flags & MS_NOEXEC)
>> + attrs |= MOUNT_ATTR_NOEXEC;
>> + if (flags & MS_NOATIME)
>> + attrs |= MOUNT_ATTR_NOATIME;
>> + else if (flags & MS_RELATIME)
>> + attrs |= MOUNT_ATTR_RELATIME;
>> + else if (flags & MS_STRICTATIME)
>> + attrs |= MOUNT_ATTR_STRICTATIME;
>> + if (flags & MS_NODIRATIME)
>> + attrs |= MOUNT_ATTR_NODIRATIME;
>> + if (flags & MS_NOSYMFOLLOW)
>> + attrs |= MOUNT_ATTR_NOSYMFOLLOW;
>> +
>> + return attrs;
>> +}
>> +
>> +/*
>> + * Apply VFS superblock flags to the filesystem context.
>> + * Only handles flags that are filesystem parameters (ro, sync, dirsync).
>> + * Mount attributes (nosuid, nodev, etc.) are handled separately via fsmount().
>> + */
>> +static int apply_mount_flags(int fsfd, unsigned long flags)
>> +{
>> + int res;
>> +
>> + /* Handle read-only flag */
>> + if (flags & MS_RDONLY) {
>> + res = fsconfig(fsfd, FSCONFIG_SET_FLAG, "ro", NULL, 0);
>> + if (res == -1) {
>> + fprintf(stderr,
>> + "fuse: fsconfig SET_FLAG ro failed: %s\n",
>> + strerror(errno));
>> + return -errno;
>> + }
>> + }
>> +
>> + /* Handle sync flag */
>> + if (flags & MS_SYNCHRONOUS) {
>> + res = fsconfig(fsfd, FSCONFIG_SET_FLAG, "sync", NULL, 0);
>> + if (res == -1) {
>> + fprintf(stderr,
>> + "fuse: fsconfig SET_FLAG sync failed: %s\n",
>> + strerror(errno));
>> + return -errno;
>> + }
>> + }
>> +
>> +#ifndef __NetBSD__
>> + /* Handle dirsync flag */
>> + if (flags & MS_DIRSYNC) {
>> + res = fsconfig(fsfd, FSCONFIG_SET_FLAG, "dirsync", NULL, 0);
>> + if (res == -1) {
>> + fprintf(stderr,
>> + "fuse: fsconfig SET_FLAG dirsync failed: %s\n",
>> + strerror(errno));
>> + return -errno;
>> + }
>> + }
>> +#endif
>> +
>> + return 0;
>> +}
>
> Oh, nice treatment of the MS_ flags that don't have a corresponding
> MOUNT_ATTR_ flag. I should incorporate that into mount_service.c.
>
>> +
>> +static int apply_opt_fd(int fsfd, const char *value)
>> +{
>> + int res;
>> +
>> + /* The fd parameter is a u32 value, not a file descriptor to pass */
>> + res = fsconfig(fsfd, FSCONFIG_SET_STRING, "fd", value, 0);
>> + if (res == -1) {
>> + fprintf(stderr, "fuse: fsconfig SET_STRING fd=%s failed: %s\n",
>> + value, strerror(errno));
>> + return -errno;
>
> Did you know that you can read the kernel's error messages from the
> fsopen fd?
>
> https://git.kernel.org/pub/scm/linux/kernel/git/djwong/libfuse.git/tree/util/mount_service.c?h=fuse-service-container&id=d02bbab680ab4689bbd5e274735e91bd38e5f47f#n693
Ah nice, I kind of copied over this function now.
>
>> + }
>> + return 0;
>> +}
>> +
>> +static int apply_opt_string(int fsfd, const char *key, const char *value)
>> +{
>> + int res;
>> +
>> + res = fsconfig(fsfd, FSCONFIG_SET_STRING, key, value, 0);
>> + if (res == -1) {
>> + fprintf(stderr,
>> + "fuse: fsconfig SET_STRING %s=%s failed: %s\n",
>> + key, value, strerror(errno));
>> + return -errno;
>
> I think you have to save errno explicitly here, because fprintf and
> strerror could fail and set errno to something else. For example, if
> fsconfig returns EINVAL but stderr points to a file on a completely full
> filesystem, the ENOSPC from the fprintf call (or more likely the write()
> underneath it) will obliterate the EINVAL.
Absolutely, had slipped through. Thank you!
>
>> + }
>> + return 0;
>> +}
>> +
>> +static int apply_opt_flag(int fsfd, const char *opt)
>> +{
>> + int res;
>> +
>> + res = fsconfig(fsfd, FSCONFIG_SET_FLAG, opt, NULL, 0);
>> + if (res == -1) {
>> + fprintf(stderr, "fuse: fsconfig SET_FLAG %s failed: %s\n",
>> + opt, strerror(errno));
>> + return -errno;
>> + }
>> + return 0;
>> +}
>> +
>> +static int apply_opt_key_value(int fsfd, char *opt)
>> +{
>> + char *eq;
>> + const char *key;
>> + const char *value;
>> +
>> + eq = strchr(opt, '=');
>> + if (!eq)
>> + return apply_opt_flag(fsfd, opt);
>> +
>> + *eq = '\0';
>> + key = opt;
>> + value = eq + 1;
>> +
>> + if (strcmp(key, "fd") == 0)
>> + return apply_opt_fd(fsfd, value);
>> +
>> + return apply_opt_string(fsfd, key, value);
>> +}
>> +
>> +/**
>> + * Check if an option is a mount attribute (handled by fsmount, not fsconfig)
>> + */
>> +static int is_mount_attr_opt(const char *opt)
>> +{
>> + /* These options are mount attributes passed to fsmount(), not fsconfig() */
>> + return strcmp(opt, "nosuid") == 0 ||
>> + strcmp(opt, "suid") == 0 ||
>> + strcmp(opt, "nodev") == 0 ||
>> + strcmp(opt, "dev") == 0 ||
>> + strcmp(opt, "noexec") == 0 ||
>> + strcmp(opt, "exec") == 0 ||
>> + strcmp(opt, "noatime") == 0 ||
>> + strcmp(opt, "atime") == 0 ||
>> + strcmp(opt, "nodiratime") == 0 ||
>> + strcmp(opt, "diratime") == 0 ||
>> + strcmp(opt, "relatime") == 0 ||
>> + strcmp(opt, "norelatime") == 0 ||
>> + strcmp(opt, "strictatime") == 0 ||
>> + strcmp(opt, "nostrictatime") == 0 ||
>> + strcmp(opt, "nosymfollow") == 0 ||
>> + strcmp(opt, "symfollow") == 0;
>> +}
>> +
>> +/*
>> + * Parse kernel options string and apply via fsconfig
>> + * Options are comma-separated key=value pairs
>> + */
>> +static int apply_mount_opts(int fsfd, const char *opts)
>> +{
>> + char *opts_copy;
>> + char *opt;
>> + char *saveptr;
>> + int res;
>> +
>> + if (!opts || !*opts)
>> + return 0;
>> +
>> + opts_copy = strdup(opts);
>> + if (!opts_copy) {
>> + fprintf(stderr, "fuse: failed to allocate memory\n");
>> + return -ENOMEM;
>> + }
>> +
>> + opt = strtok_r(opts_copy, ",", &saveptr);
>> + while (opt) {
>> + /* Skip mount attributes - they're handled by fsmount(), not fsconfig() */
>> + if (!is_mount_attr_opt(opt)) {
>> + res = apply_opt_key_value(fsfd, opt);
>
> Does fuse_session_new_versioned convert those magic string flags from
> is_mount_attr_opt into their MS_* equivalents such that apply_mount_opts
> won't see the string versions?
Comment updated to
/*
* Skip mount attributes, they're handled by fsmount()
* not fsconfig().
*
* These string options (nosuid, nodev, etc.) are reconstructed
* from MS_* flags by get_mnt_flag_opts() in lib/mount.c and
* get_mnt_opts() in util/fusermount.c. Both the library path
* (via fuse_kern_mount_get_base_mnt_opts) and fusermount3 path
* rebuild these strings from the flags bitmask and pass them in
* mnt_opts. They must be filtered here because they are mount
* attributes (passed to fsmount via MOUNT_ATTR_*), not
* filesystem parameters (which would be passed to fsconfig).
*/
if (!is_mount_attr_opt(opt)) {
>
>> + if (res < 0) {
>> + free(opts_copy);
>> + return res;
>> + }
>> + }
>> + opt = strtok_r(NULL, ",", &saveptr);
>> + }
>> +
>> + free(opts_copy);
>> + return 0;
>> +}
>> +
>> +
>> +/**
>> + * Mount using the new Linux mount API (fsopen/fsconfig/fsmount/move_mount)
>> + * @mnt: mountpoint
>> + * @flags: mount flags (MS_NOSUID, MS_NODEV, etc.)
>> + * @blkdev: 1 for fuseblk, 0 for fuse
>> + * @fsname: filesystem name (or NULL)
>> + * @subtype: filesystem subtype (or NULL)
>> + * @source_dev: device name for building source string
>> + * @kernel_opts: kernel mount options string
>> + * @mnt_opts: additional mount options to pass to the kernel
>> + *
>> + * Returns: 0 on success, -1 on failure with errno set
>> + */
>> +int fuse_kern_fsmount(const char *mnt, unsigned long flags, int blkdev,
>> + const char *fsname, const char *subtype,
>> + const char *source_dev, const char *kernel_opts,
>> + const char *mnt_opts)
>> +{
>> + const char *type;
>> + char *source = NULL;
>> + int fsfd = -1;
>> + int mntfd = -1;
>> + int err, res;
>> + unsigned long mount_attrs;
>> +
>> + /* Determine filesystem type */
>> + type = blkdev ? "fuseblk" : "fuse";
>> +
>> + /* Try to open filesystem context */
>> + fsfd = fsopen(type, FSOPEN_CLOEXEC);
>> + if (fsfd == -1) {
>> + fprintf(stderr, "fuse: fsopen(%s) failed: %s\n", type,
>> + strerror(errno));
>> + return -1;
>> + }
>> +
>> + /* Build source string */
>> + source = malloc((fsname ? strlen(fsname) : 0) +
>> + (subtype ? strlen(subtype) : 0) +
>> + strlen(source_dev) + 32);
>> + err = -ENOMEM;
>> + if (!source) {
>> + fprintf(stderr, "fuse: failed to allocate memory\n");
>> + goto out_close_fsfd;
>> + }
>> +
>> + strcpy(source, fsname ? fsname : (subtype ? subtype : source_dev));
>
> This is almost fuse_mnt_build_source()
Yeah, we don't have struct mount_opts that fuse_mnt_build_source() takes.
Going to improve that in a later patch.
Thanks,
Bernd
^ permalink raw reply [flat|nested] 59+ messages in thread
* Re: [PATCH 19/19] Add support for sync-init of unprivileged daemons
2026-03-23 17:45 ` [PATCH 19/19] Add support for sync-init of unprivileged daemons Bernd Schubert
@ 2026-03-24 20:21 ` Darrick J. Wong
2026-03-24 21:53 ` Bernd Schubert
0 siblings, 1 reply; 59+ messages in thread
From: Darrick J. Wong @ 2026-03-24 20:21 UTC (permalink / raw)
To: Bernd Schubert
Cc: linux-fsdevel, Miklos Szeredi, Joanne Koong, Bernd Schubert
On Mon, Mar 23, 2026 at 06:45:14PM +0100, Bernd Schubert wrote:
> From: Bernd Schubert <bschubert@ddn.com>
>
> This makes use of the bidirectional fusermount. Added is
> doc/README.mount, which explains the new bidirectional
> communication with fusermount.
>
> Signed-off-by: Bernd Schubert <bschubert@ddn.com>
All right, last patch before I go have some lunch and circle back to
your recent replies :)
> ---
> doc/README.mount | 86 ++++++++++++++++++++++++
> doc/README.sync-init | 184 +++++++++++++++++++++++++++++++++++++++++++++++++++
These new readmes feel like they ought to go at the beginning (or at
least a separate patch) to argue for why synchronous init is needed
in libfuse? I do appreciate the flow diagrams though.
> lib/fuse_lowlevel.c | 115 ++++++++++++++++++++++++++------
> lib/mount.c | 126 ++++++++++++++++++++++++++++++++++-
> lib/mount_i_linux.h | 7 ++
> util/fusermount.c | 2 -
> 6 files changed, 494 insertions(+), 26 deletions(-)
>
> diff --git a/doc/README.mount b/doc/README.mount
> new file mode 100644
> index 0000000000000000000000000000000000000000..526382ad8a5f6b405a7cb1927b79bacd6c2c2c5c
> --- /dev/null
> +++ b/doc/README.mount
> @@ -0,0 +1,86 @@
> +FUSE Mount API Flowcharts
> +=========================
> +
> +Old Mount API
> +-------------
> +
> +fuse_kern_mount()
> + |
> + +-- fuse_mount_sys()
> + | +-- Try direct mount → mount() syscall
> + | +-- On EPERM: fuse_mount_fusermount()
> + | +-- socketpair()
> + | +-- spawn fusermount3 (no --sync-init)
> + | +-- fusermount3: open /dev/fuse, mount(), send fd
> + | +-- receive_fd() → return fd
> + |
> + +-- Worker threads started AFTER mount
> + └─> FUSE_INIT asynchronous (queued in kernel)
> +
> +
> +New Mount API - Privileged Mount
> +---------------------------------
> +
> +fuse_session_mount_new_api()
> + |
> + +-- fuse_kern_mount_prepare() → open /dev/fuse → fd
> + |
> + +-- session_start_sync_init(se, fd)
> + | +-- ioctl(fd, FUSE_DEV_IOC_SYNC_INIT)
> + | +-- pthread_create(worker) → ready to process FUSE_INIT
> + |
> + +-- fuse_kern_fsmount_mo()
> + | +-- fsopen/fsconfig/fsmount (BLOCKS until FUSE_INIT completes)
> + | +-- Worker processes FUSE_INIT during fsmount()
> + | +-- move_mount()
> + |
> + +-- session_wait_sync_init_completion(se) → pthread_join
> + └─> return fd
> +
> +
> +New Mount API - EPERM Fallback (fusermount3 with sync-init)
> +------------------------------------------------------------
> +
> +fuse_session_mount_new_api()
> + |
> + +-- fuse_kern_mount_prepare() → open /dev/fuse → fd1
> + |
> + +-- session_start_sync_init(se, fd1)
> + | +-- ioctl(fd1, FUSE_DEV_IOC_SYNC_INIT)
> + | +-- pthread_create(worker) → ready with fd1
> + |
> + +-- fuse_kern_fsmount_mo() → EPERM
> + |
> + +-- *** FALLBACK TO FUSERMOUNT3 WITH SYNC-INIT ***
> + |
> + +-- session_wait_sync_init_completion(se)
> + | +-- pthread_cancel/join → terminate worker with wrong fd1
> + |
> + +-- close(fd1)
> + |
> + +-- fuse_mount_fusermount_sync_init() [NEW]
> + | +-- socketpair()
> + | +-- spawn fusermount3 --sync-init
> + | +-- fusermount3: open /dev/fuse → fd2, send fd2
> + | +-- receive_fd() → fd2
> + | +-- fusermount3 waits for signal
> + | └─> return fd2, sock
> + |
> + +-- session_start_sync_init(se, fd2)
> + | +-- ioctl(fd2, FUSE_DEV_IOC_SYNC_INIT)
> + | +-- pthread_create(worker) → ready with fd2
> + |
> + +-- send_proceed_signal(sock) [NEW]
> + | +-- send(sock, "\0", 1) → signal fusermount3
> + |
> + +-- fusermount3: mount() (BLOCKS)
> + | +-- Kernel sends FUSE_INIT to fd2
> + | +-- Worker processes FUSE_INIT
> + | +-- mount() returns
> + |
> + +-- close(sock)
> + |
> + +-- session_wait_sync_init_completion(se) → pthread_join
> + |
> + └─> return fd2
> +
> diff --git a/doc/README.sync-init b/doc/README.sync-init
> new file mode 100644
> index 0000000000000000000000000000000000000000..44e47a2eef2c45026abaa19562537eef37f256b9
> --- /dev/null
> +++ b/doc/README.sync-init
> @@ -0,0 +1,184 @@
> +FUSE Synchronous vs Asynchronous FUSE_INIT
> +============================================
> +
> +This document explains the difference between asynchronous and synchronous
> +FUSE_INIT processing, and when each mode is used.
> +
> +
> +Overview
> +--------
> +
> +FUSE_INIT is the initial handshake between the kernel FUSE module and the
> +userspace filesystem daemon. During this handshake, the kernel and daemon
> +negotiate capabilities, protocol version, and various feature flags.
> +
> +Asynchronous FUSE_INIT (Traditional Behavior)
> +----------------------------------------------
> +
> +In the traditional asynchronous mode:
> +
> +1. mount() syscall completes and returns to caller
> +2. Filesystem appears mounted to the system
> +3. FUSE daemon starts worker threads
> +4. Worker threads process FUSE_INIT request
> +5. Filesystem becomes fully operational
> +
> +Timeline:
> + mount() -----> returns
> + |
> + v
> + FUSE_INIT sent
> + |
> + v
> + daemon processes FUSE_INIT
> + |
> + v
> + filesystem ready
> +
> +Limitations:
> +
> +1. **No early requests**: The kernel cannot send requests (like getxattr)
> + during the mount() syscall. This breaks SELinux, which needs to query
> + extended attributes on the root inode immediately upon mounting.
> +
> +2. **Daemonization timing**: With the old fuse_daemonize() API, the daemon
> + must call it AFTER mount, because there's no way to report mount failures
> + to the parent process if daemonization happens first.
> +
> +3. **No custom root inode**: The root inode ID is hardcoded to FUSE_ROOT_ID (1)
> + because FUSE_INIT hasn't been processed yet when the mount completes.
> +
> +4. **Thread startup after mount**: io_uring threads and other worker threads
> + can only start after mount() returns, not before.
Especially this part which explains why we care about sync init :)
> +
> +Synchronous FUSE_INIT (New Behavior)
> +-------------------------------------
> +
> +Kernel support: Linux kernel commit dfb84c330794 (v6.18+)
> +libfuse support: libfuse 3.19+
> +
> +In synchronous mode:
> +
> +1. FUSE daemon opens /dev/fuse
> +2. Daemon calls ioctl(fd, FUSE_DEV_IOC_SYNC_INIT)
> +3. Daemon starts worker thread
> +4. Daemon calls mount() syscall
> +5. Kernel sends FUSE_INIT during mount() - mount() blocks
> +6. Worker thread processes FUSE_INIT while mount() is blocked
> +7. Worker thread may process additional requests (getxattr, etc.)
> +8. mount() syscall completes and returns
> +9. Filesystem is fully operational
> +
> +Timeline:
> + open /dev/fuse
> + |
> + v
> + ioctl(FUSE_DEV_IOC_SYNC_INIT)
> + |
> + v
> + start worker thread
> + |
> + v
> + mount() -----> blocks
> + | |
> + | v
> + | FUSE_INIT sent
> + | |
> + | v
> + | worker processes FUSE_INIT
> + | |
> + | v
> + | (possible getxattr, etc.)
> + | |
> + +-------> returns
> + |
> + v
> + filesystem ready
> +
> +Advantages:
> +
> +1. **SELinux support**: The kernel can send getxattr requests during mount()
> + to query security labels on the root inode.
> +
> +2. **Early daemonization**: The daemon can fork BEFORE mount using the new
> + fuse_daemonize_start()/signal() API, and report mount failures to the
> + parent process.
> +
> +3. **Custom root inode**: The daemon can specify a custom root inode ID
> + during FUSE_INIT, before mount() completes.
> +
> +4. **Thread startup before mount**: io_uring threads and worker threads
> + start before mount(), ensuring they're ready to handle requests.
> +
> +5. **Better error reporting**: Mount failures and initialization errors
> + can be properly reported to the parent process when using the new
> + daemonization API.
> +
> +
> +When Synchronous FUSE_INIT is Used
> +-----------------------------------
> +
> +libfuse automatically enables synchronous FUSE_INIT when:
> +
> +1. The application calls fuse_session_want_sync_init(), OR
> +2. The new daemonization API is used (fuse_daemonize_start() was called)
> +
> +Synchronous FUSE_INIT requires:
> +- Kernel support (commit dfb84c330794 or later)
> +- Worker thread started before mount()
> +- ioctl(FUSE_DEV_IOC_SYNC_INIT) succeeds
> +
> +If the kernel doesn't support synchronous FUSE_INIT, libfuse automatically
> +falls back to asynchronous mode.
> +
> +
> +Implementation Details
> +----------------------
> +
> +The synchronous FUSE_INIT implementation uses a worker thread:
> +
> +- **session_sync_init_worker()**: Thread function that polls /dev/fuse
> + and processes FUSE_INIT and any subsequent requests until mount completes.
> +
> +- **session_start_sync_init()**: Creates the worker thread before mount().
> + Calls ioctl(FUSE_DEV_IOC_SYNC_INIT) to enable kernel support.
> +
> +- **session_wait_sync_init_completion()**: Waits for the worker thread
> + to complete after mount() returns. Checks for errors.
> +
> +The worker thread processes requests in a loop until se->terminate_mount_worker
> +is set, which happens after mount() completes successfully.
> +
> +
> +Compatibility
> +-------------
> +
> +Synchronous FUSE_INIT is fully backward compatible:
> +
> +- Old kernels: ioctl returns ENOTTY, libfuse falls back to async mode
> +- Old applications: Continue to work with async FUSE_INIT
> +- New applications on old kernels: Graceful fallback to async mode
> +- New applications on new kernels: Automatic sync mode when appropriate
> +
> +
> +Example: Enabling Synchronous FUSE_INIT
> +----------------------------------------
> +
> +Explicit request:
> + struct fuse_session *se = fuse_session_new(...);
> + fuse_session_want_sync_init(se);
> + fuse_session_mount(se, mountpoint);
> +
> +Automatic (with new daemonization API):
> + fuse_daemonize_start(0); // Triggers sync init automatically
> + fuse_session_mount(se, mountpoint);
> +
> +
> +See Also
> +--------
> +
> +- doc/README.daemonize - New daemonization API documentation
> +- doc/README.fusermount - Synchronous FUSE_INIT protocol with fusermount3
> +- doc/README.mount - Mount implementation details
> +
> diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c
> index a7293a3898c37c3877eadf965d310ae2aa5cc2d1..da966217ed841744a20bee60de8ae615d1015b47 100644
> --- a/lib/fuse_lowlevel.c
> +++ b/lib/fuse_lowlevel.c
> @@ -41,6 +41,7 @@
> #include <assert.h>
> #include <sys/file.h>
> #include <sys/ioctl.h>
> +#include <sys/wait.h>
> #include <stdalign.h>
> #include <poll.h>
>
> @@ -4551,6 +4552,8 @@ static int session_wait_sync_init_completion(struct fuse_session *se)
> se->init_wakeup_fd = -1;
> }
>
> + se->init_thread = 0;
> +
> if (se->init_error != 0) {
> fuse_log(FUSE_LOG_ERR, "fuse: init worker failed: %d\n", se->init_error);
> return -1;
> @@ -4564,56 +4567,125 @@ static int session_wait_sync_init_completion(struct fuse_session *se)
> return 0;
> }
>
> -/* Only linux supports sync FUSE_INIT so far */
> +/*
> + * Mount using the new Linux mount API (fsopen/fsconfig/fsmount/move_mount)
> + * Sync-init is only supported with the new API, as the mount might hang
> + * in case of daemon crash during FUSE_INIT. That also means once the sync init
> + * ioctl succeed fallback is not allowed anymore.
> + * Returns: fd on success, -1 on failure
> + */
> static int fuse_session_mount_new_api(struct fuse_session *se,
> - const char *mountpoint)
> + const char *mountpoint, bool *fall_back)
> {
> int fd = -1;
> + int sock_fd = -1;
> + pid_t fusermount_pid = -1;
> int res, err;
> char *mnt_opts = NULL;
> char *mnt_opts_with_fd = NULL;
> char fd_opt[32];
>
> res = fuse_kern_mount_get_base_mnt_opts(se->mo, &mnt_opts);
> + err = -EIO;
> if (res == -1) {
> fuse_log(FUSE_LOG_ERR, "fuse: failed to get base mount options\n");
> - err = -EIO;
Odd churn in this function...
> goto err;
> }
>
> fd = fuse_kern_mount_prepare(mountpoint, se->mo);
> if (fd == -1) {
> fuse_log(FUSE_LOG_ERR, "Mount preparation failed.\n");
> - err = -EIO;
> goto err;
> }
>
> - /*
> - * Enable synchronous FUSE_INIT and start worker thread, sync init
> - * failure is not an error
> - */
> + *fall_back = true;
> se->fd = fd;
> err = session_start_sync_init(se, fd);
> if (err) {
> /* ENOTTY means kernel doesn't support sync init - not an error */
> if (err != -ENOTTY)
> goto err;
> + } else {
> + *fall_back = false;
> }
> +
> +
> snprintf(fd_opt, sizeof(fd_opt), "fd=%i", fd);
> + err = -ENOMEM;
> if (fuse_opt_add_opt(&mnt_opts_with_fd, mnt_opts) == -1 ||
> fuse_opt_add_opt(&mnt_opts_with_fd, fd_opt) == -1) {
> - err = -ENOMEM;
> goto err;
> }
>
> + /* Try to mount directly */
> err = fuse_kern_fsmount_mo(mountpoint, se->mo, mnt_opts_with_fd);
> +
> + /* If mount failed with EPERM, fall back to fusermount3 with sync-init */
...since this is the new "actually use bidirectional fusermount3" code
mentioned in the commit message.
> + if (err < 0 && errno == EPERM) {
> + if (se->debug)
> + fuse_log(FUSE_LOG_DEBUG,
> + "fuse: privileged mount failed with EPERM, falling back to fusermount3\n");
> +
> + /* Terminate worker thread with wrong fd */
> + if (session_wait_sync_init_completion(se) < 0)
> + fuse_log(FUSE_LOG_ERR, "fuse: sync init completion failed\n");
> +
> + /* Close the privileged fd */
> + close(fd);
> + fd = -1;
> + se->fd = -1;
> +
> + /* Call fusermount3 with --sync-init */
> + err = -ENOTSUP;
> + fd = mount_fusermount_obtain_fd(mountpoint, se->mo, mnt_opts,
> + &sock_fd, &fusermount_pid);
> + if (fd < 0) {
> + fuse_log(
> + FUSE_LOG_ERR,
> + "fuse: fusermount3 sync-init failed\n");
> + goto err;
> + }
> +
> + /* Start worker thread with correct fd from fusermount3 */
> + se->fd = fd;
> + err = session_start_sync_init(se, fd);
> + if (err) {
> + if (err != -ENOTTY) {
> + fuse_log(
> + FUSE_LOG_ERR,
> + "fuse: failed to start sync init worker\n");
> + goto err_with_sock;
> + }
> + } else {
> + *fall_back = false;
We already set *fall_back to false above, didn't we? I'm slightly
confused -- should we set *fall_back=true any time this function returns
nonzero?
> + }
> +
> + /* Send proceed signal and wait for mount result */
> + err = fuse_fusermount_proceed_mnt(sock_fd);
> + if (err < 0) {
> + err = -EIO;
> + goto err_with_sock;
> + }
> + } else if (err < 0) {
> + /* Mount failed with non-EPERM error, bail out */
> + goto err;
> + }
> +
> +err_with_sock:
> + if (sock_fd >= 0) {
> + close(sock_fd);
> + /* Reap fusermount3 child process to prevent zombie */
> + if (fusermount_pid > 0)
> + waitpid(fusermount_pid, NULL, 0);
> + }
> err:
> if (err < 0) {
> + /* Close fd first to unblock worker thread */
> if (fd >= 0)
> close(fd);
> fd = -1;
> se->fd = -1;
> - se->error = -errno;
> + se->error = err;
> }
> /* Wait for synchronous FUSE_INIT to complete */
> if (session_wait_sync_init_completion(se) < 0)
> @@ -4625,10 +4697,11 @@ err:
> }
> #else
> static int fuse_session_mount_new_api(struct fuse_session *se,
> - const char *mountpoint)
> + const char *mountpoint, bool *fall_back)
> {
> (void) se;
> (void) mountpoint;
> + (void) fall_back;
>
> return -1;
> }
> @@ -4638,6 +4711,7 @@ int fuse_session_mount(struct fuse_session *se, const char *_mountpoint)
> {
> int fd;
> char *mountpoint;
> + bool fall_back;
>
> if (_mountpoint == NULL) {
> fuse_log(FUSE_LOG_ERR, "Invalid null-ptr mountpoint!\n");
> @@ -4681,21 +4755,18 @@ int fuse_session_mount(struct fuse_session *se, const char *_mountpoint)
> return 0;
> }
>
> - /* new linux mount api */
> - fd = fuse_session_mount_new_api(se, mountpoint);
> - if (fd >= 0)
> - goto out;
> + /* new linux mount api (and sync init) */
> + fd = fuse_session_mount_new_api(se, mountpoint, &fall_back);
>
> /* fall back to old API */
> - se->error = 0; /* reset error of new api */
> - fd = fuse_kern_mount(mountpoint, se->mo);
> - if (fd < 0)
> - goto error_out;
> + if (fall_back && fd < 0) {
> + se->error = 0; /* reset error of new api */
> + fd = fuse_kern_mount(mountpoint, se->mo);
> + if (fd < 0)
> + goto error_out;
> + }
>
> -out:
> se->fd = fd;
> -
> - /* Save mountpoint */
> se->mountpoint = mountpoint;
>
> return 0;
> diff --git a/lib/mount.c b/lib/mount.c
> index 263b05051c236458b830c40181bce7f494803800..985938ea0be3e1affad19adad527a31ac2ca6034 100644
> --- a/lib/mount.c
> +++ b/lib/mount.c
> @@ -41,6 +41,7 @@
> #define FUSERMOUNT_PROG "fusermount3"
> #define FUSE_COMMFD_ENV "_FUSE_COMMFD"
> #define FUSE_COMMFD2_ENV "_FUSE_COMMFD2"
> +#define ARG_FD_ENTRY_SIZE 30
Thirty seems a bit much for an integer, especially one that can't go
above 1 million. Eh, it's just stack space. :)
> enum {
> KEY_KERN_FLAG,
> @@ -313,7 +314,7 @@ static int setup_auto_unmount(const char *mountpoint, int quiet)
> return -1;
> }
>
> - char arg_fd_entry[30];
> + char arg_fd_entry[ARG_FD_ENTRY_SIZE];
> snprintf(arg_fd_entry, sizeof(arg_fd_entry), "%i", fds[0]);
> setenv(FUSE_COMMFD_ENV, arg_fd_entry, 1);
> /*
> @@ -386,7 +387,7 @@ static int fuse_mount_fusermount(const char *mountpoint, struct mount_opts *mo,
> return -1;
> }
>
> - char arg_fd_entry[30];
> + char arg_fd_entry[ARG_FD_ENTRY_SIZE];
> snprintf(arg_fd_entry, sizeof(arg_fd_entry), "%i", fds[0]);
> setenv(FUSE_COMMFD_ENV, arg_fd_entry, 1);
> /*
> @@ -446,6 +447,127 @@ static int fuse_mount_fusermount(const char *mountpoint, struct mount_opts *mo,
> return fd;
> }
>
> +/*
> + * Mount using fusermount3 with --sync-init flag for bidirectional fd exchange
> + * Used by new mount API when privileged mount fails with EPERM
> + *
> + * Returns: fd on success, -1 on failure
> + * On success, *sock_fd_out contains the socket fd for signaling fusermount3
> + */
> +int mount_fusermount_obtain_fd(const char *mountpoint, struct mount_opts *mo,
> + const char *opts, int *sock_fd_out,
> + pid_t *pid_out)
> +{
> + int fds[2];
> + pid_t pid;
> + int res;
> + char arg_fd_entry[ARG_FD_ENTRY_SIZE];
> + posix_spawn_file_actions_t action;
> + int fd, status;
> +
> + (void)mo;
> +
> + if (!mountpoint) {
> + fuse_log(FUSE_LOG_ERR, "fuse: missing mountpoint parameter\n");
> + return -1;
> + }
> +
> + res = socketpair(PF_UNIX, SOCK_STREAM, 0, fds);
> + if (res == -1) {
> + fuse_log(FUSE_LOG_ERR, "Running %s: socketpair() failed: %s\n",
> + FUSERMOUNT_PROG, strerror(errno));
> + return -1;
> + }
> +
> + snprintf(arg_fd_entry, sizeof(arg_fd_entry), "%i", fds[0]);
> + setenv(FUSE_COMMFD_ENV, arg_fd_entry, 1);
Oh! /me realizes that FUSE_COMMFD{,2}_ENV can convey different things!
If you're trying to get fusermount to *mount* a filesystem, then it's
the AF_UNIX socket that is used to pass the /dev/fuse fd to the fuse
server and then to trigger the mount.
If you pass --auto-unmount/-U then fusermount waits for the socket to
close and then unmounts the mount.
> + snprintf(arg_fd_entry, sizeof(arg_fd_entry), "%i", fds[1]);
> + setenv(FUSE_COMMFD2_ENV, arg_fd_entry, 1);
...and I guess you can pass the fds on the cli instead of goofy
environment variables? I wonder if you should be passing them via CLI
since you know fusermount supports it. OTOH I don't really care either
way ;)
> +
> + char const *const argv[] = {
> + FUSERMOUNT_PROG,
> + "--sync-init",
> + "-o", opts ? opts : "",
> + "--",
> + mountpoint,
> + NULL,
> + };
> +
> + posix_spawn_file_actions_init(&action);
> + posix_spawn_file_actions_addclose(&action, fds[1]);
> + status = fusermount_posix_spawn(&action, argv, &pid);
> + posix_spawn_file_actions_destroy(&action);
> +
> + if (status != 0) {
> + close(fds[0]);
> + close(fds[1]);
> + return -1;
> + }
> +
> + close(fds[0]);
> +
> + fd = receive_fd(fds[1]);
> + if (fd < 0) {
> + close(fds[1]);
> + waitpid(pid, NULL, 0);
> + return -1;
> + }
> +
> + fcntl(fd, F_SETFD, FD_CLOEXEC);
> +
> + /* Return socket fd for later signaling */
> + *sock_fd_out = fds[1];
> + *pid_out = pid;
> +
> + return fd;
> +}
> +
> +/*
> + * Send proceed signal to fusermount3 and wait for mount result
> + * Returns: 0 on success, -1 on failure
> + */
> +int fuse_fusermount_proceed_mnt(int sock_fd)
> +{
> + char buf = '\0';
> + ssize_t res;
> +
> + /* Send proceed signal */
> + do {
> + res = send(sock_fd, &buf, 1, 0);
> + } while (res == -1 && errno == EINTR);
I wonder if all the pipe/socket communications ought to have been turned
into a bunch of wrappers like what I did for
mount_service.c/fuse_service.c?
That said, it looks like most of the fusermount/sync-init communcations
are single ints so maybe it doesn't matter. The communications for the
fuse servers is much more complex and hence needs more structure.
--D
> +
> + if (res != 1) {
> + fuse_log(FUSE_LOG_ERR, "fuse: failed to send proceed signal: %s\n",
> + strerror(errno));
> + return -1;
> + }
> +
> + /* Wait for mount result from fusermount3 (4-byte error code) */
> + int32_t status;
> +
> + do {
> + res = recv(sock_fd, &status, sizeof(status), 0);
> + } while (res == -1 && errno == EINTR);
> +
> + if (res != sizeof(status)) {
> + if (res == 0)
> + fuse_log(FUSE_LOG_ERR, "fuse: fusermount3 closed connection\n");
> + else
> + fuse_log(FUSE_LOG_ERR, "fuse: failed to receive mount status: %s\n",
> + strerror(errno));
> + return -1;
> + }
> +
> + if (status != 0) {
> + if (status != -EPERM)
> + fuse_log(FUSE_LOG_ERR, "fuse: fusermount3 mount failed: %s\n",
> + strerror(-status));
> + return -1;
> + }
> +
> + return 0;
> +}
> +
> #ifndef O_CLOEXEC
> #define O_CLOEXEC 0
> #endif
> diff --git a/lib/mount_i_linux.h b/lib/mount_i_linux.h
> index 867105019fa57576682091d1a650302f31e450b3..2a3386443afa73a508454124332527c35a184398 100644
> --- a/lib/mount_i_linux.h
> +++ b/lib/mount_i_linux.h
> @@ -76,5 +76,12 @@ int fuse_kern_fsmount_mo(const char *mnt, struct mount_opts *mo,
> const char *mnt_opts);
> char *fuse_mnt_build_source(const struct mount_opts *mo);
> char *fuse_mnt_build_type(const struct mount_opts *mo);
> +int mount_fusermount_obtain_fd(const char *mountpoint,
> + struct mount_opts *mo,
> + const char *opts, int *sock_fd_out,
> + pid_t *pid_out);
> +
> +int fuse_fusermount_proceed_mnt(int sock_fd);
> +
>
> #endif /* FUSE_MOUNT_I_H_ */
> diff --git a/util/fusermount.c b/util/fusermount.c
> index 808b4afd89ceb49273c944d43bffe5033e27549b..e6d973687bb4eef0d7e6626a1028cc32dd177e89 100644
> --- a/util/fusermount.c
> +++ b/util/fusermount.c
> @@ -1397,7 +1397,6 @@ struct mount_context {
> char *source;
> char *mnt_opts;
> char *x_opts;
> - const char *type;
> };
>
> /*
> @@ -1521,7 +1520,6 @@ static int mount_fuse_finish_fsmount(const char *mnt, const char *opts,
>
> /* Store results in context */
> ctx->source = mp.source;
> - ctx->type = mp.type;
> ctx->mnt_opts = final_mnt_opts;
> *type = mp.type;
>
>
> --
> 2.43.0
>
>
^ permalink raw reply [flat|nested] 59+ messages in thread
* Re: [PATCH 12/19] fuse mount: Support synchronous FUSE_INIT (privileged daemon)
2026-03-24 0:03 ` Darrick J. Wong
@ 2026-03-24 20:42 ` Bernd Schubert
2026-03-24 22:50 ` Darrick J. Wong
0 siblings, 1 reply; 59+ messages in thread
From: Bernd Schubert @ 2026-03-24 20:42 UTC (permalink / raw)
To: Darrick J. Wong, Bernd Schubert
Cc: linux-fsdevel@vger.kernel.org, Miklos Szeredi, Joanne Koong,
Kevin Chen
On 3/24/26 01:03, Darrick J. Wong wrote:
> On Mon, Mar 23, 2026 at 06:45:07PM +0100, Bernd Schubert wrote:
>> From: Bernd Schubert <bschubert@ddn.com>
>>
>> Add synchronous FUSE_INIT processing during mount() to
>> enable early daemonization with proper error reporting
>> to the parent process.
>>
>> A new mount thread is needed that handles FUSE_INIT and
>> possible other requests at mount time (like getxattr for selinux).
>> The kernel sends FUSE_INIT during the mount() syscall. Without a thread
>> to process it, mount() blocks forever.
>>
>> Mount thread lifetime:
>> Created before mount() syscall in fuse_start_sync_init_worker()
>> Processes requests until se->mount_finished is set (after mount() returns)
>> Joined after successful mount in fuse_wait_sync_init_completion()
>> Cancelled if mount fails (direct → fusermount3 fallback)
>
> Hrmm, so the main thread (of the child process after daemonization)
> calls mount(), having started a child thread to read and process
> FUSE_INIT + any other mount-related fuse requests?
>
> Later I see an eventfd being used to signal this background thread that
> it should exit and (presumably) let the real request processing queues
> take over. I'm curious why an eventfd here when pipes were used
> earlier?
We cannot use eventfds for anything possibly used by BSD - which is
fuse_daemonize_ and fusermount. So far sync-init is only in the new
mount API - linux only. We need to switch to a pipe if BSD ever gets that.
>
>> Key changes:
>>
>> Add init_thread, init_error, mount_finished to struct fuse_session
>> Use FUSE_DEV_IOC_SYNC_INIT ioctl for kernel support
>> Fall back to async FUSE_INIT if unsupported
>> Auto-enabled when fuse_daemonize_active() or via
>> fuse_session_want_sync_init()
>> Allows parent to report mount/init failures instead of
>> exiting immediately after fork.
>>
>> Note: For now synchronous FUSE_INIT is only supported for privileged
>> mounts.
>>
>> Signed-off-by: Bernd Schubert <bschubert@ddn.com>
>> ---
>> include/fuse_lowlevel.h | 12 +++
>> lib/fuse_daemonize.c | 8 ++
>> lib/fuse_i.h | 16 ++++
>> lib/fuse_lowlevel.c | 192 ++++++++++++++++++++++++++++++++++++++++++++++--
>> lib/mount.c | 5 +-
>> lib/mount_fsmount.c | 5 +-
>> 6 files changed, 229 insertions(+), 9 deletions(-)
>>
>> diff --git a/include/fuse_lowlevel.h b/include/fuse_lowlevel.h
>> index ee0bd8d71d95e4d57ebb4873dca0f2b36e22a649..d8626f85bdaf497534cd2835a589e30f1f4e2466 100644
>> --- a/include/fuse_lowlevel.h
>> +++ b/include/fuse_lowlevel.h
>> @@ -2429,6 +2429,18 @@ void fuse_session_process_buf(struct fuse_session *se,
>> */
>> int fuse_session_receive_buf(struct fuse_session *se, struct fuse_buf *buf);
>>
>> +/**
>> + * Request synchronous FUSE_INIT, i.e. FUSE_INIT is handled by the
>> + * kernel before mount is returned.
>> + *
>> + * As FUSE_INIT also starts io-uring ring threads, fork() must not be
>> + * called after this if io-uring is enabled. Also see
>> + * fuse_session_daemonize_start().
>> + *
>> + * This must be called before fuse_session_mount() to have any effect.
>> + */
>> +void fuse_session_want_sync_init(struct fuse_session *se);
>> +
>> /**
>> * Check if the request is submitted through fuse-io-uring
>> */
>> diff --git a/lib/fuse_daemonize.c b/lib/fuse_daemonize.c
>> index 5d191e7d737d04876d02ef6bd526061c003d2ab7..1a32ef74093f231091b4f541e5b9136bff72024f 100644
>> --- a/lib/fuse_daemonize.c
>> +++ b/lib/fuse_daemonize.c
>> @@ -9,6 +9,7 @@
>> #define _GNU_SOURCE
>>
>> #include "fuse_daemonize.h"
>> +#include "fuse_i.h"
>>
>> #include <fcntl.h>
>> #include <poll.h>
>> @@ -282,3 +283,10 @@ bool fuse_daemonize_active(void)
>>
>> return dm != NULL && (dm->daemonized || dm->active);
>> }
>> +
>> +bool fuse_daemonize_set(void)
>> +{
>> + struct fuse_daemonize *dm = atomic_load(&daemonize);
>> +
>> + return dm != NULL;
>> +}
>> diff --git a/lib/fuse_i.h b/lib/fuse_i.h
>> index 6d63c9fd2149eb4ae3b0e0170640a4ce2eed4705..93ab7ac2fadf9395af70487c7626cc57c2948d56 100644
>> --- a/lib/fuse_i.h
>> +++ b/lib/fuse_i.h
>> @@ -112,6 +112,10 @@ struct fuse_session {
>>
>> /* synchronous FUSE_INIT support */
>> bool want_sync_init;
>> + pthread_t init_thread;
>> + int init_error;
>> + _Atomic bool terminate_mount_worker;
>> + int init_wakeup_fd;
>>
>> /* io_uring */
>> struct fuse_session_uring uring;
>> @@ -221,7 +225,11 @@ void fuse_chan_put(struct fuse_chan *ch);
>> /* Mount-related functions */
>> void fuse_mount_version(void);
>> void fuse_kern_unmount(const char *mountpoint, int fd);
>> +int fuse_kern_mount_get_base_mnt_opts(struct mount_opts *mo, char **mnt_optsp);
>> int fuse_kern_mount(const char *mountpoint, struct mount_opts *mo);
>> +int fuse_kern_mount_prepare(const char *mountpoint, struct mount_opts *mo);
>> +int fuse_kern_do_mount(const char *mountpoint, struct mount_opts *mo,
>> + const char *mnt_opts);
>>
>> int fuse_send_reply_iov_nofree(fuse_req_t req, int error, struct iovec *iov,
>> int count);
>> @@ -255,6 +263,14 @@ int fuse_session_loop_mt_312(struct fuse_session *se, struct fuse_loop_config *c
>> */
>> int fuse_loop_cfg_verify(struct fuse_loop_config *config);
>>
>> +/**
>> + * Check if daemonization is set.
>> + *
>> + * @return true if set, false otherwise
>> + */
>> +bool fuse_daemonize_set(void);
>> +
>> +
>>
>> /*
>> * This can be changed dynamically on recent kernels through the
>> diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c
>> index 626233df20f49fa89cd9327f94340169d7061f75..b10def03f3666757d312f87f177a560483691d6f 100644
>> --- a/lib/fuse_lowlevel.c
>> +++ b/lib/fuse_lowlevel.c
>> @@ -4230,6 +4230,7 @@ fuse_session_new_versioned(struct fuse_args *args,
>> goto out1;
>> }
>> se->fd = -1;
>> + se->init_wakeup_fd = -1;
>> se->conn.max_write = FUSE_DEFAULT_MAX_PAGES_LIMIT * getpagesize();
>> se->bufsize = se->conn.max_write + FUSE_BUFFER_HEADER_SIZE;
>> se->conn.max_readahead = UINT_MAX;
>> @@ -4402,6 +4403,167 @@ int fuse_session_custom_io_30(struct fuse_session *se,
>> }
>>
>> #if defined(HAVE_NEW_MOUNT_API)
>> +
>> +/* Worker thread for synchronous FUSE_INIT */
>> +static void *session_sync_init_worker(void *data)
>> +{
>> + struct fuse_session *se = (struct fuse_session *)data;
>> + struct fuse_buf fbuf = {
>> + .mem = NULL,
>> + };
>> + struct pollfd pfds[2];
>> + int res;
>> +
>> + pfds[0].fd = se->fd;
>> + pfds[0].events = POLLIN;
>> + pfds[0].revents = 0;
>> + pfds[1].fd = se->init_wakeup_fd;
>> + pfds[1].events = POLLIN;
>> + pfds[1].revents = 0;
>> +
>> + /*
>> + * Process requests until mount completes. With SELinux there may be
>> + * additional requests (like getattr) after FUSE_INIT before mount
>> + * returns.
>> + */
>> + while (!atomic_load(&se->terminate_mount_worker)) {
>
> We don't ever *read* the init_wakeup_fd event, so I wonder if the
> _Atomic flag here is really needed? I gather we need acquire semantics
> or something?
My lazyness. I had quickly updated the code yesterday morning to use
poll and eventfd, because Kevin noticed issues with xfstests. Before it
was a regular thread stopped with pthread_cancel and then actually
terminated the thread when it already might have fetched a post-mount
request - the request then got never handled.
Next on my list is to look into the fuse-io-uring issues that Kevin also
reports with this patch series.
>
>> + res = poll(pfds, 2, -1);
>> + if (res == -1) {
>> + if (errno == EINTR)
>> + continue;
>> + se->init_error = -errno;
>> + break;
>> + }
>> +
>> + if (pfds[1].revents & POLLIN)
>> + break;
>> +
>> + if (pfds[0].revents & POLLIN) {
>> + res = fuse_session_receive_buf_internal(se, &fbuf, NULL);
>> + if (res == -EINTR)
>> + continue;
>> + if (res <= 0) {
>> + se->init_error = res < 0 ? res : -EINVAL;
>> + break;
>> + }
>> +
>> + fuse_session_process_buf_internal(se, &fbuf, NULL);
>> + }
>> + }
>> +
>> + fuse_buf_free(&fbuf);
>> + return NULL;
>> +}
>> +
>> +/* Enable synchronous FUSE_INIT and start worker thread */
>> +static int session_start_sync_init(struct fuse_session *se, int fd)
>> +{
>> + int err;
>> + int res;
>> +
>> + if (!se->want_sync_init &&
>> + (se->uring.enable && !fuse_daemonize_set())) {
>> + if (se->debug)
>> + fuse_log(FUSE_LOG_DEBUG,
>> + "fuse: sync init not enabled\n");
>> + return 0;
>> + }
>> +
>> + /* Try to enable synchronous FUSE_INIT */
>> + res = ioctl(fd, FUSE_DEV_IOC_SYNC_INIT);
>> + if (res) {
>> + /* ENOTTY means kernel doesn't support sync init - not an error */
>> + if (errno != ENOTTY) {
>> + fuse_log(
>> + FUSE_LOG_ERR,
>> + "fuse: failed to enable sync init: %s\n",
>> + strerror(errno));
>> + } else if (se->debug) {
>> + fuse_log(
>> + FUSE_LOG_DEBUG,
>> + "fuse: kernel doesn't support sync init\n");
>> + }
>> + return -ENOTTY;
>> + }
>> +
>> + if (se->debug)
>> + fuse_log(FUSE_LOG_DEBUG,
>> + "fuse: synchronous FUSE_INIT enabled\n");
>> +
>> + se->init_error = 0;
>> + se->terminate_mount_worker = false;
>> +
>> + se->init_wakeup_fd = eventfd(0, EFD_CLOEXEC);
>> + if (se->init_wakeup_fd == -1) {
>> + fuse_log(
>> + FUSE_LOG_ERR,
>> + "fuse: failed to create eventfd for init worker: %s\n",
>> + strerror(errno));
>> + return -EIO;
>> + }
>> +
>> + err = pthread_create(&se->init_thread, NULL,
>> + session_sync_init_worker, se);
>> + if (err != 0) {
>> + fuse_log(
>> + FUSE_LOG_ERR,
>> + "fuse: failed to create init worker thread: %s\n",
>> + strerror(err));
>> + close(se->init_wakeup_fd);
>> + se->init_wakeup_fd = -1;
>> + return -EIO;
>> + }
>> +
>> + return 0;
>> +}
>> +
>> +/* Wait for synchronous FUSE_INIT to complete */
>> +static int session_wait_sync_init_completion(struct fuse_session *se)
>> +{
>> + void *retval;
>> + int err;
>> + uint64_t val = 1;
>> +
>> + if (se->init_thread == 0)
>
> pthread_t is supposed to be an opaque datatype, so "0" could be a valid
> value for a running thread. For that matter, it could be a struct. I
> think you have to have a separate flag (or just use init_wakeup_fd >= 0)
> to determine if we're doing a synchronous init.
Ah right. I'm using se->init_wakeup_fd as you suggested.
>
>> + return 0;
>> +
>> + se->terminate_mount_worker = true;
>> +
>> + if (se->init_wakeup_fd != -1) {
>> + ssize_t res = write(se->init_wakeup_fd, &val, sizeof(val));
>> +
>> + if (res != sizeof(val)) {
>> + fuse_log(FUSE_LOG_ERR,
>> + "fuse: failed to signal init worker: %s\n",
>> + strerror(errno));
>> + }
>> + }
>> +
>> + err = pthread_join(se->init_thread, &retval);
>> + if (err != 0) {
>> + fuse_log(FUSE_LOG_ERR, "fuse: failed to join init worker thread: %s\n",
>> + strerror(err));
>> + return -1;
>> + }
>> +
>> + if (se->init_wakeup_fd != -1) {
>> + close(se->init_wakeup_fd);
>> + se->init_wakeup_fd = -1;
>> + }
>> +
>> + if (se->init_error != 0) {
>> + fuse_log(FUSE_LOG_ERR, "fuse: init worker failed: %d\n", se->init_error);
>
> Could you strerror(-se->init_error) to give the user a real error
> message?
>
>> + return -1;
>> + }
>> +
>> + if (fuse_session_exited(se)) {
>> + fuse_log(FUSE_LOG_ERR, "FUSE_INIT failed: session exited\n");
>> + return -1;
>> + }
>> +
>> + return 0;
>> +}
>> +
>> /* Only linux supports sync FUSE_INIT so far */
>> static int fuse_session_mount_new_api(struct fuse_session *se,
>> const char *mountpoint)
>> @@ -4414,8 +4576,7 @@ static int fuse_session_mount_new_api(struct fuse_session *se,
>>
>> res = fuse_kern_mount_get_base_mnt_opts(se->mo, &mnt_opts);
>> if (res == -1) {
>> - fuse_log(FUSE_LOG_ERR,
>> - "fuse: failed to get base mount options\n");
>> + fuse_log(FUSE_LOG_ERR, "fuse: failed to get base mount options\n");
>
> Unrelated change?
Removed here.
>
>> err = -EIO;
>> goto err;
>> }
>> @@ -4427,6 +4588,17 @@ static int fuse_session_mount_new_api(struct fuse_session *se,
>> goto err;
>> }
>>
>> + /*
>> + * Enable synchronous FUSE_INIT and start worker thread, sync init
>> + * failure is not an error
>> + */
>> + se->fd = fd;
>> + err = session_start_sync_init(se, fd);
>> + if (err) {
>> + /* ENOTTY means kernel doesn't support sync init - not an error */
>> + if (err != -ENOTTY)
>> + goto err;
>> + }
>
> Perhaps session_start_sync_init should not return ENOTTY, since you
> already check in session_wait_sync_init_completion if the synchronous
> init worker has been started?
Good idea, done.
>
>> snprintf(fd_opt, sizeof(fd_opt), "fd=%i", fd);
>> if (fuse_opt_add_opt(&mnt_opts_with_fd, mnt_opts) == -1 ||
>> fuse_opt_add_opt(&mnt_opts_with_fd, fd_opt) == -1) {
>> @@ -4436,13 +4608,16 @@ static int fuse_session_mount_new_api(struct fuse_session *se,
>>
>> err = fuse_kern_fsmount_mo(mountpoint, se->mo, mnt_opts_with_fd);
>> err:
>> - if (err) {
>> + if (err < 0) {
>> if (fd >= 0)
>> close(fd);
>> fd = -1;
>> se->fd = -1;
>> se->error = -errno;
>> }
>> + /* Wait for synchronous FUSE_INIT to complete */
>> + if (session_wait_sync_init_completion(se) < 0)
>> + fuse_log(FUSE_LOG_ERR, "fuse: sync init completion failed\n");
>>
>> free(mnt_opts);
>> free(mnt_opts_with_fd);
>> @@ -4452,8 +4627,8 @@ err:
>> static int fuse_session_mount_new_api(struct fuse_session *se,
>> const char *mountpoint)
>> {
>> - (void)se;
>> - (void)mountpoint;
>> + (void) se;
>> + (void) mountpoint;
>>
>> return -1;
>> }
>> @@ -4825,3 +5000,10 @@ void fuse_session_stop_teardown_watchdog(void *data)
>> pthread_join(tt->thread_id, NULL);
>> fuse_tt_destruct(tt);
>> }
>> +
>> +void fuse_session_want_sync_init(struct fuse_session *se)
>> +{
>> + if (se == NULL)
>> + return;
>> + se->want_sync_init = true;
>> +}
>> diff --git a/lib/mount.c b/lib/mount.c
>> index 30fd4d2f9bbb84c817b2363b2075456acd1c1255..12df49d9109cf918cc41aa75c5fdf84231d4d5ff 100644
>> --- a/lib/mount.c
>> +++ b/lib/mount.c
>> @@ -30,6 +30,7 @@
>> #include <sys/socket.h>
>> #include <sys/un.h>
>> #include <sys/wait.h>
>> +#include <sys/ioctl.h>
>>
>> #include "fuse_mount_compat.h"
>>
>> @@ -529,8 +530,8 @@ int fuse_kern_fsmount_mo(const char *mnt, struct mount_opts *mo,
>> * Returns: 0 on success, -1 on failure,
>> * FUSE_MOUNT_FALLBACK_NEEDED if fusermount should be used
>> */
>> -static int fuse_kern_do_mount(const char *mnt, struct mount_opts *mo,
>> - const char *mnt_opts)
>> +int fuse_kern_do_mount(const char *mnt, struct mount_opts *mo,
>> + const char *mnt_opts)
>> {
>> char *source = NULL;
>> char *type = NULL;
>> diff --git a/lib/mount_fsmount.c b/lib/mount_fsmount.c
>> index cba998bc60c783a5edc0c16570f7e5512b7f1253..f1fec790bb80f8815d485a068dc7efdff1746309 100644
>> --- a/lib/mount_fsmount.c
>> +++ b/lib/mount_fsmount.c
>> @@ -287,8 +287,9 @@ int fuse_kern_fsmount(const char *mnt, unsigned long flags, int blkdev,
>> /* Try to open filesystem context */
>> fsfd = fsopen(type, FSOPEN_CLOEXEC);
>> if (fsfd == -1) {
>> - fprintf(stderr, "fuse: fsopen(%s) failed: %s\n", type,
>> - strerror(errno));
>> + if (errno != EPERM)
>> + fprintf(stderr, "fuse: fsopen(%s) failed: %s\n", type,
>> + strerror(errno));
>
> More unrelated changes?
Right, moved one patch up.
Thanks,Bernd
^ permalink raw reply [flat|nested] 59+ messages in thread
* Re: [PATCH 14/19] Move more generic mount code to mount_util.{c,h}
2026-03-24 0:06 ` Darrick J. Wong
@ 2026-03-24 20:57 ` Bernd Schubert
0 siblings, 0 replies; 59+ messages in thread
From: Bernd Schubert @ 2026-03-24 20:57 UTC (permalink / raw)
To: Darrick J. Wong
Cc: linux-fsdevel, Miklos Szeredi, Joanne Koong, Bernd Schubert
On 3/24/26 01:06, Darrick J. Wong wrote:
> On Mon, Mar 23, 2026 at 06:45:09PM +0100, Bernd Schubert wrote:
>> From: Bernd Schubert <bschubert@ddn.com>
>>
>> This is to allow fusermount to use the code from mount_fsmount.c.
>> I.e. avoid code dup and add just re-use the new linux api mount
>> functions from that file for fusermount.
>>
>> Signed-off-by: Bernd Schubert <bschubert@ddn.com>
>> ---
>> lib/mount.c | 7 -------
>> lib/mount_common_i.h | 2 --
>> lib/mount_i_linux.h | 3 ++-
>> lib/mount_util.c | 10 ++++++++++
>> lib/mount_util.h | 5 +++++
>> 5 files changed, 17 insertions(+), 10 deletions(-)
>>
>> diff --git a/lib/mount.c b/lib/mount.c
>> index 12df49d9109cf918cc41aa75c5fdf84231d4d5ff..263b05051c236458b830c40181bce7f494803800 100644
>> --- a/lib/mount.c
>> +++ b/lib/mount.c
>> @@ -720,13 +720,6 @@ out:
>> return res;
>> }
>>
>> -const char *fuse_mnt_get_devname(void)
>> -{
>> - const char *devname = getenv(FUSE_KERN_DEVICE_ENV);
>> -
>> - return devname ? devname : "/dev/fuse";
>> -}
>> -
>> char *fuse_mnt_build_source(const struct mount_opts *mo)
>> {
>> const char *devname = fuse_mnt_get_devname();
>> diff --git a/lib/mount_common_i.h b/lib/mount_common_i.h
>> index d27d2aa624ae3806c61a0fe382c2d024080c9bb3..b015e8f98d46980891216e93358aae9f7836d156 100644
>> --- a/lib/mount_common_i.h
>> +++ b/lib/mount_common_i.h
>> @@ -24,8 +24,6 @@ struct mount_opts;
>> struct mount_opts *parse_mount_opts(struct fuse_args *args);
>> void destroy_mount_opts(struct mount_opts *mo);
>> unsigned int get_max_read(struct mount_opts *o);
>> -char *fuse_mnt_build_source(const struct mount_opts *mo);
>> -char *fuse_mnt_build_type(const struct mount_opts *mo);
>>
>>
>> #endif /* FUSE_MOUNT_COMMON_I_H_ */
>> diff --git a/lib/mount_i_linux.h b/lib/mount_i_linux.h
>> index 52eab9e650c055142feec329264f82c2b08be0d5..867105019fa57576682091d1a650302f31e450b3 100644
>> --- a/lib/mount_i_linux.h
>> +++ b/lib/mount_i_linux.h
>> @@ -74,6 +74,7 @@ int fuse_kern_fsmount_mo(const char *mnt, struct mount_opts *mo,
>>
>> int fuse_kern_fsmount_mo(const char *mnt, struct mount_opts *mo,
>> const char *mnt_opts);
>> -
>> +char *fuse_mnt_build_source(const struct mount_opts *mo);
>> +char *fuse_mnt_build_type(const struct mount_opts *mo);
>
> These are linux-only things? I thought they applied to BSD too and
> should go in mount_util.h...
mount_bsd.c has its own definition of "struct mount_opts *mo" and I
don't want to update that code, until we have full BSD tests. So far the
github BSD test only compiles, but doesn't run the actual tests.
>
>> #endif /* FUSE_MOUNT_I_H_ */
>
> ...but I'm now terribly confused, is mount_i_linux.h just the symbols
> needed to build on Linux, or to build anywhere?
I updated the earlier patch that introduced mount_i_linux.h
#ifndef FUSE_MOUNT_I_LINUX_H_
#define FUSE_MOUNT_I_LINUX_H_
>
>> diff --git a/lib/mount_util.c b/lib/mount_util.c
>> index a42a02a0b92f98778abb2c491cdc7a01260f56ee..6a7908cb74b429a1ffd811678cfd39c35093b265 100644
>> --- a/lib/mount_util.c
>> +++ b/lib/mount_util.c
>> @@ -10,6 +10,9 @@
>>
>> #include "fuse_config.h"
>> #include "mount_util.h"
>> +#ifdef __linux__
>> +#include "mount_i_linux.h"
>> +#endif
>>
>> #include <stdio.h>
>> #include <unistd.h>
>> @@ -399,3 +402,10 @@ int fuse_mnt_add_mount_helper(const char *mnt, const char *source,
>> (void)mnt_opts;
>> return 0;
>> }
>> +
>> +const char *fuse_mnt_get_devname(void)
>> +{
>> + const char *devname = getenv(FUSE_KERN_DEVICE_ENV);
>> +
>> + return devname ? devname : "/dev/fuse";
>> +}
>> diff --git a/lib/mount_util.h b/lib/mount_util.h
>> index 4688d00091f001b3ccd36b1ef3b7e799031ed773..00397943a46a58e640d15b0c0531e5bc6e76006b 100644
>> --- a/lib/mount_util.h
>> +++ b/lib/mount_util.h
>> @@ -6,6 +6,9 @@
>> See the file LGPL2.txt.
>> */
>>
>> +#ifndef FUSE_MOUNT_UTIL_H_
>> +#define FUSE_MOUNT_UTIL_H_
>
> This is a positive development, at least :)
>
> --D
>
>> +
>> #include <sys/types.h>
>> #include "mount_common_i.h" // IWYU pragma: keep
>>
>> @@ -22,3 +25,5 @@ int fuse_mnt_parse_fuse_fd(const char *mountpoint);
>> const char *fuse_mnt_get_devname(void);
>> int fuse_mnt_add_mount_helper(const char *mnt, const char *source,
>> const char *type, const char *mnt_opts);
>> +
>> +#endif /* FUSE_MOUNT_UTIL_H_ */
>>
>> --
>> 2.43.0
>>
>>
Thanks,
Bernd
^ permalink raw reply [flat|nested] 59+ messages in thread
* Re: [PATCH 15/19] Split the fusermount do_mount function
2026-03-24 0:14 ` Darrick J. Wong
@ 2026-03-24 21:05 ` Bernd Schubert
2026-03-24 22:53 ` Darrick J. Wong
0 siblings, 1 reply; 59+ messages in thread
From: Bernd Schubert @ 2026-03-24 21:05 UTC (permalink / raw)
To: Darrick J. Wong
Cc: linux-fsdevel, Miklos Szeredi, Joanne Koong, Bernd Schubert
On 3/24/26 01:14, Darrick J. Wong wrote:
> On Mon, Mar 23, 2026 at 06:45:10PM +0100, Bernd Schubert wrote:
>> From: Bernd Schubert <bschubert@ddn.com>
>>
>> We will need new API and old API and need to pass the options to
>> two different functions - factor out the option parsing.
>>
>> Signed-off-by: Bernd Schubert <bschubert@ddn.com>
>> ---
>> util/fusermount.c | 298 +++++++++++++++++++++++++++++++++++++-----------------
>> 1 file changed, 205 insertions(+), 93 deletions(-)
>>
>> diff --git a/util/fusermount.c b/util/fusermount.c
>> index f17b44f51142c682b339d0ce2287f7c00d644454..ecf509bb80d5cd129f6e582f1ec666502c55603a 100644
>> --- a/util/fusermount.c
>> +++ b/util/fusermount.c
>> @@ -917,30 +917,132 @@ static int mount_notrunc(const char *source, const char *target,
>> return mount(source, target, filesystemtype, mountflags, data);
>> }
>>
>> +struct mount_params {
>> + /* Input parameters */
>> + int fd; /* /dev/fuse file descriptor */
>> + mode_t rootmode; /* Root mode from stat */
>> + const char *dev; /* Device path (/dev/fuse) */
>>
>> -static int do_mount(const char *mnt, const char **typep, mode_t rootmode,
>> - int fd, const char *opts, const char *dev, char **sourcep,
>> - char **mnt_optsp)
>> + /* Parsed mount options */
>> + unsigned long flags; /* Mount flags (MS_NOSUID, etc.) */
>> + char *optbuf; /* Kernel mount options buffer */
>> + char *fsname; /* Filesystem name from options */
>> + char *subtype; /* Subtype from options */
>> + int blkdev; /* Block device flag */
>> +
>> + /* Generated mount parameters */
>> + char *source; /* Mount source string */
>> + char *type; /* Filesystem type string */
>> + char *mnt_opts; /* Mount table options */
>> +
>> + /* Pointer for optbuf manipulation */
>> + char *optbuf_end; /* Points to end of optbuf for sprintf */
>> +};
>> +
>> +static void free_mount_params(struct mount_params *mp)
>> +{
>> + free(mp->optbuf);
>> + free(mp->fsname);
>> + free(mp->subtype);
>> + free(mp->source);
>> + free(mp->type);
>> + free(mp->mnt_opts);
>> +}
>> +
>> +/*
>> + * Check if option is deprecated large_read.
>> + *
>> + * Returns true if the option should be skipped (large_read on kernel > 2.4),
>> + * false otherwise (all other options or large_read on old kernels).
>> + */
>> +static bool check_large_read(const char *opt, unsigned int len)
>> +{
>> + struct utsname utsname;
>> + unsigned int kmaj, kmin;
>> + int res;
>> +
>> + if (!opt_eq(opt, len, "large_read"))
>> + return false;
>> +
>> + res = uname(&utsname);
>> + if (res == 0 &&
>> + sscanf(utsname.release, "%u.%u", &kmaj, &kmin) == 2 &&
>> + (kmaj > 2 || (kmaj == 2 && kmin > 4))) {
>> + fprintf(stderr,
>> + "%s: note: 'large_read' mount option is deprecated for %i.%i kernels\n",
>> + progname, kmaj, kmin);
>> + return true;
>
> Ugh, you can drop the uname and all that. Nobody's running 2.4 kernels
> anymore, right?
Added a new commit that removes this. If someone complains I can recvert
that
>
>> + }
>> + return false;
>> +}
>> +
>> +/*
>> + * Check if user has permission to use allow_other or allow_root options.
>> + *
>> + * Returns -1 if permission denied, 0 if allowed or option is not
>> + * allow_other/allow_root.
>> + */
>> +static int check_allow_permission(const char *opt, unsigned int len)
>> +{
>> + if (getuid() != 0 && !user_allow_other &&
>> + (opt_eq(opt, len, "allow_other") || opt_eq(opt, len, "allow_root"))) {
>> + fprintf(stderr, "%s: option %.*s only allowed if 'user_allow_other' is set in %s\n",
>> + progname, len, opt, FUSE_CONF);
>> + return -1;
>> + }
>> + return 0;
>> +}
>> +
>> +/*
>> + * Process generic mount option.
>> + *
>> + * Handles mount flags (ro, rw, suid, etc.), kernel options
>> + * (default_permissions, allow_other, max_read, blksize), or exits on
>> + * unknown options.
>> + */
>> +static int process_generic_option(const char *opt, unsigned int len,
>> + unsigned long *flags, char **dest)
>> +{
>> + int on;
>> + int flag;
>> +
>> + if (find_mount_flag(opt, len, &on, &flag)) {
>> + if (on)
>> + *flags |= flag;
>> + else
>> + *flags &= ~flag;
>> + return 0;
>> + }
>> +
>> + if (opt_eq(opt, len, "default_permissions") ||
>> + opt_eq(opt, len, "allow_other") ||
>> + begins_with(opt, "max_read=") ||
>> + begins_with(opt, "blksize=")) {
>> + memcpy(*dest, opt, len);
>> + *dest += len;
>> + **dest = ',';
>> + (*dest)++;
>> + return 0;
>> + }
>> +
>> + fprintf(stderr, "%s: unknown option '%.*s'\n", progname, len, opt);
>> + exit(1);
>> +}
>> +
>> +static int prepare_mount(const char *opts, struct mount_params *mp)
>> {
>> int res;
>> - int flags = MS_NOSUID | MS_NODEV;
>> - char *optbuf;
>> - char *mnt_opts = NULL;
>> const char *s;
>> char *d;
>> - char *fsname = NULL;
>> - char *subtype = NULL;
>> - char *source = NULL;
>> - char *type = NULL;
>> - int blkdev = 0;
>>
>> - optbuf = (char *) malloc(strlen(opts) + 128);
>> - if (!optbuf) {
>> + mp->flags = MS_NOSUID | MS_NODEV;
>> + mp->optbuf = (char *) malloc(strlen(opts) + 128);
>> + if (!mp->optbuf) {
>> fprintf(stderr, "%s: failed to allocate memory\n", progname);
>> return -1;
>> }
>>
>> - for (s = opts, d = optbuf; *s;) {
>> + for (s = opts, d = mp->optbuf; *s;) {
>> unsigned len;
>> const char *fsname_str = "fsname=";
>> const char *subtype_str = "subtype=";
>> @@ -953,10 +1055,10 @@ static int do_mount(const char *mnt, const char **typep, mode_t rootmode,
>> break;
>> }
>> if (begins_with(s, fsname_str)) {
>> - if (!get_string_opt(s, len, fsname_str, &fsname))
>> + if (!get_string_opt(s, len, fsname_str, &mp->fsname))
>> goto err;
>> } else if (begins_with(s, subtype_str)) {
>> - if (!get_string_opt(s, len, subtype_str, &subtype))
>> + if (!get_string_opt(s, len, subtype_str, &mp->subtype))
>> goto err;
>> } else if (opt_eq(s, len, "blkdev")) {
>> if (getuid() != 0) {
>> @@ -965,7 +1067,7 @@ static int do_mount(const char *mnt, const char **typep, mode_t rootmode,
>> progname);
>> goto err;
>> }
>> - blkdev = 1;
>> + mp->blkdev = 1;
>> } else if (opt_eq(s, len, "auto_unmount")) {
>> auto_unmount = 1;
>> } else if (!opt_eq(s, len, "nonempty") &&
>> @@ -973,122 +1075,132 @@ static int do_mount(const char *mnt, const char **typep, mode_t rootmode,
>> !begins_with(s, "rootmode=") &&
>> !begins_with(s, "user_id=") &&
>> !begins_with(s, "group_id=")) {
>> - int on;
>> - int flag;
>> - int skip_option = 0;
>> - if (opt_eq(s, len, "large_read")) {
>> - struct utsname utsname;
>> - unsigned kmaj, kmin;
>> - res = uname(&utsname);
>> - if (res == 0 &&
>> - sscanf(utsname.release, "%u.%u",
>> - &kmaj, &kmin) == 2 &&
>> - (kmaj > 2 || (kmaj == 2 && kmin > 4))) {
>> - fprintf(stderr, "%s: note: 'large_read' mount option is deprecated for %i.%i kernels\n", progname, kmaj, kmin);
>> - skip_option = 1;
>> - }
>> - }
>> - if (getuid() != 0 && !user_allow_other &&
>> - (opt_eq(s, len, "allow_other") ||
>> - opt_eq(s, len, "allow_root"))) {
>> - fprintf(stderr, "%s: option %.*s only allowed if 'user_allow_other' is set in %s\n", progname, len, s, FUSE_CONF);
>> + bool skip;
>> +
>> + if (check_allow_permission(s, len) == -1)
>> goto err;
>> - }
>> - if (!skip_option) {
>> - if (find_mount_flag(s, len, &on, &flag)) {
>> - if (on)
>> - flags |= flag;
>> - else
>> - flags &= ~flag;
>> - } else if (opt_eq(s, len, "default_permissions") ||
>> - opt_eq(s, len, "allow_other") ||
>> - begins_with(s, "max_read=") ||
>> - begins_with(s, "blksize=")) {
>> - memcpy(d, s, len);
>> - d += len;
>> - *d++ = ',';
>> - } else {
>> - fprintf(stderr, "%s: unknown option '%.*s'\n", progname, len, s);
>> - exit(1);
>> - }
>> - }
>> +
>> + skip = check_large_read(s, len);
>> +
>> + /*
>> + * Skip deprecated large_read to avoid passing it to
>> + * kernel which would reject it as unknown option.
>> + */
>> + if (!skip)
>> + process_generic_option(s, len, &mp->flags, &d);
>> }
>> s += len;
>> if (*s)
>> s++;
>> }
>> *d = '\0';
>> - res = get_mnt_opts(flags, optbuf, &mnt_opts);
>> + res = get_mnt_opts(mp->flags, mp->optbuf, &mp->mnt_opts);
>> if (res == -1)
>> goto err;
>>
>> + mp->optbuf_end = d;
>> +
>> sprintf(d, "fd=%i,rootmode=%o,user_id=%u,group_id=%u",
>> - fd, rootmode, getuid(), getgid());
>> + mp->fd, mp->rootmode, getuid(), getgid());
>>
>> - source = malloc((fsname ? strlen(fsname) : 0) +
>> - (subtype ? strlen(subtype) : 0) + strlen(dev) + 32);
>> + mp->source = malloc((mp->fsname ? strlen(mp->fsname) : 0) +
>> + (mp->subtype ? strlen(mp->subtype) : 0) + strlen(mp->dev) + 32);
>>
>> - type = malloc((subtype ? strlen(subtype) : 0) + 32);
>> - if (!type || !source) {
>> + mp->type = malloc((mp->subtype ? strlen(mp->subtype) : 0) + 32);
>> + if (!mp->type || !mp->source) {
>> fprintf(stderr, "%s: failed to allocate memory\n", progname);
>> goto err;
>> }
>>
>> - if (subtype)
>> - sprintf(type, "%s.%s", blkdev ? "fuseblk" : "fuse", subtype);
>> + if (mp->subtype)
>> + sprintf(mp->type, "%s.%s", mp->blkdev ? "fuseblk" : "fuse", mp->subtype);
>> else
>> - strcpy(type, blkdev ? "fuseblk" : "fuse");
>> + strcpy(mp->type, mp->blkdev ? "fuseblk" : "fuse");
>
> Hrm, maybe the patch adding fuse_mnt_build_source / fuse_mnt_build_type
> should have done something about this code too.
>
> The straight-up conversion looks ok though.
Yeah, I need to do that in a later commit that makes these two functions
more generic.
Thanks,
Bernd
^ permalink raw reply [flat|nested] 59+ messages in thread
* Re: [PATCH 17/19] Make fusermount work bidirectional for sync init
2026-03-24 19:35 ` Darrick J. Wong
@ 2026-03-24 21:24 ` Bernd Schubert
2026-03-24 22:59 ` Darrick J. Wong
0 siblings, 1 reply; 59+ messages in thread
From: Bernd Schubert @ 2026-03-24 21:24 UTC (permalink / raw)
To: Darrick J. Wong
Cc: linux-fsdevel, Miklos Szeredi, Joanne Koong, Bernd Schubert
On 3/24/26 20:35, Darrick J. Wong wrote:
> On Mon, Mar 23, 2026 at 06:45:12PM +0100, Bernd Schubert wrote:
>> From: Bernd Schubert <bschubert@ddn.com>
>>
>> Signed-off-by: Bernd Schubert <bschubert@ddn.com>
>> ---
>> doc/README.fusermount | 359 ++++++++++++++++++++++++++++++++++++++++++++++++++
>> util/fusermount.c | 317 ++++++++++++++++++++++++++++++++++++++++++--
>> util/meson.build | 2 +-
>> 3 files changed, 665 insertions(+), 13 deletions(-)
>>
>> diff --git a/doc/README.fusermount b/doc/README.fusermount
>> new file mode 100644
>> index 0000000000000000000000000000000000000000..54a3bac4f58964a4ed312d6f6bc15606fed1e647
>> --- /dev/null
>> +++ b/doc/README.fusermount
>> @@ -0,0 +1,359 @@
>> +Synchronous FUSE_INIT Protocol
>> +================================
>> +
>> +Overview
>> +--------
>> +
>> +The sync-init feature enables the FUSE library to start worker threads and
>> +perform initialization ioctl calls BEFORE the actual mount() syscall happens.
>> +This is required for the kernel's synchronous FUSE_INIT feature, where the
>> +mount() syscall blocks until the FUSE daemon processes the INIT request.
>> +
>> +Without this feature, there would be a deadlock:
>> +- mount() blocks waiting for INIT response
>> +- Worker threads can't start because mount() hasn't returned
>> +- INIT request can't be processed because worker threads aren't running
>> +
>> +
>> +Protocol Flow
>> +-------------
>> +
>> +Traditional mount flow:
>> + 1. Library calls fusermount3
>
> Heh. I haven't looked much at fusermount until recently. I gather that
> fuservicemount has somewhat similar goals to fusermount3? fusermount3
> seems to be a helper subprocess that libfuse can invoke on behalf of an
> unprivileged fuse server. The helper is responsible for:
>
> 1) opening /dev/fuse
> 2) sending it to the fuse server via the FUSE_COMMFD_ENV fd which is
> supposed to be an AF_UNIX socket
> 3) calling mount()
> 4) waiting for the parent to die
> 5) maybe calling unmount()
>
> and it's really 1, 3, and 5 that need to be privileged, so that's why
> it's a setuid program.
>
>> + 2. fusermount3 opens /dev/fuse
>> + 3. fusermount3 performs mount() syscall
>> + 4. fusermount3 sends fd to library
>> + 5. Library starts worker threads
>> + 6. Worker threads process FUSE requests
>
> Ah, yes. Thanks for adding this description! fuservicemount is I think
> an upside-down version of fusermount -- fuservicemount runs in the
> user's mount namespace, so it
>
> 1) connects to a named AF_UNIX socket to start an instance of the fuse
> server
> 2) opens /dev/fuse and a memfd to pass cli arguments
> 3) passes those to the fuse server
> 4) the fuse server asks fuservicemount to open resources and pass them
> over the socket
> 5) the fuse server passes source/type/mount options to fuservicemount
> 6) fuservicemount mounts the fs and exits
> 7) at some point the user unmounts, so the fuse server exits
>
>> +Sync-init mount flow:
>> + 1. Library calls fusermount3 with --sync-init flag
>> + 2. fusermount3 opens /dev/fuse
>> + 3. fusermount3 sends fd to library
>> + 4. Library receives fd
>> + 5. Library performs FUSE_DEV_IOC_SYNC_INIT ioctl
>> + 6. Library starts worker threads
>> + 7. Library sends "proceed" signal to fusermount3
>> + 8. fusermount3 performs mount() syscall (blocks until INIT completes)
>> + 9. Worker threads process INIT request
>> + 10. mount() syscall completes
>> + 11. fusermount3 exits
>> +
>> +
>> +Implementation Details
>> +----------------------
>> +
>> +Bidirectional Communication:
>> + - Uses the existing unix socket (_FUSE_COMMFD environment variable)
>> + - Simple 1-byte protocol for signaling
>> + - Library signals fusermount3 when ready to proceed with mount
>> +
>> +fusermount3 Changes:
>> + - New --sync-init command-line option
>> + - Split mount operation into two phases:
>> + * mount_fuse_prepare(): Opens device, prepares parameters
>> + * mount_fuse_finish_fsmount(): Performs actual mount() syscall
>> + - wait_for_signal(): Waits for library to signal readiness
>> + - struct mount_context: Preserves state between phases
>> +
>> +Library Changes:
>> + - fuse_session_mount_new_api(): Uses new protocol when available
>> + - Sends "proceed" signal after worker thread is ready
>> + - Handles both old and new mount protocols for compatibility
>> +
>> +
>> +Backward Compatibility
>> +----------------------
>> +
>> +The implementation maintains full backward compatibility:
>> + - Old library + new fusermount3: Works (uses traditional flow)
>> + - New library + old fusermount3: Falls back to traditional flow
>> + - New library + new fusermount3: Uses sync-init flow when appropriate
>> +
>> +
>> +Error Handling
>> +--------------
>> +
>> +If any step fails during the sync-init flow:
>> + - fusermount3 closes the fd and exits with error
>> + - Library detects failure and cleans up
>> + - No mount is left in inconsistent state
>> +
>> +Connection closure:
>> + - If library closes socket before signaling, fusermount3 detects and exits
>> + - If fusermount3 crashes, library detects closed socket
>> +
>> +
>> +Security Considerations
>> +-----------------------
>> +
>> +The sync-init protocol does not introduce new security concerns:
>> + - Uses the same privilege separation as traditional mount
>> + - Socket communication is already established and trusted
>> + - No new privileged operations are added
>> + - File descriptor passing uses existing SCM_RIGHTS mechanism
>> +
>> +
>> +Performance Impact
>> +------------------
>> +
>> +Minimal performance impact:
>> + - One additional recv() call in fusermount3
>> + - One additional send() call in library
>> + - Total overhead: ~2 context switches
>> + - Only affects mount time, not runtime performance
>> +
>> +
>> +Future Enhancements
>> +-------------------
>> +
>> +Potential improvements:
>> + - Extended protocol for more complex initialization sequences
>> + - Support for multiple worker threads coordination
>> + - Enhanced error reporting through the socket
>> + - Timeout mechanisms for detecting hung initialization
>> +
>> +
>> +ASCII Workflow Diagrams
>> +========================
>> +
>> +1. Traditional Mount Flow (without --sync-init, async INIT)
>> +------------------------------------------------------------
>> +
>> +Library fusermount3 Kernel
>> + | | |
>> + |--- spawn fusermount3 ---->| |
>> + | | |
>> + | [open /dev/fuse] |
>> + | |------- open -------->|
>> + | |<------ fd ---------- |
>> + | | |
>> + | [mount() syscall] |
>> + | |------ mount -------->|
>> + | |<----- success ------ | [mount returns immediately]
>> + | | | [INIT queued in kernel]
>> + | [send_fd(fd)] |
>> + |<------- fd --------------| |
>> + | | |
>> + | [fusermount3 exits] |
>> + | |
>> + | [start worker thread] |
>> + | [worker reads /dev/fuse] |
>> + |---------------------------------------- read -->|
>> + |<--------------------------------------- INIT ---| [dequeued from kernel]
>> + | |
>> + | OK: INIT was queued, worker reads it later |
>> + | Works fine for async INIT |
>
> Hmm, looking at this, perhaps it /is/ possible for fuservicemount to
> employ synchronous init. The fuse server would start that background
> init-only request handler thread before telling fuservicemount to call
> mount(). That blocks while the kernel sends FUSE_INIT to the fuse
> server, it processes everything up to the init request, and returns.
>
>> +
>> +
>> +1b. Problem: Synchronous INIT without --sync-init
>> +--------------------------------------------------
>> +
>> +Library fusermount3 Kernel
>> + | | |
>> + |--- spawn fusermount3 ---->| |
>> + | | |
>> + | [open /dev/fuse] |
>> + | |------- open -------->|
>> + | |<------ fd ---------- |
>> + | | |
>> + | [mount() syscall] |
>> + | |------ mount -------->|
>> + | | | [mount BLOCKS waiting for INIT]
>> + | | (BLOCKED) | [needs worker to process INIT]
>> + | | |
>> + | [waiting for fd...] | |
>> + | | |
>> + | | |
>> + | DEADLOCK: mount() waits for INIT response |
>> + | but worker thread not started yet |
>> + | because we're waiting for fd |
>> +
>> +
>> +2. Sync-Init Mount Flow (with --sync-init)
>> +-------------------------------------------
>> +
>> +Library fusermount3 Kernel
>> + | | |
>> + |--- spawn fusermount3 ---->| |
>> + | with --sync-init | |
>> + | | |
>> + | [open /dev/fuse] |
>> + | |------- open -------->|
>> + | |<------ fd ---------- |
>> + | | |
>> + | [send_fd(fd)] |
>> + |<------- fd --------------| |
>> + | | |
>> + | [wait_for_signal()] |
>> + | | (BLOCKED) |
>> + | | |
>> + | [ioctl SYNC_INIT] | |
>> + |---------------------------------------- ioctl -->|
>> + | |
>> + | [start worker thread] |
>> + | [worker ready] |
>> + | | |
>> + |--- "proceed" signal ----->| |
>> + | [signal received] |
>> + | | |
>> + | [mount() syscall] |
>> + | |------ mount -------->|
>> + | | | [mount blocks]
>> + | | | [sends INIT]
>> + |<------------------------------------------------ |
>> + | | |
>> + | [worker processes INIT] | |
>> + |------------------------------------------------->|
>> + | | | [mount unblocks]
>> + | |<----- success ------ |
>> + | | |
>> + | [fusermount3 exits] |
>> + | |
>> + | SUCCESS: Worker ready before mount() |
>> + | INIT processed synchronously |
>> +
>> +
>> +3. Error Scenario: Library Crashes Before Signaling
>> +----------------------------------------------------
>> +
>> +Library fusermount3 Kernel
>> + | | |
>> + |--- spawn fusermount3 ---->| |
>> + | with --sync-init | |
>> + | | |
>> + | [open /dev/fuse] |
>> + | |------- open -------->|
>> + | |<------ fd ---------- |
>> + | | |
>> + | [send_fd(fd)] |
>> + |<------- fd --------------| |
>> + | | |
>> + | [wait_for_signal()] |
>> + | | (BLOCKED) |
>> + | | |
>> + X [library crashes] | |
>> + | | |
>> + | [recv() returns 0] |
>> + | [socket closed] |
>> + | | |
>> + | [cleanup and exit] |
>> + | X |
>> + | |
>> + | RESULT: Clean failure, no mount performed |
>> +
>> +
>> +4. Detailed Function Call Flow
>> +-------------------------------
>> +
>> +Library (lib/fuse_lowlevel.c):
>> +fuse_session_mount_new_api()
>> + |
>> + +-- fuse_kern_mount_prepare() [lib/mount.c]
>> + | |
>> + | +-- fuse_mount_fusermount() [lib/mount_util.c]
>> + | |
>> + | +-- socketpair() [create comm socket]
>> + | |
>> + | +-- fork()
>> + | |
>> + | +-- [child] execl("fusermount3", "--sync-init", ...)
>> + | |
>> + | +-- [parent] receive_fd() <--- BLOCKS until fd arrives
>> + | |
>> + | +-- recvmsg(SCM_RIGHTS)
>> + | |
>> + | +-- return fd
>> + |
>> + +-- session_start_sync_init() [lib/fuse_lowlevel.c]
>> + | |
>> + | +-- ioctl(fd, FUSE_DEV_IOC_SYNC_INIT)
>> + | |
>> + | +-- pthread_create(worker_thread)
>> + | |
>> + | +-- return
>> + |
>> + +-- fuse_fusermount_proceed_mnt(socket) [lib/mount.c] <--- NEW: Bidirectional handshake
>> + |
>> + +-- send(socket, "proceed", 1) <--- Signal fusermount3 to proceed
>> + |
>> + +-- recv(socket, &status, 1) <--- BLOCKS until mount result arrives
>> + | |
>> + | +-- [fusermount3 performs mount and sends status byte]
>> + |
>> + +-- if (status != 0) return -1 <--- Mount failed
>> + |
>> + +-- return 0 <--- Mount succeeded
>> +
>> +
>> +Utility (util/fusermount.c):
>> +fusermount3 main() with --sync-init
>> + |
>> + +-- mount_fuse_sync_init() [util/fusermount.c]
>> + |
>> + +-- mount_fuse_prepare() [util/fusermount.c]
>> + | |
>> + | +-- open("/dev/fuse")
>> + | |
>> + | +-- check_perm() [util/fusermount.c]
>> + | |
>> + | +-- return fd
>> + |
>> + +-- send_fd(socket, fd) [util/fusermount.c]
>> + | |
>> + | +-- sendmsg(SCM_RIGHTS)
>> + |
>> + +-- wait_for_signal(socket) [util/fusermount.c] <--- BLOCKS until library signals
>> + | |
>> + | +-- recv(socket, buf, 1)
>> + | |
>> + | +-- return 0
>> + |
>> + +-- mount_fuse_finish_fsmount() [util/fusermount.c]
>> + | |
>> + | +-- fuse_kern_fsmount() [lib/mount_fsmount.c]
>> + | | |
>> + | | +-- fsopen("fuse", FSOPEN_CLOEXEC)
>> + | | | |
>> + | | | +-- [kernel creates filesystem context]
>> + | | |
>> + | | +-- fsconfig(fsfd, SET_STRING, "source", ...)
>> + | | +-- fsconfig(fsfd, SET_STRING, "fd", fd_value, ...)
>> + | | +-- fsconfig(fsfd, ...) [apply mount options]
>> + | | +-- fsconfig(fsfd, CMD_CREATE, ...)
>> + | | |
>> + | | +-- fsmount(fsfd, FSMOUNT_CLOEXEC, mount_attrs)
>> + | | | |
>> + | | | +-- [kernel sends FUSE_INIT here]
>> + | | | |
>> + | | | +-- [worker thread processes INIT]
>> + | | | |
>> + | | | +-- [fsmount returns mntfd]
>> + | | |
>> + | | +-- move_mount(mntfd, "", AT_FDCWD, target, ...)
>> + | | | |
>> + | | | +-- [attach mount to target directory]
>> + | | | |
>> + | | | +-- [no blocking - INIT already processed]
>> + | | |
>> + | | +-- add_mount() [lib/mount_fsmount.c - update /etc/mtab]
>> + | | |
>> + | | +-- return 0 on success, -1 on failure
>> + | |
>> + | +-- if mount failed: return -1
>> + | +-- if mount succeeded: continue
>> + |
>> + +-- send_status_byte(socket) [util/fusermount.c] <--- NEW: Send result to library
>> + | |
>> + | +-- status = (mount_result == 0) ? 0 : 1
>> + | +-- send(socket, &status, 1)
>> + | |
>> + | +-- return
>> + |
>> + +-- return 0
>> +
>> +
>> +Note: The new mount API (fsopen/fsconfig/fsmount/move_mount) is REQUIRED
>> + for sync-init because fsmount() triggers FUSE_INIT before the mount
>> + is attached. This allows the worker thread to process INIT before
>> + move_mount() completes, preventing deadlock.
>
> ...and so we don't expose the directory tree to the mountns until we
> know that FUSE_INIT didn't crash the server.
Added that as well.
>
>> diff --git a/util/fusermount.c b/util/fusermount.c
>> index 80b42a594e89cdc2f43824f5e274892522fd8cce..808b4afd89ceb49273c944d43bffe5033e27549b 100644
>> --- a/util/fusermount.c
>> +++ b/util/fusermount.c
>> @@ -957,6 +957,7 @@ static void free_mount_params(struct mount_params *mp)
>> free(mp->source);
>> free(mp->type);
>> free(mp->mnt_opts);
>> + memset(mp, 0, sizeof(*mp));
>> }
>>
>> /*
>> @@ -1378,6 +1379,179 @@ static int open_fuse_device(const char *dev)
>> return fd;
>> }
>>
>> +#ifdef HAVE_NEW_MOUNT_API
>> +/* Forward declaration from lib/mount_fsmount.c */
>> +int fuse_kern_fsmount(const char *mnt, unsigned long flags, int blkdev,
>> + const char *fsname, const char *subtype,
>> + const char *source_dev, const char *kernel_opts,
>> + const char *mnt_opts);
>> +#endif
>
> Shouldn't this be included from a header file somewhere?
Fixed, included from mount_i_linux.h
>
>> +
>> +/*
>> + * Context for split mount operation (sync-init mode)
>> + */
>> +struct mount_context {
>> + int fd;
>> + const char *dev;
>> + struct stat stbuf;
>> + char *source;
>> + char *mnt_opts;
>> + char *x_opts;
>> + const char *type;
>> +};
>> +
>> +/*
>> + * Phase 1: Open device and prepare for mount (sync-init mode)
>> + * Returns fd on success, -1 on failure
>> + */
>> +static int mount_fuse_prepare(const char *mnt, const char *opts,
>> + struct mount_context *ctx)
>> +{
>> + int res;
>> + int mountpoint_fd = -1;
>> + char *do_mount_opts = NULL;
>> + const char *real_mnt = mnt;
>> +
>> + memset(ctx, 0, sizeof(*ctx));
>> + ctx->dev = getenv(FUSE_KERN_DEVICE_ENV) ?: FUSE_DEV;
>> +
>> + ctx->fd = open_fuse_device(ctx->dev);
>> + if (ctx->fd == -1)
>> + return -1;
>> +
>> + drop_privs();
>> + read_conf();
>> +
>> + if (getuid() != 0 && mount_max != -1) {
>> + int mount_count = count_fuse_fs();
>> +
>> + if (mount_count >= mount_max) {
>> + fprintf(stderr,
>> + "%s: too many FUSE filesystems mounted; mount_max=N can be set in %s\n",
>> + progname, FUSE_CONF);
>> + goto fail_close_fd;
>> + }
>> + }
>
> /me notes that he's refactored this configuration file related function
> into fuser_conf.c though that's in the fuse-services v4 that I'll send
> you soon.
>
>> +
>> + res = extract_x_options(opts, &do_mount_opts, &ctx->x_opts);
>> + if (res)
>> + goto fail_close_fd;
>> +
>> + res = check_perm(&real_mnt, &ctx->stbuf, &mountpoint_fd);
>> + restore_privs();
>> +
>> + if (mountpoint_fd != -1)
>> + close(mountpoint_fd);
>> +
>> + if (res == -1)
>> + goto fail_close_fd;
>> +
>> + free(do_mount_opts);
>> + return ctx->fd;
>> +
>> +fail_close_fd:
>> + close(ctx->fd);
>> + free(do_mount_opts);
>> + free(ctx->x_opts);
>> + ctx->fd = -1;
>> + return -1;
>> +}
>> +
>> +#ifdef HAVE_NEW_MOUNT_API
>> +/*
>> + * Phase 2: Perform the actual mount using new mount API (sync-init mode)
>> + * Returns 0 on success, -1 on failure
>> + */
>> +static int mount_fuse_finish_fsmount(const char *mnt, const char *opts,
>> + struct mount_context *ctx,
>> + const char **type)
>> +{
>> + int res;
>> + char *do_mount_opts = NULL;
>> + char *x_prefixed_opts = NULL;
>> + struct mount_params mp = { .fd = ctx->fd };
>> + char *final_mnt_opts = NULL;
>> +
>> + /* Extract x-options */
>> + res = extract_x_options(opts, &do_mount_opts, &x_prefixed_opts);
>> + if (res)
>> + goto fail;
>> +
>> + /* Prepare mount parameters */
>> + mp.rootmode = ctx->stbuf.st_mode & S_IFMT;
>> + mp.dev = ctx->dev;
>
> I think those could be set in the mp variable definition?
Absolutely.
>
>> +
>> + res = prepare_mount(do_mount_opts, &mp);
>> + if (res == -1)
>> + goto fail;
>> +
>> + /* Merge x-options if running as root */
>> + final_mnt_opts = mp.mnt_opts;
>> + if (geteuid() == 0 && ctx->x_opts && strlen(ctx->x_opts) > 0) {
>> + size_t mnt_opts_len = strlen(mp.mnt_opts);
>> + size_t x_mnt_opts_len = mnt_opts_len + strlen(ctx->x_opts) + 2;
>> + char *x_mnt_opts = calloc(1, x_mnt_opts_len);
>> +
>> + if (!x_mnt_opts)
>> + goto fail_free_params;
>> +
>> + if (mnt_opts_len) {
>> + strcpy(x_mnt_opts, mp.mnt_opts);
>> + strncat(x_mnt_opts, ",", 2);
>> + }
>> + strncat(x_mnt_opts, ctx->x_opts,
>> + x_mnt_opts_len - mnt_opts_len - 2);
>> +
>> + final_mnt_opts = x_mnt_opts;
>> + }
>
> Curious, I thought the x- options were edited out by /sbin/mount so fuse
> would never see them? Does the x- option handling in fusermount.c exist
> to handle the case where someone passes them directly to the fuse
> server, aka
>
> $ sshfs <whatever> /mnt -o x-systemd-hahaha=1
>
> and now you need to ensure that x-systemd-hahaha doesn't get sent to the
> kernel but does get seen by the fuse server?
https://github.com/libfuse/libfuse/issues/651
So specially added as mount option to to go into mtab/utab and to
suppress some 3rd party (gnome) actions.
>
>> +
>> + /* Use new mount API */
>> + res = fuse_kern_fsmount(mnt, mp.flags, mp.blkdev,
>> + mp.fsname, mp.subtype, ctx->dev,
>> + mp.optbuf, final_mnt_opts);
>> + if (res == -1)
>> + goto fail_free_merged;
>> +
>> + /* Change to root directory */
>> + res = chdir("/");
>> + if (res == -1) {
>> + fprintf(stderr, "%s: failed to chdir to '/'\n", progname);
>> + goto fail_free_merged;
>> + }
>> +
>> + /* Store results in context */
>> + ctx->source = mp.source;
>> + ctx->type = mp.type;
>> + ctx->mnt_opts = final_mnt_opts;
>> + *type = mp.type;
>> +
>> + res = 0;
>> +
>> + /* Only free what is not assigned to ctx */
>> + free(mp.fsname);
>> + free(mp.subtype);
>> + free(mp.optbuf);
>> + if (final_mnt_opts != mp.mnt_opts)
>> + free(mp.mnt_opts);
>> +
>> +out:
>> + free(do_mount_opts);
>> + free(x_prefixed_opts);
>> +
>> + return res;
>> +
>> +fail_free_merged:
>> + if (final_mnt_opts != mp.mnt_opts)
>> + free(final_mnt_opts);
>> +fail_free_params:
>> + free_mount_params(&mp);
>> +fail:
>> + res = -1;
>> + goto out;
>> +}
>> +#endif /* HAVE_NEW_MOUNT_API */
>> +
>> +
>> static int mount_fuse(const char *mnt, const char *opts, const char **type)
>> {
>> int res;
>> @@ -1473,6 +1647,75 @@ fail_close_fd:
>> goto out_free;
>> }
>>
>> +/* Forward declarations for helper functions */
>> +static int send_fd(int sock_fd, int fd);
>> +static int wait_for_signal(int sock_fd);
>> +
>> +#ifdef HAVE_NEW_MOUNT_API
>> +/*
>> + * Perform sync-init mount using new mount API
>> + * Returns 0 on success, -1 on failure
>> + */
>> +static int mount_fuse_sync_init(const char *mnt, const char *opts,
>> + int cfd, const char **type)
>> +{
>> + struct mount_context ctx;
>> + int fd, res;
>> + int32_t status, send_res;
>> +
>> + /* Phase 1: Open device and prepare */
>> + fd = mount_fuse_prepare(mnt, opts, &ctx);
>> + if (fd == -1)
>> + return -1;
>> +
>> + /* Send fd to caller so it can start worker thread */
>> + res = send_fd(cfd, fd);
>> + if (res != 0) {
>> + close(fd);
>> + free(ctx.x_opts);
>> + return -1;
>> + }
>> +
>> + /* Wait for caller to signal that worker thread is ready */
>> + res = wait_for_signal(cfd);
>> + if (res != 0) {
>> + close(fd);
>> + free(ctx.x_opts);
>> + return -1;
>> + }
>> +
>> + /* Phase 2: Perform the actual mount using new API */
>> + res = mount_fuse_finish_fsmount(mnt, opts, &ctx, type);
>> +
>> + /* Send mount result back to caller (4-byte error code) */
>> + status = (res == 0) ? 0 : -(int32_t)errno;
>> + do {
>> + send_res = send(cfd, &status, sizeof(status), 0);
>> + } while (send_res == -1 && errno == EINTR);
>> + if (send_res != sizeof(status)) {
>> + fprintf(stderr, "%s: failed to send mount status: %s\n",
>> + progname, strerror(errno));
>> + }
>> +
>> + if (res == -1) {
>> + close(fd);
>> + free(ctx.source);
>> + free(ctx.mnt_opts);
>> + free(ctx.x_opts);
>> + return -1;
>> + }
>> +
>> + close(fd);
>> +
>> + /* Cleanup */
>> + free(ctx.source);
>> + free(ctx.mnt_opts);
>> + free(ctx.x_opts);
>> +
>> + return 0;
>
> Err... these cleanups are identical, so I think you can get rid of the
> "if (res == -1)" chunk and just return res at the end.
Oh yeah, fixed.
Thanks,
Bernd
^ permalink raw reply [flat|nested] 59+ messages in thread
* Re: [PATCH 19/19] Add support for sync-init of unprivileged daemons
2026-03-24 20:21 ` Darrick J. Wong
@ 2026-03-24 21:53 ` Bernd Schubert
2026-03-24 23:13 ` Darrick J. Wong
0 siblings, 1 reply; 59+ messages in thread
From: Bernd Schubert @ 2026-03-24 21:53 UTC (permalink / raw)
To: Darrick J. Wong
Cc: linux-fsdevel, Miklos Szeredi, Joanne Koong, Bernd Schubert
On 3/24/26 21:21, Darrick J. Wong wrote:
> On Mon, Mar 23, 2026 at 06:45:14PM +0100, Bernd Schubert wrote:
>> From: Bernd Schubert <bschubert@ddn.com>
>>
>> This makes use of the bidirectional fusermount. Added is
>> doc/README.mount, which explains the new bidirectional
>> communication with fusermount.
>>
>> Signed-off-by: Bernd Schubert <bschubert@ddn.com>
>
> All right, last patch before I go have some lunch and circle back to
> your recent replies :)
>
>> ---
>> doc/README.mount | 86 ++++++++++++++++++++++++
>> doc/README.sync-init | 184 +++++++++++++++++++++++++++++++++++++++++++++++++++
>
> These new readmes feel like they ought to go at the beginning (or at
> least a separate patch) to argue for why synchronous init is needed
> in libfuse? I do appreciate the flow diagrams though.
These new READMEs are kind of used by myself to understand and remember
the flow graph and reasoning. Given the frequency I jump between
projects, I prefer to have some files that help me to remember.
I had asked for AI help to create these flow graphs, simple to give the
right commands when one is in the middle of the code...
I can move these files into a separate patch if you prefer.
>
>> lib/fuse_lowlevel.c | 115 ++++++++++++++++++++++++++------
>> lib/mount.c | 126 ++++++++++++++++++++++++++++++++++-
>> lib/mount_i_linux.h | 7 ++
>> util/fusermount.c | 2 -
>> 6 files changed, 494 insertions(+), 26 deletions(-)
>>
>> diff --git a/doc/README.mount b/doc/README.mount
>> new file mode 100644
>> index 0000000000000000000000000000000000000000..526382ad8a5f6b405a7cb1927b79bacd6c2c2c5c
>> --- /dev/null
>> +++ b/doc/README.mount
>> @@ -0,0 +1,86 @@
>> +FUSE Mount API Flowcharts
>> +=========================
>> +
>> +Old Mount API
>> +-------------
>> +
>> +fuse_kern_mount()
>> + |
>> + +-- fuse_mount_sys()
>> + | +-- Try direct mount → mount() syscall
>> + | +-- On EPERM: fuse_mount_fusermount()
>> + | +-- socketpair()
>> + | +-- spawn fusermount3 (no --sync-init)
>> + | +-- fusermount3: open /dev/fuse, mount(), send fd
>> + | +-- receive_fd() → return fd
>> + |
>> + +-- Worker threads started AFTER mount
>> + └─> FUSE_INIT asynchronous (queued in kernel)
>> +
>> +
>> +New Mount API - Privileged Mount
>> +---------------------------------
>> +
>> +fuse_session_mount_new_api()
>> + |
>> + +-- fuse_kern_mount_prepare() → open /dev/fuse → fd
>> + |
>> + +-- session_start_sync_init(se, fd)
>> + | +-- ioctl(fd, FUSE_DEV_IOC_SYNC_INIT)
>> + | +-- pthread_create(worker) → ready to process FUSE_INIT
>> + |
>> + +-- fuse_kern_fsmount_mo()
>> + | +-- fsopen/fsconfig/fsmount (BLOCKS until FUSE_INIT completes)
>> + | +-- Worker processes FUSE_INIT during fsmount()
>> + | +-- move_mount()
>> + |
>> + +-- session_wait_sync_init_completion(se) → pthread_join
>> + └─> return fd
>> +
>> +
>> +New Mount API - EPERM Fallback (fusermount3 with sync-init)
>> +------------------------------------------------------------
>> +
>> +fuse_session_mount_new_api()
>> + |
>> + +-- fuse_kern_mount_prepare() → open /dev/fuse → fd1
>> + |
>> + +-- session_start_sync_init(se, fd1)
>> + | +-- ioctl(fd1, FUSE_DEV_IOC_SYNC_INIT)
>> + | +-- pthread_create(worker) → ready with fd1
>> + |
>> + +-- fuse_kern_fsmount_mo() → EPERM
>> + |
>> + +-- *** FALLBACK TO FUSERMOUNT3 WITH SYNC-INIT ***
>> + |
>> + +-- session_wait_sync_init_completion(se)
>> + | +-- pthread_cancel/join → terminate worker with wrong fd1
>> + |
>> + +-- close(fd1)
>> + |
>> + +-- fuse_mount_fusermount_sync_init() [NEW]
>> + | +-- socketpair()
>> + | +-- spawn fusermount3 --sync-init
>> + | +-- fusermount3: open /dev/fuse → fd2, send fd2
>> + | +-- receive_fd() → fd2
>> + | +-- fusermount3 waits for signal
>> + | └─> return fd2, sock
>> + |
>> + +-- session_start_sync_init(se, fd2)
>> + | +-- ioctl(fd2, FUSE_DEV_IOC_SYNC_INIT)
>> + | +-- pthread_create(worker) → ready with fd2
>> + |
>> + +-- send_proceed_signal(sock) [NEW]
>> + | +-- send(sock, "\0", 1) → signal fusermount3
>> + |
>> + +-- fusermount3: mount() (BLOCKS)
>> + | +-- Kernel sends FUSE_INIT to fd2
>> + | +-- Worker processes FUSE_INIT
>> + | +-- mount() returns
>> + |
>> + +-- close(sock)
>> + |
>> + +-- session_wait_sync_init_completion(se) → pthread_join
>> + |
>> + └─> return fd2
>> +
>> diff --git a/doc/README.sync-init b/doc/README.sync-init
>> new file mode 100644
>> index 0000000000000000000000000000000000000000..44e47a2eef2c45026abaa19562537eef37f256b9
>> --- /dev/null
>> +++ b/doc/README.sync-init
>> @@ -0,0 +1,184 @@
>> +FUSE Synchronous vs Asynchronous FUSE_INIT
>> +============================================
>> +
>> +This document explains the difference between asynchronous and synchronous
>> +FUSE_INIT processing, and when each mode is used.
>> +
>> +
>> +Overview
>> +--------
>> +
>> +FUSE_INIT is the initial handshake between the kernel FUSE module and the
>> +userspace filesystem daemon. During this handshake, the kernel and daemon
>> +negotiate capabilities, protocol version, and various feature flags.
>> +
>> +Asynchronous FUSE_INIT (Traditional Behavior)
>> +----------------------------------------------
>> +
>> +In the traditional asynchronous mode:
>> +
>> +1. mount() syscall completes and returns to caller
>> +2. Filesystem appears mounted to the system
>> +3. FUSE daemon starts worker threads
>> +4. Worker threads process FUSE_INIT request
>> +5. Filesystem becomes fully operational
>> +
>> +Timeline:
>> + mount() -----> returns
>> + |
>> + v
>> + FUSE_INIT sent
>> + |
>> + v
>> + daemon processes FUSE_INIT
>> + |
>> + v
>> + filesystem ready
>> +
>> +Limitations:
>> +
>> +1. **No early requests**: The kernel cannot send requests (like getxattr)
>> + during the mount() syscall. This breaks SELinux, which needs to query
>> + extended attributes on the root inode immediately upon mounting.
>> +
>> +2. **Daemonization timing**: With the old fuse_daemonize() API, the daemon
>> + must call it AFTER mount, because there's no way to report mount failures
>> + to the parent process if daemonization happens first.
>> +
>> +3. **No custom root inode**: The root inode ID is hardcoded to FUSE_ROOT_ID (1)
>> + because FUSE_INIT hasn't been processed yet when the mount completes.
>> +
>> +4. **Thread startup after mount**: io_uring threads and other worker threads
>> + can only start after mount() returns, not before.
>
> Especially this part which explains why we care about sync init :)
>
>> +
>> +Synchronous FUSE_INIT (New Behavior)
>> +-------------------------------------
>> +
>> +Kernel support: Linux kernel commit dfb84c330794 (v6.18+)
>> +libfuse support: libfuse 3.19+
>> +
>> +In synchronous mode:
>> +
>> +1. FUSE daemon opens /dev/fuse
>> +2. Daemon calls ioctl(fd, FUSE_DEV_IOC_SYNC_INIT)
>> +3. Daemon starts worker thread
>> +4. Daemon calls mount() syscall
>> +5. Kernel sends FUSE_INIT during mount() - mount() blocks
>> +6. Worker thread processes FUSE_INIT while mount() is blocked
>> +7. Worker thread may process additional requests (getxattr, etc.)
>> +8. mount() syscall completes and returns
>> +9. Filesystem is fully operational
>> +
>> +Timeline:
>> + open /dev/fuse
>> + |
>> + v
>> + ioctl(FUSE_DEV_IOC_SYNC_INIT)
>> + |
>> + v
>> + start worker thread
>> + |
>> + v
>> + mount() -----> blocks
>> + | |
>> + | v
>> + | FUSE_INIT sent
>> + | |
>> + | v
>> + | worker processes FUSE_INIT
>> + | |
>> + | v
>> + | (possible getxattr, etc.)
>> + | |
>> + +-------> returns
>> + |
>> + v
>> + filesystem ready
>> +
>> +Advantages:
>> +
>> +1. **SELinux support**: The kernel can send getxattr requests during mount()
>> + to query security labels on the root inode.
>> +
>> +2. **Early daemonization**: The daemon can fork BEFORE mount using the new
>> + fuse_daemonize_start()/signal() API, and report mount failures to the
>> + parent process.
>> +
>> +3. **Custom root inode**: The daemon can specify a custom root inode ID
>> + during FUSE_INIT, before mount() completes.
>> +
>> +4. **Thread startup before mount**: io_uring threads and worker threads
>> + start before mount(), ensuring they're ready to handle requests.
>> +
>> +5. **Better error reporting**: Mount failures and initialization errors
>> + can be properly reported to the parent process when using the new
>> + daemonization API.
>> +
>> +
>> +When Synchronous FUSE_INIT is Used
>> +-----------------------------------
>> +
>> +libfuse automatically enables synchronous FUSE_INIT when:
>> +
>> +1. The application calls fuse_session_want_sync_init(), OR
>> +2. The new daemonization API is used (fuse_daemonize_start() was called)
>> +
>> +Synchronous FUSE_INIT requires:
>> +- Kernel support (commit dfb84c330794 or later)
>> +- Worker thread started before mount()
>> +- ioctl(FUSE_DEV_IOC_SYNC_INIT) succeeds
>> +
>> +If the kernel doesn't support synchronous FUSE_INIT, libfuse automatically
>> +falls back to asynchronous mode.
>> +
>> +
>> +Implementation Details
>> +----------------------
>> +
>> +The synchronous FUSE_INIT implementation uses a worker thread:
>> +
>> +- **session_sync_init_worker()**: Thread function that polls /dev/fuse
>> + and processes FUSE_INIT and any subsequent requests until mount completes.
>> +
>> +- **session_start_sync_init()**: Creates the worker thread before mount().
>> + Calls ioctl(FUSE_DEV_IOC_SYNC_INIT) to enable kernel support.
>> +
>> +- **session_wait_sync_init_completion()**: Waits for the worker thread
>> + to complete after mount() returns. Checks for errors.
>> +
>> +The worker thread processes requests in a loop until se->terminate_mount_worker
>> +is set, which happens after mount() completes successfully.
>> +
>> +
>> +Compatibility
>> +-------------
>> +
>> +Synchronous FUSE_INIT is fully backward compatible:
>> +
>> +- Old kernels: ioctl returns ENOTTY, libfuse falls back to async mode
>> +- Old applications: Continue to work with async FUSE_INIT
>> +- New applications on old kernels: Graceful fallback to async mode
>> +- New applications on new kernels: Automatic sync mode when appropriate
>> +
>> +
>> +Example: Enabling Synchronous FUSE_INIT
>> +----------------------------------------
>> +
>> +Explicit request:
>> + struct fuse_session *se = fuse_session_new(...);
>> + fuse_session_want_sync_init(se);
>> + fuse_session_mount(se, mountpoint);
>> +
>> +Automatic (with new daemonization API):
>> + fuse_daemonize_start(0); // Triggers sync init automatically
>> + fuse_session_mount(se, mountpoint);
>> +
>> +
>> +See Also
>> +--------
>> +
>> +- doc/README.daemonize - New daemonization API documentation
>> +- doc/README.fusermount - Synchronous FUSE_INIT protocol with fusermount3
>> +- doc/README.mount - Mount implementation details
>> +
>> diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c
>> index a7293a3898c37c3877eadf965d310ae2aa5cc2d1..da966217ed841744a20bee60de8ae615d1015b47 100644
>> --- a/lib/fuse_lowlevel.c
>> +++ b/lib/fuse_lowlevel.c
>> @@ -41,6 +41,7 @@
>> #include <assert.h>
>> #include <sys/file.h>
>> #include <sys/ioctl.h>
>> +#include <sys/wait.h>
>> #include <stdalign.h>
>> #include <poll.h>
>>
>> @@ -4551,6 +4552,8 @@ static int session_wait_sync_init_completion(struct fuse_session *se)
>> se->init_wakeup_fd = -1;
>> }
>>
>> + se->init_thread = 0;
>> +
>> if (se->init_error != 0) {
>> fuse_log(FUSE_LOG_ERR, "fuse: init worker failed: %d\n", se->init_error);
>> return -1;
>> @@ -4564,56 +4567,125 @@ static int session_wait_sync_init_completion(struct fuse_session *se)
>> return 0;
>> }
>>
>> -/* Only linux supports sync FUSE_INIT so far */
>> +/*
>> + * Mount using the new Linux mount API (fsopen/fsconfig/fsmount/move_mount)
>> + * Sync-init is only supported with the new API, as the mount might hang
>> + * in case of daemon crash during FUSE_INIT. That also means once the sync init
>> + * ioctl succeed fallback is not allowed anymore.
>> + * Returns: fd on success, -1 on failure
>> + */
>> static int fuse_session_mount_new_api(struct fuse_session *se,
>> - const char *mountpoint)
>> + const char *mountpoint, bool *fall_back)
>> {
>> int fd = -1;
>> + int sock_fd = -1;
>> + pid_t fusermount_pid = -1;
>> int res, err;
>> char *mnt_opts = NULL;
>> char *mnt_opts_with_fd = NULL;
>> char fd_opt[32];
>>
>> res = fuse_kern_mount_get_base_mnt_opts(se->mo, &mnt_opts);
>> + err = -EIO;
>> if (res == -1) {
>> fuse_log(FUSE_LOG_ERR, "fuse: failed to get base mount options\n");
>> - err = -EIO;
>
> Odd churn in this function...
>
>> goto err;
>> }
>>
>> fd = fuse_kern_mount_prepare(mountpoint, se->mo);
>> if (fd == -1) {
>> fuse_log(FUSE_LOG_ERR, "Mount preparation failed.\n");
>> - err = -EIO;
>> goto err;
>> }
>>
>> - /*
>> - * Enable synchronous FUSE_INIT and start worker thread, sync init
>> - * failure is not an error
>> - */
>> + *fall_back = true;
>> se->fd = fd;
>> err = session_start_sync_init(se, fd);
>> if (err) {
>> /* ENOTTY means kernel doesn't support sync init - not an error */
>> if (err != -ENOTTY)
>> goto err;
>> + } else {
>> + *fall_back = false;
>> }
>> +
>> +
>> snprintf(fd_opt, sizeof(fd_opt), "fd=%i", fd);
>> + err = -ENOMEM;
>> if (fuse_opt_add_opt(&mnt_opts_with_fd, mnt_opts) == -1 ||
>> fuse_opt_add_opt(&mnt_opts_with_fd, fd_opt) == -1) {
>> - err = -ENOMEM;
>> goto err;
>> }
>>
>> + /* Try to mount directly */
>> err = fuse_kern_fsmount_mo(mountpoint, se->mo, mnt_opts_with_fd);
>> +
>> + /* If mount failed with EPERM, fall back to fusermount3 with sync-init */
>
>
> ...since this is the new "actually use bidirectional fusermount3" code
> mentioned in the commit message.
Here I'm lost what you mean., bidirectional fusermount3 only follows below.
>
>> + if (err < 0 && errno == EPERM) {
>> + if (se->debug)
>> + fuse_log(FUSE_LOG_DEBUG,
>> + "fuse: privileged mount failed with EPERM, falling back to fusermount3\n");
>> +
>> + /* Terminate worker thread with wrong fd */
>> + if (session_wait_sync_init_completion(se) < 0)
>> + fuse_log(FUSE_LOG_ERR, "fuse: sync init completion failed\n");
>> +
>> + /* Close the privileged fd */
>> + close(fd);
>> + fd = -1;
>> + se->fd = -1;
>> +
>> + /* Call fusermount3 with --sync-init */
>> + err = -ENOTSUP;
>> + fd = mount_fusermount_obtain_fd(mountpoint, se->mo, mnt_opts,
>> + &sock_fd, &fusermount_pid);
>> + if (fd < 0) {
>> + fuse_log(
>> + FUSE_LOG_ERR,
>> + "fuse: fusermount3 sync-init failed\n");
>> + goto err;
>> + }
>> +
>> + /* Start worker thread with correct fd from fusermount3 */
>> + se->fd = fd;
>> + err = session_start_sync_init(se, fd);
>> + if (err) {
>> + if (err != -ENOTTY) {
>> + fuse_log(
>> + FUSE_LOG_ERR,
>> + "fuse: failed to start sync init worker\n");
>> + goto err_with_sock;
>> + }
>> + } else {
>> + *fall_back = false;
>
> We already set *fall_back to false above, didn't we? I'm slightly
> confused -- should we set *fall_back=true any time this function returns
> nonzero?
Already updated, because there was merge conflict since
session_start_sync_init() doesn't return ENOTTY anymore. fall_back is
possible as long as the ioctl doesn't succeed.
>
>> + }
>> +
>> + /* Send proceed signal and wait for mount result */
>> + err = fuse_fusermount_proceed_mnt(sock_fd);
>> + if (err < 0) {
>> + err = -EIO;
>> + goto err_with_sock;
>> + }
>> + } else if (err < 0) {
>> + /* Mount failed with non-EPERM error, bail out */
>> + goto err;
>> + }
>> +
>> +err_with_sock:
>> + if (sock_fd >= 0) {
>> + close(sock_fd);
>> + /* Reap fusermount3 child process to prevent zombie */
>> + if (fusermount_pid > 0)
>> + waitpid(fusermount_pid, NULL, 0);
>> + }
>> err:
>> if (err < 0) {
>> + /* Close fd first to unblock worker thread */
>> if (fd >= 0)
>> close(fd);
>> fd = -1;
>> se->fd = -1;
>> - se->error = -errno;
>> + se->error = err;
>> }
>> /* Wait for synchronous FUSE_INIT to complete */
>> if (session_wait_sync_init_completion(se) < 0)
>> @@ -4625,10 +4697,11 @@ err:
>> }
>> #else
>> static int fuse_session_mount_new_api(struct fuse_session *se,
>> - const char *mountpoint)
>> + const char *mountpoint, bool *fall_back)
>> {
>> (void) se;
>> (void) mountpoint;
>> + (void) fall_back;
>>
>> return -1;
>> }
>> @@ -4638,6 +4711,7 @@ int fuse_session_mount(struct fuse_session *se, const char *_mountpoint)
>> {
>> int fd;
>> char *mountpoint;
>> + bool fall_back;
>>
>> if (_mountpoint == NULL) {
>> fuse_log(FUSE_LOG_ERR, "Invalid null-ptr mountpoint!\n");
>> @@ -4681,21 +4755,18 @@ int fuse_session_mount(struct fuse_session *se, const char *_mountpoint)
>> return 0;
>> }
>>
>> - /* new linux mount api */
>> - fd = fuse_session_mount_new_api(se, mountpoint);
>> - if (fd >= 0)
>> - goto out;
>> + /* new linux mount api (and sync init) */
>> + fd = fuse_session_mount_new_api(se, mountpoint, &fall_back);
>>
>> /* fall back to old API */
>> - se->error = 0; /* reset error of new api */
>> - fd = fuse_kern_mount(mountpoint, se->mo);
>> - if (fd < 0)
>> - goto error_out;
>> + if (fall_back && fd < 0) {
>> + se->error = 0; /* reset error of new api */
>> + fd = fuse_kern_mount(mountpoint, se->mo);
>> + if (fd < 0)
>> + goto error_out;
>> + }
>>
>> -out:
>> se->fd = fd;
>> -
>> - /* Save mountpoint */
>> se->mountpoint = mountpoint;
>>
>> return 0;
>> diff --git a/lib/mount.c b/lib/mount.c
>> index 263b05051c236458b830c40181bce7f494803800..985938ea0be3e1affad19adad527a31ac2ca6034 100644
>> --- a/lib/mount.c
>> +++ b/lib/mount.c
>> @@ -41,6 +41,7 @@
>> #define FUSERMOUNT_PROG "fusermount3"
>> #define FUSE_COMMFD_ENV "_FUSE_COMMFD"
>> #define FUSE_COMMFD2_ENV "_FUSE_COMMFD2"
>> +#define ARG_FD_ENTRY_SIZE 30
>
> Thirty seems a bit much for an integer, especially one that can't go
> above 1 million. Eh, it's just stack space. :)
I just made it a define. We can change it later, though userspace stack
space is not that limited.
>
>> enum {
>> KEY_KERN_FLAG,
>> @@ -313,7 +314,7 @@ static int setup_auto_unmount(const char *mountpoint, int quiet)
>> return -1;
>> }
>>
>> - char arg_fd_entry[30];
>> + char arg_fd_entry[ARG_FD_ENTRY_SIZE];
>> snprintf(arg_fd_entry, sizeof(arg_fd_entry), "%i", fds[0]);
>> setenv(FUSE_COMMFD_ENV, arg_fd_entry, 1);
>> /*
>> @@ -386,7 +387,7 @@ static int fuse_mount_fusermount(const char *mountpoint, struct mount_opts *mo,
>> return -1;
>> }
>>
>> - char arg_fd_entry[30];
>> + char arg_fd_entry[ARG_FD_ENTRY_SIZE];
>> snprintf(arg_fd_entry, sizeof(arg_fd_entry), "%i", fds[0]);
>> setenv(FUSE_COMMFD_ENV, arg_fd_entry, 1);
>> /*
>> @@ -446,6 +447,127 @@ static int fuse_mount_fusermount(const char *mountpoint, struct mount_opts *mo,
>> return fd;
>> }
>>
>> +/*
>> + * Mount using fusermount3 with --sync-init flag for bidirectional fd exchange
>> + * Used by new mount API when privileged mount fails with EPERM
>> + *
>> + * Returns: fd on success, -1 on failure
>> + * On success, *sock_fd_out contains the socket fd for signaling fusermount3
>> + */
>> +int mount_fusermount_obtain_fd(const char *mountpoint, struct mount_opts *mo,
>> + const char *opts, int *sock_fd_out,
>> + pid_t *pid_out)
>> +{
>> + int fds[2];
>> + pid_t pid;
>> + int res;
>> + char arg_fd_entry[ARG_FD_ENTRY_SIZE];
>> + posix_spawn_file_actions_t action;
>> + int fd, status;
>> +
>> + (void)mo;
>> +
>> + if (!mountpoint) {
>> + fuse_log(FUSE_LOG_ERR, "fuse: missing mountpoint parameter\n");
>> + return -1;
>> + }
>> +
>> + res = socketpair(PF_UNIX, SOCK_STREAM, 0, fds);
>> + if (res == -1) {
>> + fuse_log(FUSE_LOG_ERR, "Running %s: socketpair() failed: %s\n",
>> + FUSERMOUNT_PROG, strerror(errno));
>> + return -1;
>> + }
>> +
>> + snprintf(arg_fd_entry, sizeof(arg_fd_entry), "%i", fds[0]);
>> + setenv(FUSE_COMMFD_ENV, arg_fd_entry, 1);
>
> Oh! /me realizes that FUSE_COMMFD{,2}_ENV can convey different things!
>
> If you're trying to get fusermount to *mount* a filesystem, then it's
> the AF_UNIX socket that is used to pass the /dev/fuse fd to the fuse
> server and then to trigger the mount.
>
> If you pass --auto-unmount/-U then fusermount waits for the socket to
> close and then unmounts the mount.
>
>> + snprintf(arg_fd_entry, sizeof(arg_fd_entry), "%i", fds[1]);
>> + setenv(FUSE_COMMFD2_ENV, arg_fd_entry, 1);
>
> ...and I guess you can pass the fds on the cli instead of goofy
> environment variables? I wonder if you should be passing them via CLI
> since you know fusermount supports it. OTOH I don't really care either
> way ;)
I had added the parameter to fusermount to avoid the env, issue is that
an old fusermount might be used with a new libfuse. I did that in the
past myself. For the new mount API and sync init, yeah, we can switch to
parameter, requires all the new functionality anyway.
>
>> +
>> + char const *const argv[] = {
>> + FUSERMOUNT_PROG,
>> + "--sync-init",
>> + "-o", opts ? opts : "",
>> + "--",
>> + mountpoint,
>> + NULL,
>> + };
>> +
>> + posix_spawn_file_actions_init(&action);
>> + posix_spawn_file_actions_addclose(&action, fds[1]);
>> + status = fusermount_posix_spawn(&action, argv, &pid);
>> + posix_spawn_file_actions_destroy(&action);
>> +
>> + if (status != 0) {
>> + close(fds[0]);
>> + close(fds[1]);
>> + return -1;
>> + }
>> +
>> + close(fds[0]);
>> +
>> + fd = receive_fd(fds[1]);
>> + if (fd < 0) {
>> + close(fds[1]);
>> + waitpid(pid, NULL, 0);
>> + return -1;
>> + }
>> +
>> + fcntl(fd, F_SETFD, FD_CLOEXEC);
>> +
>> + /* Return socket fd for later signaling */
>> + *sock_fd_out = fds[1];
>> + *pid_out = pid;
>> +
>> + return fd;
>> +}
>> +
>> +/*
>> + * Send proceed signal to fusermount3 and wait for mount result
>> + * Returns: 0 on success, -1 on failure
>> + */
>> +int fuse_fusermount_proceed_mnt(int sock_fd)
>> +{
>> + char buf = '\0';
>> + ssize_t res;
>> +
>> + /* Send proceed signal */
>> + do {
>> + res = send(sock_fd, &buf, 1, 0);
>> + } while (res == -1 && errno == EINTR);
>
> I wonder if all the pipe/socket communications ought to have been turned
> into a bunch of wrappers like what I did for
> mount_service.c/fuse_service.c?
>
> That said, it looks like most of the fusermount/sync-init communcations
> are single ints so maybe it doesn't matter. The communications for the
> fuse servers is much more complex and hence needs more structure.
Maybe we can look into that after merging the series and before making a
3.19 release? I don't want to make this series any longer than
absolutely neded.
Thanks,
Bernd
^ permalink raw reply [flat|nested] 59+ messages in thread
* Re: [PATCH 04/19] Add a new daemonize API
2026-03-24 17:36 ` Bernd Schubert
@ 2026-03-24 22:20 ` Darrick J. Wong
0 siblings, 0 replies; 59+ messages in thread
From: Darrick J. Wong @ 2026-03-24 22:20 UTC (permalink / raw)
To: Bernd Schubert; +Cc: linux-fsdevel, Miklos Szeredi, Joanne Koong
On Tue, Mar 24, 2026 at 06:36:49PM +0100, Bernd Schubert wrote:
>
>
> On 3/23/26 23:28, Darrick J. Wong wrote:
> > On Mon, Mar 23, 2026 at 06:44:59PM +0100, Bernd Schubert wrote:
> >> In complex fuse file systems one often wants
> >> a) fork
> >> b) start extra threads and system initialization (like network
> >> connection and RDMA memory registration).
> >> c) Start the fuse session
> >>
> >> With fuse_daemonize() there is no way to return a notification
> >> if step b) or c) failed. Therefore exising examples do
> >> the fuse_daemonize() after fuse_session_mount().
> >> Step, i.e. starting extra threads and possible failures are
> >> do not exist in these examples - unhandled use case.
> >
> > Er, this sentence is a little garbled. Is this supposed to say "The
> > example servers do not cover step b. This is an un-demonstrated use
> > case"?
>
> Oops sorry. How about
>
> Existing example/ file systems do the fuse_daemonize() after
> fuse_session_mount() - i.e. after the mount point is already
> established. Though, these example/ daemons do not start
> extra threads and do not need network initialization either.
>
> fuse_daemonize() also does not allow to return notification
> from the forked child to the parent.
>
> Complex fuse file system daemons often want the order of
> 1) fork - parent watches, child does the work
>
> Child:
> 2) start extra threads and system initialization (like network
> connection and RDMA memory registration) from the fork child.
> 3) Start the fuse session after everything else succeeded
>
> Parent:
> Report child initialization success or failure
Yeah, that's much clearer. :)
> >> With the FUSE_SYNC_INIT feature, which does FUSE_INIT at mount time
> >> and not after the mount anymore, it becomes even more complex
> >> as FUSE_INIT triggers startup of fuse_io_uring threads. That
> >> means for FUSE_SYNC_INIT forking/daemonization has to be done
> >> _before_ the fuse_session_mount().
> >
> > Hey, that /is/ a neat trick!
> >
> >> A new API is introduced to overcome the limitations of
> >> fuse_daemonize()
> >>
> >> fuse_daemonize_start() - fork, but foreground process does not
> >> terminate yet and watches the background.
> >>
> >> fuse_daemonize_signal() - background daemon signals to
> >> the foreground process success or failure.
> >>
> >> fuse_daemonize_active() - helper function for the high level
> >> interface, which needs to handle both APIs. fuse_daemonize()
> >> is called within fuse_main(), which now needs to know if the caller
> >> actually already used the new API itself.
> >>
> >> The object 'struct fuse_daemonize *' is allocated dynamically
> >> and stored a global variable in fuse_daemonize.c, because
> >> - high level fuse_main_real_versioned() needs to know
> >> if already daemonized
> >> - high level daemons do not have access to struct fuse_session
> >> - FUSE_SYNC_INIT in later commits can only be done if the new
> >> API (or a file system internal) daemonization is used.
> >>
> >> Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
> >> ---
> >> doc/README.daemonize | 186 +++++++++++++++++++++++++++++
> >> example/passthrough_hp.cc | 18 ++-
> >> include/fuse_daemonize.h | 71 +++++++++++
> >> include/meson.build | 3 +-
> >> lib/fuse_daemonize.c | 284 ++++++++++++++++++++++++++++++++++++++++++++
> >> lib/fuse_i.h | 4 +-
> >> lib/fuse_lowlevel.c | 1 +
> >> lib/fuse_versionscript | 3 +
> >> lib/helper.c | 13 +-
> >> lib/meson.build | 3 +-
> >> test/test_want_conversion.c | 1 +
> >> 11 files changed, 576 insertions(+), 11 deletions(-)
> >>
> >> diff --git a/doc/README.daemonize b/doc/README.daemonize
> >> new file mode 100644
> >> index 0000000000000000000000000000000000000000..f7473361b3a39369291e9401dfc5f7928ff4160b
> >> --- /dev/null
> >> +++ b/doc/README.daemonize
> >> @@ -0,0 +1,186 @@
> >> +FUSE Daemonization API
> >> +======================
> >> +
> >> +This document describes the FUSE daemonization APIs, including the legacy
> >> +fuse_daemonize() function and the new fuse_daemonize_start()/signal() API
> >> +introduced in libfuse 3.19.
> >> +
> >> +
> >> +Overview
> >> +--------
> >> +
> >> +FUSE filesystems often need to run as background daemons. Daemonization
> >> +involves forking the process, creating a new session, and redirecting
> >> +standard file descriptors. The challenge is properly reporting initialization
> >> +failures to the parent process.
> >
> > Yeah, I found this part pretty murky when writing fuse[24]fs -- the
> > internal library failures are fuse_log()'d, but then what if the fuse
> > server has its own logging library? libext2fs has its own, and there's
> > rather a lot of shenanigans that go on to make sure that libfuse and
> > libext2fs print to the same streams.
>
> Did you find fuse_set_log_func()
I didn't, actually. In the end there were enough twists that I decided
it would be cleaner to go scorched earth -- open whatever logfile I
wanted and use dup2+fdreopen to make sure that both the stdio streams
and fd 1&2 actually write to wherever they're supposed to go.
> >
> > If you want to run as a systemd service, stdout/stderr are usually
> > routed to journald so there's no need to mess with stdout/stderr. But
> > then if you're running as a system service you don't want daemonize
> > anyway.
> >
> > Ok enough rambling about my thing. I appreciate you documenting the old
> > fuse_daemonize in detail:
> >
> >> +Old API: fuse_daemonize()
> >> +--------------------------
> >> +
> >> +Function signature:
> >> + int fuse_daemonize(int foreground);
> >> +
> >> +Location: lib/helper.c
> >> +
> >> +This is the legacy daemonization API, primarily used with the high-level
> >> +fuse_main() interface.
> >> +
> >> +Behavior:
> >> +- If foreground=0: forks the process, creates new session, redirects stdio
> >
> > It creates a new session? I see that a pipe gets created between parent
> > and child, but I don't see a new fuse_session or ... oh, you meant a new
> > *Unix* session via setsid(). Can you say "creates new Unix session"?
> >
> >> +- If foreground=1: only changes directory to "/"
> >> +- Parent waits for a single byte on a pipe before exiting
> >> +- Child writes completion byte immediately after redirecting stdio
> >> +- Always changes directory to "/"
> >> +
> >> +Limitations:
> >> +1. No failure reporting: The parent receives notification immediately after
> >> + fork/setsid, before any meaningful initialization (like mounting the
> >> + filesystem or starting threads).
> >
> > <nod> and I gather that's what fuse_daemonize_signal() is for.
> >
> >> +2. Timing constraint: Must be called AFTER fuse_session_mount() in existing
> >> + examples, because there's no way to report mount failures to the parent.
> >> +
> >> +3. Thread initialization: Cannot report failures from complex initialization
> >> + steps like:
> >> + - Starting worker threads
> >
> > Where do those errors go? It looks like by default they go to stderr,
> > which is /dev/null after daemonization, right?
>
> Yeah, one way for fuse_log() is fuse_log_enable_syslog().
>
> >
> >> + - Network connection setup
> >> + - RDMA memory registration
> >> + - Resource allocation
> >> +
> >> +4. FUSE_SYNC_INIT incompatibility: With the FUSE_SYNC_INIT feature, FUSE_INIT
> >> + happens at mount time and may start io_uring threads. This requires
> >> + daemonization BEFORE mount, which the old API cannot handle properly.
> >
> > (For anyone following at home, fuse-containers cannot use synchronous
> > init because the new fuservicemount helper opens /dev/fuse and calls
> > mount() at the behest of the fuse server, so I'm mostly interested in
> > this as an exercise in getting to know libfuse.)
>
> Hmm, that is a pity. And many many thanks for your reviews!
Now that I've seen how you're using them in fusermount, I've changed my
mind; I think fuse services actually *can* use synchronous mounts. The
aspect that convinced me is seeing how libfuse creates that background
thread to handle the fuse requests until FUSE_INIT is complete.
0) fuse server tells libfuse to start the background FUSE_INIT reply
thread
1) fuse server sends a packet through the AF_UNIX socket to initiate
mounting process
2) fuservicemount asking it to enable sync_init and call fsmount(2)
(Note that fuservicemount runs with the same mount ns as whatever
wants to mount the filesystem; the systemd service can be configured
with a private mountns)
3) kernel sets up the filesystem, initiates FUSE_INIT, waits
4) the FUSE_INIT thread sees the request, does stuff, replies
5) kernel returns to fuservicemount (and presumably move_mounts)
6) fuservicemount sends a reply back to the fuse server
7) fuse server sees the fuservicemount reply, shuts down the background
thread
8) fuse server starts the real event handler threads, starts serving
requests
> >
> >> +
> >> +Example usage (old API):
> >> + fuse = fuse_new(...);
> >> + fuse_mount(fuse, mountpoint);
> >> + fuse_daemonize(opts.foreground); // After mount, can't report mount failure
> >> + fuse_set_signal_handlers(se);
> >> + fuse_session_loop(se);
> >> +
> >> +
> >> +New API: fuse_daemonize_start() / fuse_daemonize_signal()
> >> +----------------------------------------------------------
> >> +
> >> +Functions:
> >> + int fuse_daemonize_start(unsigned int flags);
> >> + void fuse_daemonize_signal(int status);
> >> + bool fuse_daemonize_active(void);
> >> +
> >> +Location: lib/fuse_daemonize.c, include/fuse_daemonize.h
> >> +Available since: libfuse 3.19
> >> +
> >> +This new API solves the limitations of fuse_daemonize() by splitting
> >> +daemonization into two phases:
> >> +
> >> +1. fuse_daemonize_start() - Fork and setup, but parent waits
> >> +2. fuse_daemonize_signal() - Signal success/failure to parent
> >> +
> >> +
> >> +fuse_daemonize_start()
> >> +----------------------
> >> +
> >> +Flags:
> >> +- FUSE_DAEMONIZE_NO_CHDIR: Don't change directory to "/"
> >> +- FUSE_DAEMONIZE_NO_BACKGROUND: Don't fork (foreground mode)
> >> +
> >> +Behavior:
> >> +- Unless NO_BACKGROUND: forks the process
> >> +- Parent waits for status signal from child
> >> +- Child creates new session and continues
> >> +- Unless NO_CHDIR: changes directory to "/"
> >> +- Closes stdin immediately in child
> >> +- Starts a watcher thread to detect parent death
> >> +- Returns 0 in child on success, negative errno on error
> >> +
> >> +Parent death detection:
> >> +- Uses a "death pipe" - parent keeps write end open
> >> +- Child's watcher thread polls the read end
> >> +- When parent dies, pipe gets POLLHUP and child exits
> >> +- Prevents orphaned daemons if parent is killed
> >> +
> >> +
> >> +fuse_daemonize_signal()
> >> +-----------------------
> >> +
> >> +Status values:
> >> +- FUSE_DAEMONIZE_SUCCESS (0): Initialization succeeded
> >> +- FUSE_DAEMONIZE_FAILURE (1): Initialization failed
> >> +
> >> +Behavior:
> >> +- Signals the parent process with success or failure status
> >
> > Does this get written back through the death pipe? Or does it use
> > process signals?
>
> /* Private/internal data */
> struct fuse_daemonize {
> unsigned int flags;
> int signal_pipe_wr; /* write end for signaling parent */
> int death_pipe_rd; /* read end, POLLHUP when parent dies */
>
>
> I.e. it writes through signal_pipe_wr. I probably could simplify it and
> just use a single pipe. Although from my point of view, these are
> details that can be improved later. I'm quite under time pressure...
Ah, ok. How about changing the wording "Send the success or failure
status to the parent process" ?
> >
> >> +- Parent exits with EXIT_SUCCESS or EXIT_FAILURE accordingly
> >> +- On success: redirects stdout/stderr to /dev/null
> >> +- Stops the parent watcher thread
> >> +- Cleans up pipes and internal state
> >
> > I think these might be better called fuse_daemonize_success() and
> > fuse_daemonize_fail() since one of them does more than just signal the
> > parent process.
>
> Absolutely and thanks a lot! The API functions are the main reason why I
> sent it to the list - once the API is in place I cann change it anymore
> after a libfuse
> release.
<nod>
> >
> >> +- Safe to call multiple times
> >> +- Safe to call even if fuse_daemonize_start() failed
> >> +
> >> +
> >> +fuse_daemonize_active()
> >> +-----------------------
> >> +
> >> +Returns true if daemonization is active and waiting for signal.
> >> +
> >> +Used by the high-level fuse_main() to detect if the application already
> >> +called the new API, avoiding double-daemonization.
> >> +
> >> +
> >> +Example usage (new API):
> >> +-------------------------
> >> +
> >> + // Start daemonization BEFORE mount
> >> + unsigned int daemon_flags = 0;
> >> + if (foreground)
> >> + daemon_flags |= FUSE_DAEMONIZE_NO_BACKGROUND;
> >> +
> >> + if (fuse_daemonize_start(daemon_flags) != 0)
> >> + goto error;
> >> +
> >> + // Mount can now fail and be reported to parent
> >> + if (fuse_session_mount(se, mountpoint) != 0)
> >> + goto error_signal;
> >> +
> >> + // Complex initialization can fail and be reported
> >> + if (setup_threads() != 0)
> >> + goto error_signal;
> >> +
> >> + if (setup_network() != 0)
> >> + goto error_signal;
> >
> > Hrmm, interesting, this solves the problem of fuse2fs having to wait
> > until FUSE_INIT to create background threads.
>
> So exactly what the new API for.
>
> I turned the order around in the example above, though. I think one
> first wants to set up threads and network and then mount.
<nod> fuse4fs does all the important setup pieces (opening the block
device, parsing the ext4 superblock, etc) before starting on the libfuse
part, so FUSE_INIT is mostly just setting feature bits and opening a few
things (like PSI stall monitors and background flush threads) that are
either optional or actually have to happen after daemonize().
> >> +
> >> + // Signal success - parent exits with EXIT_SUCCESS
> >> + // This is typically done in the init() callback after FUSE_INIT
> >> + fuse_daemonize_signal(FUSE_DAEMONIZE_SUCCESS);
> >> +
> >> + // Run main loop
> >> + fuse_session_loop(se);
> >> +
> >> + return 0;
> >> +
> >> +error_signal:
> >> + // Signal failure - parent exits with EXIT_FAILURE
> >> + fuse_daemonize_signal(FUSE_DAEMONIZE_FAILURE);
> >> +error:
> >> + return 1;
> >> +
> >> +
> >> +When to signal success
> >> +----------------------
> >> +
> >> +The success signal should be sent after all critical initialization is
> >> +complete. For FUSE filesystems, this is typically in the init() callback,
> >> +after FUSE_INIT has been processed successfully.
> >> +
> >> +Example (from passthrough_hp.cc):
> >> + static void sfs_init(void *userdata, fuse_conn_info *conn) {
> >> + // ... initialization ...
> >> + fuse_daemonize_signal(FUSE_DAEMONIZE_SUCCESS);
> >> + }
> >> +
> >> +This ensures the parent only exits after:
> >> +- Mount succeeded
> >> +- FUSE_INIT completed
> >> +- All threads started
> >> +- Filesystem is ready to serve requests
> >
> > Very nice! Does this new daemonization work for async-init servers?
>
> Sure. See passthrough_hp.cc - it does the fuse_daemonize_success() in
> sfs_init(). With async-init that is long after the mount, with sync-init
> that is as part of the mount.
<nod>
> >
> >> diff --git a/example/passthrough_hp.cc b/example/passthrough_hp.cc
> >> index 9f795c5546ee8312e1393c5b8fcebfd77724fb49..a0da5c7e6ab07f921df65db9c700e7de77fe1598 100644
> >> --- a/example/passthrough_hp.cc
> >> +++ b/example/passthrough_hp.cc
> >> @@ -55,6 +55,7 @@
> >> #include <errno.h>
> >> #include <ftw.h>
> >> #include <fuse_lowlevel.h>
> >> +#include <fuse_daemonize.h>
> >> #include <inttypes.h>
> >> #include <string.h>
> >> #include <sys/file.h>
> >> @@ -243,6 +244,9 @@ static void sfs_init(void *userdata, fuse_conn_info *conn)
> >>
> >> /* Try a large IO by default */
> >> conn->max_write = 4 * 1024 * 1024;
> >> +
> >> + /* Signal successful init to parent */
> >> + fuse_daemonize_signal(FUSE_DAEMONIZE_SUCCESS);
> >> }
> >>
> >> static void sfs_getattr(fuse_req_t req, fuse_ino_t ino, fuse_file_info *fi)
> >> @@ -1580,6 +1584,7 @@ int main(int argc, char *argv[])
> >> {
> >> struct fuse_loop_config *loop_config = NULL;
> >> void *teardown_watchog = NULL;
> >> + unsigned int daemon_flags = 0;
> >>
> >> // Parse command line options
> >> auto options{ parse_options(argc, argv) };
> >> @@ -1638,10 +1643,14 @@ int main(int argc, char *argv[])
> >>
> >> fuse_loop_cfg_set_clone_fd(loop_config, fs.clone_fd);
> >>
> >> - if (fuse_session_mount(se, argv[2]) != 0)
> >> + /* Start daemonization before mount so parent can report mount failure */
> >> + if (fs.foreground)
> >> + daemon_flags |= FUSE_DAEMONIZE_NO_BACKGROUND;
> >> + if (fuse_daemonize_start(daemon_flags) != 0)
> >> goto err_out3;
> >>
> >> - fuse_daemonize(fs.foreground);
> >> + if (fuse_session_mount(se, argv[2]) != 0)
> >> + goto err_out4;
> >>
> >> if (!fs.foreground)
> >> fuse_log_enable_syslog("passthrough-hp", LOG_PID | LOG_CONS,
> >> @@ -1650,7 +1659,7 @@ int main(int argc, char *argv[])
> >> teardown_watchog = fuse_session_start_teardown_watchdog(
> >> se, fs.root.stop_timeout_secs, NULL, NULL);
> >> if (teardown_watchog == NULL)
> >> - goto err_out3;
> >> + goto err_out4;
> >>
> >> if (options.count("single"))
> >> ret = fuse_session_loop(se);
> >> @@ -1659,6 +1668,9 @@ int main(int argc, char *argv[])
> >>
> >> fuse_session_unmount(se);
> >>
> >> +err_out4:
> >> + if (fuse_daemonize_active())
> >> + fuse_daemonize_signal(FUSE_DAEMONIZE_FAILURE);
> >> err_out3:
> >> fuse_remove_signal_handlers(se);
> >> err_out2:
> >> diff --git a/include/fuse_daemonize.h b/include/fuse_daemonize.h
> >> new file mode 100644
> >> index 0000000000000000000000000000000000000000..f49f6aa580a93547198e7dabe65498708e65d024
> >> --- /dev/null
> >> +++ b/include/fuse_daemonize.h
> >> @@ -0,0 +1,71 @@
> >> +/*
> >> + * FUSE: Filesystem in Userspace
> >> + * Copyright (C) 2026 Bernd Schubert <bsbernd.com>
> >> + *
> >> + * This program can be distributed under the terms of the GNU LGPLv2.
> >> + * See the file COPYING.LIB.
> >> + *
> >> + */
> >> +
> >> +#ifndef FUSE_DAEMONIZE_H_
> >> +#define FUSE_DAEMONIZE_H_
> >> +
> >> +#include <stdint.h>
> >> +#include <stdbool.h>
> >> +
> >> +#ifdef __cplusplus
> >> +extern "C" {
> >> +#endif
> >> +
> >> +/**
> >> + * Flags for fuse_daemonize_start()
> >> + */
> >> +#define FUSE_DAEMONIZE_NO_CHDIR (1 << 0)
> >> +#define FUSE_DAEMONIZE_NO_BACKGROUND (1 << 1)
> >> +
> >> +/**
> >> + * Status values for fuse_daemonize_signal()
> >> + */
> >> +#define FUSE_DAEMONIZE_SUCCESS 0
> >> +#define FUSE_DAEMONIZE_FAILURE 1
> >
> > What if fuse_daemonize_signal() took an exitcode and passed it directly
> > to the parent process via the death pipe, and the parent process can
> > pass that to exit()? IOWs, what if fuse_daemonize_signal writes @status
> > into signal_pipe_wr instead of flattening it to EXIT_SUCCESS/FAILURE?
> >
> > Or, if you want to constrain the values to binary, then why not use
> >
> > #define FUSE_DAEMONIZE_SUCCESS (EXIT_SUCCESS) ?
>
> Good idea. Updated to
>
> /**
> * Signal daemonization failure to parent and cleanup.
> *
> * @param err error code to pass to parent
> */
> void fuse_daemonize_fail(int err);
Cool!
>
> >
> >> +
> >> +/**
> >> + * Start daemonization process.
> >> + *
> >> + * Unless FUSE_DAEMONIZE_NO_BACKGROUND is set, this forks the process.
> >> + * The parent waits for a signal from the child via fuse_daemonize_signal().
> >> + * The child returns from this call and continues setup.
> >> + *
> >> + * Unless FUSE_DAEMONIZE_NO_CHDIR is set, changes directory to "/".
> >> + *
> >> + * Must be called before fuse_session_mount().
> >> + *
> >> + * @param flags combination of FUSE_DAEMONIZE_* flags
> >> + * @return 0 on success, negative errno on error
> >> + */
> >> +int fuse_daemonize_start(unsigned int flags);
> >> +
> >> +/**
> >> + * Signal daemonization status to parent and cleanup.
> >> + *
> >> + * The child calls this after setup is complete (or failed).
> >> + * The parent receives the status and exits with it.
> >> + * Safe to call multiple times or if start failed.
> >> + *
> >> + * @param status FUSE_DAEMONIZE_SUCCESS or FUSE_DAEMONIZE_FAILURE
> >> + */
> >> +void fuse_daemonize_signal(int status);
> >> +
> >> +/**
> >> + * Check if daemonization is active and waiting for signal.
> >> + *
> >> + * @return true if active, false otherwise
> >> + */
> >> +bool fuse_daemonize_active(void);
> >> +
> >> +#ifdef __cplusplus
> >> +}
> >> +#endif
> >> +
> >> +#endif /* FUSE_DAEMONIZE_H_ */
> >> +
> >> diff --git a/include/meson.build b/include/meson.build
> >> index bf671977a5a6a9142bd67aceabd8a919e3d968d0..cfbaf52ac5d84369e92948c631e2fcfdd04ac2eb 100644
> >> --- a/include/meson.build
> >> +++ b/include/meson.build
> >> @@ -1,4 +1,5 @@
> >> libfuse_headers = [ 'fuse.h', 'fuse_common.h', 'fuse_lowlevel.h',
> >> - 'fuse_opt.h', 'cuse_lowlevel.h', 'fuse_log.h' ]
> >> + 'fuse_opt.h', 'cuse_lowlevel.h', 'fuse_log.h',
> >> + 'fuse_daemonize.h' ]
> >>
> >> install_headers(libfuse_headers, subdir: 'fuse3')
> >> diff --git a/lib/fuse_daemonize.c b/lib/fuse_daemonize.c
> >> new file mode 100644
> >> index 0000000000000000000000000000000000000000..5d191e7d737d04876d02ef6bd526061c003d2ab7
> >> --- /dev/null
> >> +++ b/lib/fuse_daemonize.c
> >> @@ -0,0 +1,284 @@
> >> +/*
> >> + * FUSE: Filesystem in Userspace
> >> + * Copyright (C) 2026 Bernd Schubert <bsbernd.com>
> >> + *
> >> + * This program can be distributed under the terms of the GNU LGPLv2.
> >> + * See the file COPYING.LIB.
> >> + */
> >> +
> >> +#define _GNU_SOURCE
> >> +
> >> +#include "fuse_daemonize.h"
> >> +
> >> +#include <fcntl.h>
> >> +#include <poll.h>
> >> +#include <pthread.h>
> >> +#include <stdatomic.h>
> >> +#include <stdio.h>
> >> +#include <stdlib.h>
> >> +#include <string.h>
> >> +#include <sys/types.h>
> >> +#include <unistd.h>
> >> +#include <stdbool.h>
> >> +#include <errno.h>
> >> +
> >> +/* Private/internal data */
> >> +struct fuse_daemonize {
> >> + unsigned int flags;
> >> + int signal_pipe_wr; /* write end for signaling parent */
> >
> > Hrm. Ok, so signal_pipe[2] is the pipe through which the child writes
> > an int back to the parent, and then the parent can convey that outcome
> > to whatever started the parent.
> >
> >> + int death_pipe_rd; /* read end, POLLHUP when parent dies */
> >
> > death_pipe[2] is a different pipe. The parent closes its end of the
> > pipe and the child treats POLLHUP as a message that the parent died.
>
> Yeah, let's say network initialization takes a long time or never
> succeeds, the user wants to abort the mount and kills the parent. In
> that case client should not continue to run in the background/
<nod>
> >
> >> + int stop_pipe_rd; /* read end for stop signal */
> >> + int stop_pipe_wr; /* write end for stop signal */
> >
> > and this third pipe exist so that the child can wake its own "parent
> > watcher" thread and have it abort.
> >
> >> + pthread_t watcher;
> >> + int watcher_started;
> >
> > Hm. So watcher_started is initialized to 0 in the parent, gets set to 1
> > in the child's copy of memory when it starts the parent-watcher thread,
> > and later becomes zero when the child shuts down the parent-watcher
> > thread.
> >
> >> + _Atomic bool active;
> >
> > active is set in the parent process, copied to the child process, and
> > cleared in fuse_daemonize_signal in the child.
> >
> >> + _Atomic bool daemonized;
> >
> > and daemonized is set in the child upon creation of the child; and
> > read by fuse_daemonize_signal. The parent never accesses its copy of
> > the variable.
> >
> >> +};
> >> +
> >> +/* Global daemonization object pointer */
> >> +static _Atomic(struct fuse_daemonize *) daemonize;
> >> +
> >> +/* Watcher thread: polls for parent death or stop signal */
> >> +static void *parent_watcher_thread(void *arg)
> >> +{
> >> + struct fuse_daemonize *di = arg;
> >> + struct pollfd pfd[2];
> >> +
> >> + pfd[0].fd = di->death_pipe_rd;
> >> + pfd[0].events = POLLIN;
> >> + pfd[1].fd = di->stop_pipe_rd;
> >> + pfd[1].events = POLLIN;
> >> +
> >> + while (1) {
> >> + int rc = poll(pfd, 2, -1);
> >> +
> >> + if (rc < 0)
> >> + continue;
> >> +
> >> + /* Parent died - death pipe write end closed */
> >> + if (pfd[0].revents & (POLLHUP | POLLERR))
> >> + _exit(EXIT_FAILURE);
> >> +
> >> + /* Stop signal received */
> >> + if (pfd[1].revents & POLLIN)
> >> + break;
> >> + }
> >> + return NULL;
> >> +}
> >> +
> >> +static int start_parent_watcher(struct fuse_daemonize *daemonize)
> >> +{
> >> + int rc;
> >> +
> >> + rc = pthread_create(&daemonize->watcher, NULL, parent_watcher_thread,
> >> + daemonize);
> >> + if (rc != 0) {
> >> + perror("fuse_daemonize: pthread_create");
> >
> > pthread functions return positive error numbers and do not set errno, so
> > you can't use perror().
>
>
> Thanks, updated.
>
> >
> >> + return -1;
> >> + }
> >> + daemonize->watcher_started = 1;
> >
> > Isn't this a bool value?
>
> Already just updated it before even reading this :)
>
> >
> > FWIW the rest of the logic below looks correct, though I think the
> > daemonize object itself would need a pthread_mutex_t to coordinate
> > access if it's possible or desirable for multiple threads to access it.
> >
> > I think that's not the case, and any fuse server that did need that
> > could implement the locking on its own.
>
> Yeah, I had thought about it, but I can't find a real use case yet.
> Maybe with sync-init fuse_daemonize_success() needs to be called twice,
> although I still assume that calling it in the FUSE_INIT handler should
> be ok, because the mount then succeeded. I need to think about it
> another night.
> Hmm, actually ->init() needs to know if sync or async init used, i.e.
> the ->init() function should only call fuse_daemonize_success() with
> async-init. With sync-init that should be done after
> fuse_session_mount(). I'm going to add another new patch at the end of
> the series in the next patch version.
<nod> Thanks for bearing with my unfamiliarity! :)
--D
>
> Thanks,
> Bernd
>
^ permalink raw reply [flat|nested] 59+ messages in thread
* Re: [PATCH 11/19] Add support for the new linux mount API
2026-03-24 20:16 ` Bernd Schubert
@ 2026-03-24 22:46 ` Darrick J. Wong
0 siblings, 0 replies; 59+ messages in thread
From: Darrick J. Wong @ 2026-03-24 22:46 UTC (permalink / raw)
To: Bernd Schubert
Cc: linux-fsdevel, Miklos Szeredi, Joanne Koong, Bernd Schubert
On Tue, Mar 24, 2026 at 09:16:03PM +0100, Bernd Schubert wrote:
>
>
> On 3/24/26 00:42, Darrick J. Wong wrote:
> > On Mon, Mar 23, 2026 at 06:45:06PM +0100, Bernd Schubert wrote:
> >> From: Bernd Schubert <bschubert@ddn.com>
> >>
> >> So far only supported for fuse_session_mount(), which is called
> >> from high and low level API, but not yet supported for
> >> fuse_open_channel(), which used for privilege drop through
> >> mount.fuse. Main goal for the new API is support for synchronous
> >> FUSE_INIT and I don't think that is going to work with
> >> fuse_open_channel(). At least not with io-uring support as long
> >> as it is started from FUSE_INIT.
> >>
> >> Signed-off-by: Bernd Schubert <bschubert@ddn.com>
> >> ---
> >> lib/fuse_lowlevel.c | 75 +++++++++-
> >> lib/meson.build | 3 +
> >> lib/mount.c | 27 +++-
> >> lib/mount_fsmount.c | 405 ++++++++++++++++++++++++++++++++++++++++++++++++++++
> >> lib/mount_i_linux.h | 14 ++
> >> meson.build | 19 ++-
> >> 6 files changed, 535 insertions(+), 8 deletions(-)
> >>
> >> diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c
> >> index 4a1c7c4c02a05706e077a7ea89fab3c81bfe22cd..626233df20f49fa89cd9327f94340169d7061f75 100644
> >> --- a/lib/fuse_lowlevel.c
> >> +++ b/lib/fuse_lowlevel.c
> >> @@ -20,6 +20,9 @@
> >> #include "util.h"
> >> #include "fuse_uring_i.h"
> >> #include "fuse_daemonize.h"
> >> +#if defined(__linux__)
> >> +#include "mount_i_linux.h"
> >> +#endif
> >>
> >> #include <pthread.h>
> >> #include <stdatomic.h>
> >> @@ -4398,6 +4401,64 @@ int fuse_session_custom_io_30(struct fuse_session *se,
> >> offsetof(struct fuse_custom_io, clone_fd), fd);
> >> }
> >>
> >> +#if defined(HAVE_NEW_MOUNT_API)
> >> +/* Only linux supports sync FUSE_INIT so far */
> >
> > What does synchronous FUSE_INIT have to do with fsmount()?
> > Does it not work with the old mount(2)?
>
> So we had this discussion with Miklos in the earlier version of the
> patches here https://github.com/libfuse/libfuse/pull/1367
> where Miklos has noticed the possible deadlock and suggested the new
> mount API. I think Miklos kernel patch solves that, without the new
> mount API, but then I'm now hesitating to make the code even more
> complex by supported old and new API. Until BSD gains that sync-fuse
> init, this is a Linux only feature and was added after new the new mount
> api.
>
> The comment is not right for this commit, though.
Oh ok. I saw that discussion with Miklos about the deadlock ended in a
patch to just abort the connection if the server crashed during
FUSE_INIT.
I think it's perfectly legit to limit the new sync_init API to fsmount.
:)
> >> +static int fuse_session_mount_new_api(struct fuse_session *se,
> >> + const char *mountpoint)
> >> +{
> >> + int fd = -1;
> >> + int res, err;
> >> + char *mnt_opts = NULL;
> >> + char *mnt_opts_with_fd = NULL;
> >> + char fd_opt[32];
> >> +
> >> + res = fuse_kern_mount_get_base_mnt_opts(se->mo, &mnt_opts);
> >> + if (res == -1) {
> >> + fuse_log(FUSE_LOG_ERR,
> >> + "fuse: failed to get base mount options\n");
> >> + err = -EIO;
> >> + goto err;
> >> + }
> >> +
> >> + fd = fuse_kern_mount_prepare(mountpoint, se->mo);
> >> + if (fd == -1) {
> >> + fuse_log(FUSE_LOG_ERR, "Mount preparation failed.\n");
> >> + err = -EIO;
> >> + goto err;
> >> + }
> >> +
> >> + snprintf(fd_opt, sizeof(fd_opt), "fd=%i", fd);
> >> + if (fuse_opt_add_opt(&mnt_opts_with_fd, mnt_opts) == -1 ||
> >> + fuse_opt_add_opt(&mnt_opts_with_fd, fd_opt) == -1) {
> >> + err = -ENOMEM;
> >> + goto err;
> >> + }
> >> +
> >> + err = fuse_kern_fsmount_mo(mountpoint, se->mo, mnt_opts_with_fd);
> >> +err:
> >> + if (err) {
> >> + if (fd >= 0)
> >> + close(fd);
> >> + fd = -1;
> >> + se->fd = -1;
> >> + se->error = -errno;
> >> + }
> >> +
> >> + free(mnt_opts);
> >> + free(mnt_opts_with_fd);
> >> + return fd;
> >> +}
> >> +#else
> >> +static int fuse_session_mount_new_api(struct fuse_session *se,
> >> + const char *mountpoint)
> >> +{
> >> + (void)se;
> >> + (void)mountpoint;
> >> +
> >> + return -1;
> >> +}
> >> +#endif
> >> +
> >> int fuse_session_mount(struct fuse_session *se, const char *_mountpoint)
> >> {
> >> int fd;
> >> @@ -4425,6 +4486,8 @@ int fuse_session_mount(struct fuse_session *se, const char *_mountpoint)
> >> close(fd);
> >> } while (fd >= 0 && fd <= 2);
> >>
> >> + /* Open channel */
> >> +
> >> /*
> >> * To allow FUSE daemons to run without privileges, the caller may open
> >> * /dev/fuse before launching the file system and pass on the file
> >> @@ -4443,10 +4506,18 @@ int fuse_session_mount(struct fuse_session *se, const char *_mountpoint)
> >> return 0;
> >> }
> >>
> >> - /* Open channel */
> >> + /* new linux mount api */
> >> + fd = fuse_session_mount_new_api(se, mountpoint);
> >> + if (fd >= 0)
> >> + goto out;
> >> +
> >> + /* fall back to old API */
> >> + se->error = 0; /* reset error of new api */
> >> fd = fuse_kern_mount(mountpoint, se->mo);
> >> - if (fd == -1)
> >> + if (fd < 0)
> >> goto error_out;
> >> +
> >> +out:
> >> se->fd = fd;
> >>
> >> /* Save mountpoint */
> >> diff --git a/lib/meson.build b/lib/meson.build
> >> index 5bd449ebffe7c9229df904d647d990c6c47f80b5..5fd738a589c5aba97a738d5eedbf0f9962e4adfc 100644
> >> --- a/lib/meson.build
> >> +++ b/lib/meson.build
> >> @@ -7,6 +7,9 @@ libfuse_sources = ['fuse.c', 'fuse_i.h', 'fuse_loop.c', 'fuse_loop_mt.c',
> >>
> >> if host_machine.system().startswith('linux')
> >> libfuse_sources += [ 'mount.c' ]
> >> + if private_cfg.get('HAVE_NEW_MOUNT_API', false)
> >> + libfuse_sources += [ 'mount_fsmount.c' ]
> >> + endif
> >> else
> >> libfuse_sources += [ 'mount_bsd.c' ]
> >> endif
> >> diff --git a/lib/mount.c b/lib/mount.c
> >> index b3ee9ba4c0a74c1d0d55f916e7046e064f8468b0..30fd4d2f9bbb84c817b2363b2075456acd1c1255 100644
> >> --- a/lib/mount.c
> >> +++ b/lib/mount.c
> >> @@ -449,8 +449,8 @@ static int fuse_mount_fusermount(const char *mountpoint, struct mount_opts *mo,
> >> #define O_CLOEXEC 0
> >> #endif
> >>
> >> -static int fuse_kern_mount_prepare(const char *mnt,
> >> - struct mount_opts *mo)
> >> +int fuse_kern_mount_prepare(const char *mnt,
> >> + struct mount_opts *mo)
> >> {
> >> char tmp[128];
> >> const char *devname = fuse_mnt_get_devname();
> >> @@ -500,6 +500,26 @@ out_close:
> >> return -1;
> >> }
> >>
> >> +#if defined(HAVE_NEW_MOUNT_API)
> >> +/**
> >> + * Wrapper for fuse_kern_fsmount that accepts struct mount_opts
> >> + * @mnt: mountpoint
> >> + * @mo: mount options
> >> + * @mnt_opts: mount options to pass to the kernel
> >> + *
> >> + * Returns: 0 on success, -1 on failure with errno set
> >> + */
> >> +int fuse_kern_fsmount_mo(const char *mnt, struct mount_opts *mo,
> >> + const char *mnt_opts)
> >> +{
> >> + const char *devname = fuse_mnt_get_devname();
> >> +
> >> + return fuse_kern_fsmount(mnt, mo->flags, mo->blkdev, mo->fsname,
> >> + mo->subtype, devname, mo->kernel_opts,
> >> + mnt_opts);
> >> +}
> >> +#endif
> >> +
> >> /**
> >> * Complete the mount operation with an already-opened fd
> >> * @mnt: mountpoint
> >> @@ -643,8 +663,7 @@ void destroy_mount_opts(struct mount_opts *mo)
> >> free(mo);
> >> }
> >>
> >> -static int fuse_kern_mount_get_base_mnt_opts(struct mount_opts *mo,
> >> - char **mnt_optsp)
> >> +int fuse_kern_mount_get_base_mnt_opts(struct mount_opts *mo, char **mnt_optsp)
> >> {
> >> if (get_mnt_flag_opts(mnt_optsp, mo->flags) == -1)
> >> return -1;
> >> diff --git a/lib/mount_fsmount.c b/lib/mount_fsmount.c
> >> new file mode 100644
> >> index 0000000000000000000000000000000000000000..cba998bc60c783a5edc0c16570f7e5512b7f1253
> >> --- /dev/null
> >> +++ b/lib/mount_fsmount.c
> >> @@ -0,0 +1,405 @@
> >> +/*
> >> + * FUSE: Filesystem in Userspace
> >> + * Copyright (C) 2001-2007 Miklos Szeredi <miklos@szeredi.hu>
> >> + * 2026 Bernd Schubert <bernd@bsbernd.com>
> >> + *
> >> + * New Linux mount API (fsopen/fsconfig/fsmount/move_mount) support.
> >> + *
> >> + * This program can be distributed under the terms of the GNU LGPLv2.
> >> + * See the file LGPL2.txt.
> >> + */
> >> +
> >> +#define _GNU_SOURCE
> >> +
> >> +#include "fuse_config.h"
> >> +#include "fuse_misc.h"
> >> +#include "mount_util.h"
> >> +#include "mount_i_linux.h"
> >> +
> >> +#include <stdio.h>
> >> +#include <stdlib.h>
> >> +#include <unistd.h>
> >> +#include <string.h>
> >> +#include <fcntl.h>
> >> +#include <errno.h>
> >> +#include <sys/mount.h>
> >> +#include <sys/syscall.h>
> >> +
> >> +/* Mount attribute flags for fsmount() - from linux/mount.h */
> >> +#ifndef MOUNT_ATTR_RDONLY
> >> +#define MOUNT_ATTR_RDONLY 0x00000001
> >> +#endif
> >> +#ifndef MOUNT_ATTR_NOSUID
> >> +#define MOUNT_ATTR_NOSUID 0x00000002
> >> +#endif
> >> +#ifndef MOUNT_ATTR_NODEV
> >> +#define MOUNT_ATTR_NODEV 0x00000004
> >> +#endif
> >> +#ifndef MOUNT_ATTR_NOEXEC
> >> +#define MOUNT_ATTR_NOEXEC 0x00000008
> >> +#endif
> >> +#ifndef MOUNT_ATTR__ATIME
> >> +#define MOUNT_ATTR__ATIME 0x00000070
> >> +#endif
> >> +#ifndef MOUNT_ATTR_RELATIME
> >> +#define MOUNT_ATTR_RELATIME 0x00000000
> >> +#endif
> >> +#ifndef MOUNT_ATTR_NOATIME
> >> +#define MOUNT_ATTR_NOATIME 0x00000010
> >> +#endif
> >> +#ifndef MOUNT_ATTR_STRICTATIME
> >> +#define MOUNT_ATTR_STRICTATIME 0x00000020
> >> +#endif
> >> +#ifndef MOUNT_ATTR_NODIRATIME
> >> +#define MOUNT_ATTR_NODIRATIME 0x00000080
> >> +#endif
> >> +#ifndef MOUNT_ATTR_NOSYMFOLLOW
> >> +#define MOUNT_ATTR_NOSYMFOLLOW 0x00200000
> >> +#endif
> >
> > /me notes that all MOUNT_ATTR_ before NOSYMFOLLOW were introduced in the
> > initial commit so if the meson test thinks the new fsmount api is
> > present then you can rely on flags being defined.
>
> Thanks, removed those flags and added a comment.
>
> >
> >> +
> >> +/*
> >> + * Convert MS_* mount flags to MOUNT_ATTR_* mount attributes.
> >> + * These flags are passed to fsmount(), not fsconfig().
> >> + * Mount attributes control mount-point level behavior.
> >> + */
> >> +static unsigned long ms_flags_to_mount_attrs(unsigned long flags)
> >> +{
> >> + unsigned long attrs = 0;
> >> +
> >> + if (flags & MS_NOSUID)
> >> + attrs |= MOUNT_ATTR_NOSUID;
> >> + if (flags & MS_NODEV)
> >> + attrs |= MOUNT_ATTR_NODEV;
> >> + if (flags & MS_NOEXEC)
> >> + attrs |= MOUNT_ATTR_NOEXEC;
> >> + if (flags & MS_NOATIME)
> >> + attrs |= MOUNT_ATTR_NOATIME;
> >> + else if (flags & MS_RELATIME)
> >> + attrs |= MOUNT_ATTR_RELATIME;
> >> + else if (flags & MS_STRICTATIME)
> >> + attrs |= MOUNT_ATTR_STRICTATIME;
> >> + if (flags & MS_NODIRATIME)
> >> + attrs |= MOUNT_ATTR_NODIRATIME;
> >> + if (flags & MS_NOSYMFOLLOW)
> >> + attrs |= MOUNT_ATTR_NOSYMFOLLOW;
> >> +
> >> + return attrs;
> >> +}
> >> +
> >> +/*
> >> + * Apply VFS superblock flags to the filesystem context.
> >> + * Only handles flags that are filesystem parameters (ro, sync, dirsync).
> >> + * Mount attributes (nosuid, nodev, etc.) are handled separately via fsmount().
> >> + */
> >> +static int apply_mount_flags(int fsfd, unsigned long flags)
> >> +{
> >> + int res;
> >> +
> >> + /* Handle read-only flag */
> >> + if (flags & MS_RDONLY) {
> >> + res = fsconfig(fsfd, FSCONFIG_SET_FLAG, "ro", NULL, 0);
> >> + if (res == -1) {
> >> + fprintf(stderr,
> >> + "fuse: fsconfig SET_FLAG ro failed: %s\n",
> >> + strerror(errno));
> >> + return -errno;
> >> + }
> >> + }
> >> +
> >> + /* Handle sync flag */
> >> + if (flags & MS_SYNCHRONOUS) {
> >> + res = fsconfig(fsfd, FSCONFIG_SET_FLAG, "sync", NULL, 0);
> >> + if (res == -1) {
> >> + fprintf(stderr,
> >> + "fuse: fsconfig SET_FLAG sync failed: %s\n",
> >> + strerror(errno));
> >> + return -errno;
> >> + }
> >> + }
> >> +
> >> +#ifndef __NetBSD__
> >> + /* Handle dirsync flag */
> >> + if (flags & MS_DIRSYNC) {
> >> + res = fsconfig(fsfd, FSCONFIG_SET_FLAG, "dirsync", NULL, 0);
> >> + if (res == -1) {
> >> + fprintf(stderr,
> >> + "fuse: fsconfig SET_FLAG dirsync failed: %s\n",
> >> + strerror(errno));
> >> + return -errno;
> >> + }
> >> + }
> >> +#endif
> >> +
> >> + return 0;
> >> +}
> >
> > Oh, nice treatment of the MS_ flags that don't have a corresponding
> > MOUNT_ATTR_ flag. I should incorporate that into mount_service.c.
> >
> >> +
> >> +static int apply_opt_fd(int fsfd, const char *value)
> >> +{
> >> + int res;
> >> +
> >> + /* The fd parameter is a u32 value, not a file descriptor to pass */
> >> + res = fsconfig(fsfd, FSCONFIG_SET_STRING, "fd", value, 0);
> >> + if (res == -1) {
> >> + fprintf(stderr, "fuse: fsconfig SET_STRING fd=%s failed: %s\n",
> >> + value, strerror(errno));
> >> + return -errno;
> >
> > Did you know that you can read the kernel's error messages from the
> > fsopen fd?
> >
> > https://git.kernel.org/pub/scm/linux/kernel/git/djwong/libfuse.git/tree/util/mount_service.c?h=fuse-service-container&id=d02bbab680ab4689bbd5e274735e91bd38e5f47f#n693
>
> Ah nice, I kind of copied over this function now.
:)
> >
> >> + }
> >> + return 0;
> >> +}
> >> +
> >> +static int apply_opt_string(int fsfd, const char *key, const char *value)
> >> +{
> >> + int res;
> >> +
> >> + res = fsconfig(fsfd, FSCONFIG_SET_STRING, key, value, 0);
> >> + if (res == -1) {
> >> + fprintf(stderr,
> >> + "fuse: fsconfig SET_STRING %s=%s failed: %s\n",
> >> + key, value, strerror(errno));
> >> + return -errno;
> >
> > I think you have to save errno explicitly here, because fprintf and
> > strerror could fail and set errno to something else. For example, if
> > fsconfig returns EINVAL but stderr points to a file on a completely full
> > filesystem, the ENOSPC from the fprintf call (or more likely the write()
> > underneath it) will obliterate the EINVAL.
>
> Absolutely, had slipped through. Thank you!
(I hate C error handling so much...)
> >
> >> + }
> >> + return 0;
> >> +}
> >> +
> >> +static int apply_opt_flag(int fsfd, const char *opt)
> >> +{
> >> + int res;
> >> +
> >> + res = fsconfig(fsfd, FSCONFIG_SET_FLAG, opt, NULL, 0);
> >> + if (res == -1) {
> >> + fprintf(stderr, "fuse: fsconfig SET_FLAG %s failed: %s\n",
> >> + opt, strerror(errno));
> >> + return -errno;
> >> + }
> >> + return 0;
> >> +}
> >> +
> >> +static int apply_opt_key_value(int fsfd, char *opt)
> >> +{
> >> + char *eq;
> >> + const char *key;
> >> + const char *value;
> >> +
> >> + eq = strchr(opt, '=');
> >> + if (!eq)
> >> + return apply_opt_flag(fsfd, opt);
> >> +
> >> + *eq = '\0';
> >> + key = opt;
> >> + value = eq + 1;
> >> +
> >> + if (strcmp(key, "fd") == 0)
> >> + return apply_opt_fd(fsfd, value);
> >> +
> >> + return apply_opt_string(fsfd, key, value);
> >> +}
> >> +
> >> +/**
> >> + * Check if an option is a mount attribute (handled by fsmount, not fsconfig)
> >> + */
> >> +static int is_mount_attr_opt(const char *opt)
> >> +{
> >> + /* These options are mount attributes passed to fsmount(), not fsconfig() */
> >> + return strcmp(opt, "nosuid") == 0 ||
> >> + strcmp(opt, "suid") == 0 ||
> >> + strcmp(opt, "nodev") == 0 ||
> >> + strcmp(opt, "dev") == 0 ||
> >> + strcmp(opt, "noexec") == 0 ||
> >> + strcmp(opt, "exec") == 0 ||
> >> + strcmp(opt, "noatime") == 0 ||
> >> + strcmp(opt, "atime") == 0 ||
> >> + strcmp(opt, "nodiratime") == 0 ||
> >> + strcmp(opt, "diratime") == 0 ||
> >> + strcmp(opt, "relatime") == 0 ||
> >> + strcmp(opt, "norelatime") == 0 ||
> >> + strcmp(opt, "strictatime") == 0 ||
> >> + strcmp(opt, "nostrictatime") == 0 ||
> >> + strcmp(opt, "nosymfollow") == 0 ||
> >> + strcmp(opt, "symfollow") == 0;
> >> +}
> >> +
> >> +/*
> >> + * Parse kernel options string and apply via fsconfig
> >> + * Options are comma-separated key=value pairs
> >> + */
> >> +static int apply_mount_opts(int fsfd, const char *opts)
> >> +{
> >> + char *opts_copy;
> >> + char *opt;
> >> + char *saveptr;
> >> + int res;
> >> +
> >> + if (!opts || !*opts)
> >> + return 0;
> >> +
> >> + opts_copy = strdup(opts);
> >> + if (!opts_copy) {
> >> + fprintf(stderr, "fuse: failed to allocate memory\n");
> >> + return -ENOMEM;
> >> + }
> >> +
> >> + opt = strtok_r(opts_copy, ",", &saveptr);
> >> + while (opt) {
> >> + /* Skip mount attributes - they're handled by fsmount(), not fsconfig() */
> >> + if (!is_mount_attr_opt(opt)) {
> >> + res = apply_opt_key_value(fsfd, opt);
> >
> > Does fuse_session_new_versioned convert those magic string flags from
> > is_mount_attr_opt into their MS_* equivalents such that apply_mount_opts
> > won't see the string versions?
>
> Comment updated to
>
> /*
> * Skip mount attributes, they're handled by fsmount()
> * not fsconfig().
> *
> * These string options (nosuid, nodev, etc.) are reconstructed
> * from MS_* flags by get_mnt_flag_opts() in lib/mount.c and
> * get_mnt_opts() in util/fusermount.c. Both the library path
> * (via fuse_kern_mount_get_base_mnt_opts) and fusermount3 path
> * rebuild these strings from the flags bitmask and pass them in
> * mnt_opts. They must be filtered here because they are mount
> * attributes (passed to fsmount via MOUNT_ATTR_*), not
> * filesystem parameters (which would be passed to fsconfig).
> */
> if (!is_mount_attr_opt(opt)) {
That clears things right up, thanks!
>
> >
> >> + if (res < 0) {
> >> + free(opts_copy);
> >> + return res;
> >> + }
> >> + }
> >> + opt = strtok_r(NULL, ",", &saveptr);
> >> + }
> >> +
> >> + free(opts_copy);
> >> + return 0;
> >> +}
> >> +
> >> +
> >> +/**
> >> + * Mount using the new Linux mount API (fsopen/fsconfig/fsmount/move_mount)
> >> + * @mnt: mountpoint
> >> + * @flags: mount flags (MS_NOSUID, MS_NODEV, etc.)
> >> + * @blkdev: 1 for fuseblk, 0 for fuse
> >> + * @fsname: filesystem name (or NULL)
> >> + * @subtype: filesystem subtype (or NULL)
> >> + * @source_dev: device name for building source string
> >> + * @kernel_opts: kernel mount options string
> >> + * @mnt_opts: additional mount options to pass to the kernel
> >> + *
> >> + * Returns: 0 on success, -1 on failure with errno set
> >> + */
> >> +int fuse_kern_fsmount(const char *mnt, unsigned long flags, int blkdev,
> >> + const char *fsname, const char *subtype,
> >> + const char *source_dev, const char *kernel_opts,
> >> + const char *mnt_opts)
> >> +{
> >> + const char *type;
> >> + char *source = NULL;
> >> + int fsfd = -1;
> >> + int mntfd = -1;
> >> + int err, res;
> >> + unsigned long mount_attrs;
> >> +
> >> + /* Determine filesystem type */
> >> + type = blkdev ? "fuseblk" : "fuse";
> >> +
> >> + /* Try to open filesystem context */
> >> + fsfd = fsopen(type, FSOPEN_CLOEXEC);
> >> + if (fsfd == -1) {
> >> + fprintf(stderr, "fuse: fsopen(%s) failed: %s\n", type,
> >> + strerror(errno));
> >> + return -1;
> >> + }
> >> +
> >> + /* Build source string */
> >> + source = malloc((fsname ? strlen(fsname) : 0) +
> >> + (subtype ? strlen(subtype) : 0) +
> >> + strlen(source_dev) + 32);
> >> + err = -ENOMEM;
> >> + if (!source) {
> >> + fprintf(stderr, "fuse: failed to allocate memory\n");
> >> + goto out_close_fsfd;
> >> + }
> >> +
> >> + strcpy(source, fsname ? fsname : (subtype ? subtype : source_dev));
> >
> > This is almost fuse_mnt_build_source()
>
> Yeah, we don't have struct mount_opts that fuse_mnt_build_source() takes.
> Going to improve that in a later patch.
<nod> I reworked my patchset to pull in some of your hoisted helpers so
that rebasing my patchset won't be quite so annoying later. Will look
forward to whatever form that takes.
--D
>
> Thanks,
> Bernd
>
^ permalink raw reply [flat|nested] 59+ messages in thread
* Re: [PATCH 12/19] fuse mount: Support synchronous FUSE_INIT (privileged daemon)
2026-03-24 20:42 ` Bernd Schubert
@ 2026-03-24 22:50 ` Darrick J. Wong
2026-03-25 7:52 ` Bernd Schubert
0 siblings, 1 reply; 59+ messages in thread
From: Darrick J. Wong @ 2026-03-24 22:50 UTC (permalink / raw)
To: Bernd Schubert
Cc: Bernd Schubert, linux-fsdevel@vger.kernel.org, Miklos Szeredi,
Joanne Koong, Kevin Chen
On Tue, Mar 24, 2026 at 08:42:36PM +0000, Bernd Schubert wrote:
> On 3/24/26 01:03, Darrick J. Wong wrote:
> > On Mon, Mar 23, 2026 at 06:45:07PM +0100, Bernd Schubert wrote:
> >> From: Bernd Schubert <bschubert@ddn.com>
> >>
> >> Add synchronous FUSE_INIT processing during mount() to
> >> enable early daemonization with proper error reporting
> >> to the parent process.
> >>
> >> A new mount thread is needed that handles FUSE_INIT and
> >> possible other requests at mount time (like getxattr for selinux).
> >> The kernel sends FUSE_INIT during the mount() syscall. Without a thread
> >> to process it, mount() blocks forever.
> >>
> >> Mount thread lifetime:
> >> Created before mount() syscall in fuse_start_sync_init_worker()
> >> Processes requests until se->mount_finished is set (after mount() returns)
> >> Joined after successful mount in fuse_wait_sync_init_completion()
> >> Cancelled if mount fails (direct → fusermount3 fallback)
> >
> > Hrmm, so the main thread (of the child process after daemonization)
> > calls mount(), having started a child thread to read and process
> > FUSE_INIT + any other mount-related fuse requests?
> >
> > Later I see an eventfd being used to signal this background thread that
> > it should exit and (presumably) let the real request processing queues
> > take over. I'm curious why an eventfd here when pipes were used
> > earlier?
>
> We cannot use eventfds for anything possibly used by BSD - which is
(Yeah, I was wondering about eventfd, but I don't know how quickly
libfuse features get ported to BSD so for all I know it could be years
until BSD gets SYNC_INIT.)
> fuse_daemonize_ and fusermount. So far sync-init is only in the new
> mount API - linux only. We need to switch to a pipe if BSD ever gets that.
I wonder, are the socket communications between fusermount-libfuse
considered external interface? I'm assuming it's not, and you can
change it as long as you don't break the other side?
> >
> >> Key changes:
> >>
> >> Add init_thread, init_error, mount_finished to struct fuse_session
> >> Use FUSE_DEV_IOC_SYNC_INIT ioctl for kernel support
> >> Fall back to async FUSE_INIT if unsupported
> >> Auto-enabled when fuse_daemonize_active() or via
> >> fuse_session_want_sync_init()
> >> Allows parent to report mount/init failures instead of
> >> exiting immediately after fork.
> >>
> >> Note: For now synchronous FUSE_INIT is only supported for privileged
> >> mounts.
> >>
> >> Signed-off-by: Bernd Schubert <bschubert@ddn.com>
> >> ---
> >> include/fuse_lowlevel.h | 12 +++
> >> lib/fuse_daemonize.c | 8 ++
> >> lib/fuse_i.h | 16 ++++
> >> lib/fuse_lowlevel.c | 192 ++++++++++++++++++++++++++++++++++++++++++++++--
> >> lib/mount.c | 5 +-
> >> lib/mount_fsmount.c | 5 +-
> >> 6 files changed, 229 insertions(+), 9 deletions(-)
> >>
> >> diff --git a/include/fuse_lowlevel.h b/include/fuse_lowlevel.h
> >> index ee0bd8d71d95e4d57ebb4873dca0f2b36e22a649..d8626f85bdaf497534cd2835a589e30f1f4e2466 100644
> >> --- a/include/fuse_lowlevel.h
> >> +++ b/include/fuse_lowlevel.h
> >> @@ -2429,6 +2429,18 @@ void fuse_session_process_buf(struct fuse_session *se,
> >> */
> >> int fuse_session_receive_buf(struct fuse_session *se, struct fuse_buf *buf);
> >>
> >> +/**
> >> + * Request synchronous FUSE_INIT, i.e. FUSE_INIT is handled by the
> >> + * kernel before mount is returned.
> >> + *
> >> + * As FUSE_INIT also starts io-uring ring threads, fork() must not be
> >> + * called after this if io-uring is enabled. Also see
> >> + * fuse_session_daemonize_start().
> >> + *
> >> + * This must be called before fuse_session_mount() to have any effect.
> >> + */
> >> +void fuse_session_want_sync_init(struct fuse_session *se);
> >> +
> >> /**
> >> * Check if the request is submitted through fuse-io-uring
> >> */
> >> diff --git a/lib/fuse_daemonize.c b/lib/fuse_daemonize.c
> >> index 5d191e7d737d04876d02ef6bd526061c003d2ab7..1a32ef74093f231091b4f541e5b9136bff72024f 100644
> >> --- a/lib/fuse_daemonize.c
> >> +++ b/lib/fuse_daemonize.c
> >> @@ -9,6 +9,7 @@
> >> #define _GNU_SOURCE
> >>
> >> #include "fuse_daemonize.h"
> >> +#include "fuse_i.h"
> >>
> >> #include <fcntl.h>
> >> #include <poll.h>
> >> @@ -282,3 +283,10 @@ bool fuse_daemonize_active(void)
> >>
> >> return dm != NULL && (dm->daemonized || dm->active);
> >> }
> >> +
> >> +bool fuse_daemonize_set(void)
> >> +{
> >> + struct fuse_daemonize *dm = atomic_load(&daemonize);
> >> +
> >> + return dm != NULL;
> >> +}
> >> diff --git a/lib/fuse_i.h b/lib/fuse_i.h
> >> index 6d63c9fd2149eb4ae3b0e0170640a4ce2eed4705..93ab7ac2fadf9395af70487c7626cc57c2948d56 100644
> >> --- a/lib/fuse_i.h
> >> +++ b/lib/fuse_i.h
> >> @@ -112,6 +112,10 @@ struct fuse_session {
> >>
> >> /* synchronous FUSE_INIT support */
> >> bool want_sync_init;
> >> + pthread_t init_thread;
> >> + int init_error;
> >> + _Atomic bool terminate_mount_worker;
> >> + int init_wakeup_fd;
> >>
> >> /* io_uring */
> >> struct fuse_session_uring uring;
> >> @@ -221,7 +225,11 @@ void fuse_chan_put(struct fuse_chan *ch);
> >> /* Mount-related functions */
> >> void fuse_mount_version(void);
> >> void fuse_kern_unmount(const char *mountpoint, int fd);
> >> +int fuse_kern_mount_get_base_mnt_opts(struct mount_opts *mo, char **mnt_optsp);
> >> int fuse_kern_mount(const char *mountpoint, struct mount_opts *mo);
> >> +int fuse_kern_mount_prepare(const char *mountpoint, struct mount_opts *mo);
> >> +int fuse_kern_do_mount(const char *mountpoint, struct mount_opts *mo,
> >> + const char *mnt_opts);
> >>
> >> int fuse_send_reply_iov_nofree(fuse_req_t req, int error, struct iovec *iov,
> >> int count);
> >> @@ -255,6 +263,14 @@ int fuse_session_loop_mt_312(struct fuse_session *se, struct fuse_loop_config *c
> >> */
> >> int fuse_loop_cfg_verify(struct fuse_loop_config *config);
> >>
> >> +/**
> >> + * Check if daemonization is set.
> >> + *
> >> + * @return true if set, false otherwise
> >> + */
> >> +bool fuse_daemonize_set(void);
> >> +
> >> +
> >>
> >> /*
> >> * This can be changed dynamically on recent kernels through the
> >> diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c
> >> index 626233df20f49fa89cd9327f94340169d7061f75..b10def03f3666757d312f87f177a560483691d6f 100644
> >> --- a/lib/fuse_lowlevel.c
> >> +++ b/lib/fuse_lowlevel.c
> >> @@ -4230,6 +4230,7 @@ fuse_session_new_versioned(struct fuse_args *args,
> >> goto out1;
> >> }
> >> se->fd = -1;
> >> + se->init_wakeup_fd = -1;
> >> se->conn.max_write = FUSE_DEFAULT_MAX_PAGES_LIMIT * getpagesize();
> >> se->bufsize = se->conn.max_write + FUSE_BUFFER_HEADER_SIZE;
> >> se->conn.max_readahead = UINT_MAX;
> >> @@ -4402,6 +4403,167 @@ int fuse_session_custom_io_30(struct fuse_session *se,
> >> }
> >>
> >> #if defined(HAVE_NEW_MOUNT_API)
> >> +
> >> +/* Worker thread for synchronous FUSE_INIT */
> >> +static void *session_sync_init_worker(void *data)
> >> +{
> >> + struct fuse_session *se = (struct fuse_session *)data;
> >> + struct fuse_buf fbuf = {
> >> + .mem = NULL,
> >> + };
> >> + struct pollfd pfds[2];
> >> + int res;
> >> +
> >> + pfds[0].fd = se->fd;
> >> + pfds[0].events = POLLIN;
> >> + pfds[0].revents = 0;
> >> + pfds[1].fd = se->init_wakeup_fd;
> >> + pfds[1].events = POLLIN;
> >> + pfds[1].revents = 0;
> >> +
> >> + /*
> >> + * Process requests until mount completes. With SELinux there may be
> >> + * additional requests (like getattr) after FUSE_INIT before mount
> >> + * returns.
> >> + */
> >> + while (!atomic_load(&se->terminate_mount_worker)) {
> >
> > We don't ever *read* the init_wakeup_fd event, so I wonder if the
> > _Atomic flag here is really needed? I gather we need acquire semantics
> > or something?
>
> My lazyness. I had quickly updated the code yesterday morning to use
> poll and eventfd, because Kevin noticed issues with xfstests. Before it
> was a regular thread stopped with pthread_cancel and then actually
> terminated the thread when it already might have fetched a post-mount
> request - the request then got never handled.
>
> Next on my list is to look into the fuse-io-uring issues that Kevin also
> reports with this patch series.
Heh, well, at least it isn't pthread_cancel anymore :)
> >
> >> + res = poll(pfds, 2, -1);
> >> + if (res == -1) {
> >> + if (errno == EINTR)
> >> + continue;
> >> + se->init_error = -errno;
> >> + break;
> >> + }
> >> +
> >> + if (pfds[1].revents & POLLIN)
> >> + break;
> >> +
> >> + if (pfds[0].revents & POLLIN) {
> >> + res = fuse_session_receive_buf_internal(se, &fbuf, NULL);
> >> + if (res == -EINTR)
> >> + continue;
> >> + if (res <= 0) {
> >> + se->init_error = res < 0 ? res : -EINVAL;
> >> + break;
> >> + }
> >> +
> >> + fuse_session_process_buf_internal(se, &fbuf, NULL);
> >> + }
> >> + }
> >> +
> >> + fuse_buf_free(&fbuf);
> >> + return NULL;
> >> +}
> >> +
> >> +/* Enable synchronous FUSE_INIT and start worker thread */
> >> +static int session_start_sync_init(struct fuse_session *se, int fd)
> >> +{
> >> + int err;
> >> + int res;
> >> +
> >> + if (!se->want_sync_init &&
> >> + (se->uring.enable && !fuse_daemonize_set())) {
> >> + if (se->debug)
> >> + fuse_log(FUSE_LOG_DEBUG,
> >> + "fuse: sync init not enabled\n");
> >> + return 0;
> >> + }
> >> +
> >> + /* Try to enable synchronous FUSE_INIT */
> >> + res = ioctl(fd, FUSE_DEV_IOC_SYNC_INIT);
> >> + if (res) {
> >> + /* ENOTTY means kernel doesn't support sync init - not an error */
> >> + if (errno != ENOTTY) {
> >> + fuse_log(
> >> + FUSE_LOG_ERR,
> >> + "fuse: failed to enable sync init: %s\n",
> >> + strerror(errno));
> >> + } else if (se->debug) {
> >> + fuse_log(
> >> + FUSE_LOG_DEBUG,
> >> + "fuse: kernel doesn't support sync init\n");
> >> + }
> >> + return -ENOTTY;
> >> + }
> >> +
> >> + if (se->debug)
> >> + fuse_log(FUSE_LOG_DEBUG,
> >> + "fuse: synchronous FUSE_INIT enabled\n");
> >> +
> >> + se->init_error = 0;
> >> + se->terminate_mount_worker = false;
> >> +
> >> + se->init_wakeup_fd = eventfd(0, EFD_CLOEXEC);
> >> + if (se->init_wakeup_fd == -1) {
> >> + fuse_log(
> >> + FUSE_LOG_ERR,
> >> + "fuse: failed to create eventfd for init worker: %s\n",
> >> + strerror(errno));
> >> + return -EIO;
> >> + }
> >> +
> >> + err = pthread_create(&se->init_thread, NULL,
> >> + session_sync_init_worker, se);
> >> + if (err != 0) {
> >> + fuse_log(
> >> + FUSE_LOG_ERR,
> >> + "fuse: failed to create init worker thread: %s\n",
> >> + strerror(err));
> >> + close(se->init_wakeup_fd);
> >> + se->init_wakeup_fd = -1;
> >> + return -EIO;
> >> + }
> >> +
> >> + return 0;
> >> +}
> >> +
> >> +/* Wait for synchronous FUSE_INIT to complete */
> >> +static int session_wait_sync_init_completion(struct fuse_session *se)
> >> +{
> >> + void *retval;
> >> + int err;
> >> + uint64_t val = 1;
> >> +
> >> + if (se->init_thread == 0)
> >
> > pthread_t is supposed to be an opaque datatype, so "0" could be a valid
> > value for a running thread. For that matter, it could be a struct. I
> > think you have to have a separate flag (or just use init_wakeup_fd >= 0)
> > to determine if we're doing a synchronous init.
>
> Ah right. I'm using se->init_wakeup_fd as you suggested.
<nod>
> >
> >> + return 0;
> >> +
> >> + se->terminate_mount_worker = true;
> >> +
> >> + if (se->init_wakeup_fd != -1) {
> >> + ssize_t res = write(se->init_wakeup_fd, &val, sizeof(val));
> >> +
> >> + if (res != sizeof(val)) {
> >> + fuse_log(FUSE_LOG_ERR,
> >> + "fuse: failed to signal init worker: %s\n",
> >> + strerror(errno));
> >> + }
> >> + }
> >> +
> >> + err = pthread_join(se->init_thread, &retval);
> >> + if (err != 0) {
> >> + fuse_log(FUSE_LOG_ERR, "fuse: failed to join init worker thread: %s\n",
> >> + strerror(err));
> >> + return -1;
> >> + }
> >> +
> >> + if (se->init_wakeup_fd != -1) {
> >> + close(se->init_wakeup_fd);
> >> + se->init_wakeup_fd = -1;
> >> + }
> >> +
> >> + if (se->init_error != 0) {
> >> + fuse_log(FUSE_LOG_ERR, "fuse: init worker failed: %d\n", se->init_error);
> >
> > Could you strerror(-se->init_error) to give the user a real error
> > message?
> >
> >> + return -1;
> >> + }
> >> +
> >> + if (fuse_session_exited(se)) {
> >> + fuse_log(FUSE_LOG_ERR, "FUSE_INIT failed: session exited\n");
> >> + return -1;
> >> + }
> >> +
> >> + return 0;
> >> +}
> >> +
> >> /* Only linux supports sync FUSE_INIT so far */
> >> static int fuse_session_mount_new_api(struct fuse_session *se,
> >> const char *mountpoint)
> >> @@ -4414,8 +4576,7 @@ static int fuse_session_mount_new_api(struct fuse_session *se,
> >>
> >> res = fuse_kern_mount_get_base_mnt_opts(se->mo, &mnt_opts);
> >> if (res == -1) {
> >> - fuse_log(FUSE_LOG_ERR,
> >> - "fuse: failed to get base mount options\n");
> >> + fuse_log(FUSE_LOG_ERR, "fuse: failed to get base mount options\n");
> >
> > Unrelated change?
>
> Removed here.
>
> >
> >> err = -EIO;
> >> goto err;
> >> }
> >> @@ -4427,6 +4588,17 @@ static int fuse_session_mount_new_api(struct fuse_session *se,
> >> goto err;
> >> }
> >>
> >> + /*
> >> + * Enable synchronous FUSE_INIT and start worker thread, sync init
> >> + * failure is not an error
> >> + */
> >> + se->fd = fd;
> >> + err = session_start_sync_init(se, fd);
> >> + if (err) {
> >> + /* ENOTTY means kernel doesn't support sync init - not an error */
> >> + if (err != -ENOTTY)
> >> + goto err;
> >> + }
> >
> > Perhaps session_start_sync_init should not return ENOTTY, since you
> > already check in session_wait_sync_init_completion if the synchronous
> > init worker has been started?
>
> Good idea, done.
>
> >
> >> snprintf(fd_opt, sizeof(fd_opt), "fd=%i", fd);
> >> if (fuse_opt_add_opt(&mnt_opts_with_fd, mnt_opts) == -1 ||
> >> fuse_opt_add_opt(&mnt_opts_with_fd, fd_opt) == -1) {
> >> @@ -4436,13 +4608,16 @@ static int fuse_session_mount_new_api(struct fuse_session *se,
> >>
> >> err = fuse_kern_fsmount_mo(mountpoint, se->mo, mnt_opts_with_fd);
> >> err:
> >> - if (err) {
> >> + if (err < 0) {
> >> if (fd >= 0)
> >> close(fd);
> >> fd = -1;
> >> se->fd = -1;
> >> se->error = -errno;
> >> }
> >> + /* Wait for synchronous FUSE_INIT to complete */
> >> + if (session_wait_sync_init_completion(se) < 0)
> >> + fuse_log(FUSE_LOG_ERR, "fuse: sync init completion failed\n");
> >>
> >> free(mnt_opts);
> >> free(mnt_opts_with_fd);
> >> @@ -4452,8 +4627,8 @@ err:
> >> static int fuse_session_mount_new_api(struct fuse_session *se,
> >> const char *mountpoint)
> >> {
> >> - (void)se;
> >> - (void)mountpoint;
> >> + (void) se;
> >> + (void) mountpoint;
> >>
> >> return -1;
> >> }
> >> @@ -4825,3 +5000,10 @@ void fuse_session_stop_teardown_watchdog(void *data)
> >> pthread_join(tt->thread_id, NULL);
> >> fuse_tt_destruct(tt);
> >> }
> >> +
> >> +void fuse_session_want_sync_init(struct fuse_session *se)
> >> +{
> >> + if (se == NULL)
> >> + return;
> >> + se->want_sync_init = true;
> >> +}
> >> diff --git a/lib/mount.c b/lib/mount.c
> >> index 30fd4d2f9bbb84c817b2363b2075456acd1c1255..12df49d9109cf918cc41aa75c5fdf84231d4d5ff 100644
> >> --- a/lib/mount.c
> >> +++ b/lib/mount.c
> >> @@ -30,6 +30,7 @@
> >> #include <sys/socket.h>
> >> #include <sys/un.h>
> >> #include <sys/wait.h>
> >> +#include <sys/ioctl.h>
> >>
> >> #include "fuse_mount_compat.h"
> >>
> >> @@ -529,8 +530,8 @@ int fuse_kern_fsmount_mo(const char *mnt, struct mount_opts *mo,
> >> * Returns: 0 on success, -1 on failure,
> >> * FUSE_MOUNT_FALLBACK_NEEDED if fusermount should be used
> >> */
> >> -static int fuse_kern_do_mount(const char *mnt, struct mount_opts *mo,
> >> - const char *mnt_opts)
> >> +int fuse_kern_do_mount(const char *mnt, struct mount_opts *mo,
> >> + const char *mnt_opts)
> >> {
> >> char *source = NULL;
> >> char *type = NULL;
> >> diff --git a/lib/mount_fsmount.c b/lib/mount_fsmount.c
> >> index cba998bc60c783a5edc0c16570f7e5512b7f1253..f1fec790bb80f8815d485a068dc7efdff1746309 100644
> >> --- a/lib/mount_fsmount.c
> >> +++ b/lib/mount_fsmount.c
> >> @@ -287,8 +287,9 @@ int fuse_kern_fsmount(const char *mnt, unsigned long flags, int blkdev,
> >> /* Try to open filesystem context */
> >> fsfd = fsopen(type, FSOPEN_CLOEXEC);
> >> if (fsfd == -1) {
> >> - fprintf(stderr, "fuse: fsopen(%s) failed: %s\n", type,
> >> - strerror(errno));
> >> + if (errno != EPERM)
> >> + fprintf(stderr, "fuse: fsopen(%s) failed: %s\n", type,
> >> + strerror(errno));
> >
> > More unrelated changes?
>
> Right, moved one patch up.
<nod>
--D
^ permalink raw reply [flat|nested] 59+ messages in thread
* Re: [PATCH 15/19] Split the fusermount do_mount function
2026-03-24 21:05 ` Bernd Schubert
@ 2026-03-24 22:53 ` Darrick J. Wong
0 siblings, 0 replies; 59+ messages in thread
From: Darrick J. Wong @ 2026-03-24 22:53 UTC (permalink / raw)
To: Bernd Schubert
Cc: linux-fsdevel, Miklos Szeredi, Joanne Koong, Bernd Schubert
On Tue, Mar 24, 2026 at 10:05:13PM +0100, Bernd Schubert wrote:
>
>
> On 3/24/26 01:14, Darrick J. Wong wrote:
> > On Mon, Mar 23, 2026 at 06:45:10PM +0100, Bernd Schubert wrote:
> >> From: Bernd Schubert <bschubert@ddn.com>
> >>
> >> We will need new API and old API and need to pass the options to
> >> two different functions - factor out the option parsing.
> >>
> >> Signed-off-by: Bernd Schubert <bschubert@ddn.com>
> >> ---
> >> util/fusermount.c | 298 +++++++++++++++++++++++++++++++++++++-----------------
> >> 1 file changed, 205 insertions(+), 93 deletions(-)
> >>
> >> diff --git a/util/fusermount.c b/util/fusermount.c
> >> index f17b44f51142c682b339d0ce2287f7c00d644454..ecf509bb80d5cd129f6e582f1ec666502c55603a 100644
> >> --- a/util/fusermount.c
> >> +++ b/util/fusermount.c
> >> @@ -917,30 +917,132 @@ static int mount_notrunc(const char *source, const char *target,
> >> return mount(source, target, filesystemtype, mountflags, data);
> >> }
> >>
> >> +struct mount_params {
> >> + /* Input parameters */
> >> + int fd; /* /dev/fuse file descriptor */
> >> + mode_t rootmode; /* Root mode from stat */
> >> + const char *dev; /* Device path (/dev/fuse) */
> >>
> >> -static int do_mount(const char *mnt, const char **typep, mode_t rootmode,
> >> - int fd, const char *opts, const char *dev, char **sourcep,
> >> - char **mnt_optsp)
> >> + /* Parsed mount options */
> >> + unsigned long flags; /* Mount flags (MS_NOSUID, etc.) */
> >> + char *optbuf; /* Kernel mount options buffer */
> >> + char *fsname; /* Filesystem name from options */
> >> + char *subtype; /* Subtype from options */
> >> + int blkdev; /* Block device flag */
> >> +
> >> + /* Generated mount parameters */
> >> + char *source; /* Mount source string */
> >> + char *type; /* Filesystem type string */
> >> + char *mnt_opts; /* Mount table options */
> >> +
> >> + /* Pointer for optbuf manipulation */
> >> + char *optbuf_end; /* Points to end of optbuf for sprintf */
> >> +};
> >> +
> >> +static void free_mount_params(struct mount_params *mp)
> >> +{
> >> + free(mp->optbuf);
> >> + free(mp->fsname);
> >> + free(mp->subtype);
> >> + free(mp->source);
> >> + free(mp->type);
> >> + free(mp->mnt_opts);
> >> +}
> >> +
> >> +/*
> >> + * Check if option is deprecated large_read.
> >> + *
> >> + * Returns true if the option should be skipped (large_read on kernel > 2.4),
> >> + * false otherwise (all other options or large_read on old kernels).
> >> + */
> >> +static bool check_large_read(const char *opt, unsigned int len)
> >> +{
> >> + struct utsname utsname;
> >> + unsigned int kmaj, kmin;
> >> + int res;
> >> +
> >> + if (!opt_eq(opt, len, "large_read"))
> >> + return false;
> >> +
> >> + res = uname(&utsname);
> >> + if (res == 0 &&
> >> + sscanf(utsname.release, "%u.%u", &kmaj, &kmin) == 2 &&
> >> + (kmaj > 2 || (kmaj == 2 && kmin > 4))) {
> >> + fprintf(stderr,
> >> + "%s: note: 'large_read' mount option is deprecated for %i.%i kernels\n",
> >> + progname, kmaj, kmin);
> >> + return true;
> >
> > Ugh, you can drop the uname and all that. Nobody's running 2.4 kernels
> > anymore, right?
>
> Added a new commit that removes this. If someone complains I can recvert
> that
Thanks!
> >
> >> + }
> >> + return false;
> >> +}
> >> +
> >> +/*
> >> + * Check if user has permission to use allow_other or allow_root options.
> >> + *
> >> + * Returns -1 if permission denied, 0 if allowed or option is not
> >> + * allow_other/allow_root.
> >> + */
> >> +static int check_allow_permission(const char *opt, unsigned int len)
> >> +{
> >> + if (getuid() != 0 && !user_allow_other &&
> >> + (opt_eq(opt, len, "allow_other") || opt_eq(opt, len, "allow_root"))) {
> >> + fprintf(stderr, "%s: option %.*s only allowed if 'user_allow_other' is set in %s\n",
> >> + progname, len, opt, FUSE_CONF);
> >> + return -1;
> >> + }
> >> + return 0;
> >> +}
> >> +
> >> +/*
> >> + * Process generic mount option.
> >> + *
> >> + * Handles mount flags (ro, rw, suid, etc.), kernel options
> >> + * (default_permissions, allow_other, max_read, blksize), or exits on
> >> + * unknown options.
> >> + */
> >> +static int process_generic_option(const char *opt, unsigned int len,
> >> + unsigned long *flags, char **dest)
> >> +{
> >> + int on;
> >> + int flag;
> >> +
> >> + if (find_mount_flag(opt, len, &on, &flag)) {
> >> + if (on)
> >> + *flags |= flag;
> >> + else
> >> + *flags &= ~flag;
> >> + return 0;
> >> + }
> >> +
> >> + if (opt_eq(opt, len, "default_permissions") ||
> >> + opt_eq(opt, len, "allow_other") ||
> >> + begins_with(opt, "max_read=") ||
> >> + begins_with(opt, "blksize=")) {
> >> + memcpy(*dest, opt, len);
> >> + *dest += len;
> >> + **dest = ',';
> >> + (*dest)++;
> >> + return 0;
> >> + }
> >> +
> >> + fprintf(stderr, "%s: unknown option '%.*s'\n", progname, len, opt);
> >> + exit(1);
> >> +}
> >> +
> >> +static int prepare_mount(const char *opts, struct mount_params *mp)
> >> {
> >> int res;
> >> - int flags = MS_NOSUID | MS_NODEV;
> >> - char *optbuf;
> >> - char *mnt_opts = NULL;
> >> const char *s;
> >> char *d;
> >> - char *fsname = NULL;
> >> - char *subtype = NULL;
> >> - char *source = NULL;
> >> - char *type = NULL;
> >> - int blkdev = 0;
> >>
> >> - optbuf = (char *) malloc(strlen(opts) + 128);
> >> - if (!optbuf) {
> >> + mp->flags = MS_NOSUID | MS_NODEV;
> >> + mp->optbuf = (char *) malloc(strlen(opts) + 128);
> >> + if (!mp->optbuf) {
> >> fprintf(stderr, "%s: failed to allocate memory\n", progname);
> >> return -1;
> >> }
> >>
> >> - for (s = opts, d = optbuf; *s;) {
> >> + for (s = opts, d = mp->optbuf; *s;) {
> >> unsigned len;
> >> const char *fsname_str = "fsname=";
> >> const char *subtype_str = "subtype=";
> >> @@ -953,10 +1055,10 @@ static int do_mount(const char *mnt, const char **typep, mode_t rootmode,
> >> break;
> >> }
> >> if (begins_with(s, fsname_str)) {
> >> - if (!get_string_opt(s, len, fsname_str, &fsname))
> >> + if (!get_string_opt(s, len, fsname_str, &mp->fsname))
> >> goto err;
> >> } else if (begins_with(s, subtype_str)) {
> >> - if (!get_string_opt(s, len, subtype_str, &subtype))
> >> + if (!get_string_opt(s, len, subtype_str, &mp->subtype))
> >> goto err;
> >> } else if (opt_eq(s, len, "blkdev")) {
> >> if (getuid() != 0) {
> >> @@ -965,7 +1067,7 @@ static int do_mount(const char *mnt, const char **typep, mode_t rootmode,
> >> progname);
> >> goto err;
> >> }
> >> - blkdev = 1;
> >> + mp->blkdev = 1;
> >> } else if (opt_eq(s, len, "auto_unmount")) {
> >> auto_unmount = 1;
> >> } else if (!opt_eq(s, len, "nonempty") &&
> >> @@ -973,122 +1075,132 @@ static int do_mount(const char *mnt, const char **typep, mode_t rootmode,
> >> !begins_with(s, "rootmode=") &&
> >> !begins_with(s, "user_id=") &&
> >> !begins_with(s, "group_id=")) {
> >> - int on;
> >> - int flag;
> >> - int skip_option = 0;
> >> - if (opt_eq(s, len, "large_read")) {
> >> - struct utsname utsname;
> >> - unsigned kmaj, kmin;
> >> - res = uname(&utsname);
> >> - if (res == 0 &&
> >> - sscanf(utsname.release, "%u.%u",
> >> - &kmaj, &kmin) == 2 &&
> >> - (kmaj > 2 || (kmaj == 2 && kmin > 4))) {
> >> - fprintf(stderr, "%s: note: 'large_read' mount option is deprecated for %i.%i kernels\n", progname, kmaj, kmin);
> >> - skip_option = 1;
> >> - }
> >> - }
> >> - if (getuid() != 0 && !user_allow_other &&
> >> - (opt_eq(s, len, "allow_other") ||
> >> - opt_eq(s, len, "allow_root"))) {
> >> - fprintf(stderr, "%s: option %.*s only allowed if 'user_allow_other' is set in %s\n", progname, len, s, FUSE_CONF);
> >> + bool skip;
> >> +
> >> + if (check_allow_permission(s, len) == -1)
> >> goto err;
> >> - }
> >> - if (!skip_option) {
> >> - if (find_mount_flag(s, len, &on, &flag)) {
> >> - if (on)
> >> - flags |= flag;
> >> - else
> >> - flags &= ~flag;
> >> - } else if (opt_eq(s, len, "default_permissions") ||
> >> - opt_eq(s, len, "allow_other") ||
> >> - begins_with(s, "max_read=") ||
> >> - begins_with(s, "blksize=")) {
> >> - memcpy(d, s, len);
> >> - d += len;
> >> - *d++ = ',';
> >> - } else {
> >> - fprintf(stderr, "%s: unknown option '%.*s'\n", progname, len, s);
> >> - exit(1);
> >> - }
> >> - }
> >> +
> >> + skip = check_large_read(s, len);
> >> +
> >> + /*
> >> + * Skip deprecated large_read to avoid passing it to
> >> + * kernel which would reject it as unknown option.
> >> + */
> >> + if (!skip)
> >> + process_generic_option(s, len, &mp->flags, &d);
> >> }
> >> s += len;
> >> if (*s)
> >> s++;
> >> }
> >> *d = '\0';
> >> - res = get_mnt_opts(flags, optbuf, &mnt_opts);
> >> + res = get_mnt_opts(mp->flags, mp->optbuf, &mp->mnt_opts);
> >> if (res == -1)
> >> goto err;
> >>
> >> + mp->optbuf_end = d;
> >> +
> >> sprintf(d, "fd=%i,rootmode=%o,user_id=%u,group_id=%u",
> >> - fd, rootmode, getuid(), getgid());
> >> + mp->fd, mp->rootmode, getuid(), getgid());
> >>
> >> - source = malloc((fsname ? strlen(fsname) : 0) +
> >> - (subtype ? strlen(subtype) : 0) + strlen(dev) + 32);
> >> + mp->source = malloc((mp->fsname ? strlen(mp->fsname) : 0) +
> >> + (mp->subtype ? strlen(mp->subtype) : 0) + strlen(mp->dev) + 32);
> >>
> >> - type = malloc((subtype ? strlen(subtype) : 0) + 32);
> >> - if (!type || !source) {
> >> + mp->type = malloc((mp->subtype ? strlen(mp->subtype) : 0) + 32);
> >> + if (!mp->type || !mp->source) {
> >> fprintf(stderr, "%s: failed to allocate memory\n", progname);
> >> goto err;
> >> }
> >>
> >> - if (subtype)
> >> - sprintf(type, "%s.%s", blkdev ? "fuseblk" : "fuse", subtype);
> >> + if (mp->subtype)
> >> + sprintf(mp->type, "%s.%s", mp->blkdev ? "fuseblk" : "fuse", mp->subtype);
> >> else
> >> - strcpy(type, blkdev ? "fuseblk" : "fuse");
> >> + strcpy(mp->type, mp->blkdev ? "fuseblk" : "fuse");
> >
> > Hrm, maybe the patch adding fuse_mnt_build_source / fuse_mnt_build_type
> > should have done something about this code too.
> >
> > The straight-up conversion looks ok though.
>
> Yeah, I need to do that in a later commit that makes these two functions
> more generic.
<nod> Looking forward to it. I hoep I'm not being too annoying by doing
an xfs-style "and here's other random small cleanups" review :P
--D
^ permalink raw reply [flat|nested] 59+ messages in thread
* Re: [PATCH 17/19] Make fusermount work bidirectional for sync init
2026-03-24 21:24 ` Bernd Schubert
@ 2026-03-24 22:59 ` Darrick J. Wong
2026-03-25 19:48 ` Bernd Schubert
0 siblings, 1 reply; 59+ messages in thread
From: Darrick J. Wong @ 2026-03-24 22:59 UTC (permalink / raw)
To: Bernd Schubert
Cc: linux-fsdevel, Miklos Szeredi, Joanne Koong, Bernd Schubert
On Tue, Mar 24, 2026 at 10:24:04PM +0100, Bernd Schubert wrote:
>
>
> On 3/24/26 20:35, Darrick J. Wong wrote:
> > On Mon, Mar 23, 2026 at 06:45:12PM +0100, Bernd Schubert wrote:
> >> From: Bernd Schubert <bschubert@ddn.com>
> >>
> >> Signed-off-by: Bernd Schubert <bschubert@ddn.com>
> >> ---
> >> doc/README.fusermount | 359 ++++++++++++++++++++++++++++++++++++++++++++++++++
> >> util/fusermount.c | 317 ++++++++++++++++++++++++++++++++++++++++++--
> >> util/meson.build | 2 +-
> >> 3 files changed, 665 insertions(+), 13 deletions(-)
> >>
> >> diff --git a/doc/README.fusermount b/doc/README.fusermount
> >> new file mode 100644
> >> index 0000000000000000000000000000000000000000..54a3bac4f58964a4ed312d6f6bc15606fed1e647
> >> --- /dev/null
> >> +++ b/doc/README.fusermount
> >> @@ -0,0 +1,359 @@
> >> +Synchronous FUSE_INIT Protocol
> >> +================================
> >> +
> >> +Overview
> >> +--------
> >> +
> >> +The sync-init feature enables the FUSE library to start worker threads and
> >> +perform initialization ioctl calls BEFORE the actual mount() syscall happens.
> >> +This is required for the kernel's synchronous FUSE_INIT feature, where the
> >> +mount() syscall blocks until the FUSE daemon processes the INIT request.
> >> +
> >> +Without this feature, there would be a deadlock:
> >> +- mount() blocks waiting for INIT response
> >> +- Worker threads can't start because mount() hasn't returned
> >> +- INIT request can't be processed because worker threads aren't running
> >> +
> >> +
> >> +Protocol Flow
> >> +-------------
> >> +
> >> +Traditional mount flow:
> >> + 1. Library calls fusermount3
> >
> > Heh. I haven't looked much at fusermount until recently. I gather that
> > fuservicemount has somewhat similar goals to fusermount3? fusermount3
> > seems to be a helper subprocess that libfuse can invoke on behalf of an
> > unprivileged fuse server. The helper is responsible for:
> >
> > 1) opening /dev/fuse
> > 2) sending it to the fuse server via the FUSE_COMMFD_ENV fd which is
> > supposed to be an AF_UNIX socket
> > 3) calling mount()
> > 4) waiting for the parent to die
> > 5) maybe calling unmount()
> >
> > and it's really 1, 3, and 5 that need to be privileged, so that's why
> > it's a setuid program.
> >
> >> + 2. fusermount3 opens /dev/fuse
> >> + 3. fusermount3 performs mount() syscall
> >> + 4. fusermount3 sends fd to library
> >> + 5. Library starts worker threads
> >> + 6. Worker threads process FUSE requests
> >
> > Ah, yes. Thanks for adding this description! fuservicemount is I think
> > an upside-down version of fusermount -- fuservicemount runs in the
> > user's mount namespace, so it
> >
> > 1) connects to a named AF_UNIX socket to start an instance of the fuse
> > server
> > 2) opens /dev/fuse and a memfd to pass cli arguments
> > 3) passes those to the fuse server
> > 4) the fuse server asks fuservicemount to open resources and pass them
> > over the socket
> > 5) the fuse server passes source/type/mount options to fuservicemount
> > 6) fuservicemount mounts the fs and exits
> > 7) at some point the user unmounts, so the fuse server exits
> >
> >> +Sync-init mount flow:
> >> + 1. Library calls fusermount3 with --sync-init flag
> >> + 2. fusermount3 opens /dev/fuse
> >> + 3. fusermount3 sends fd to library
> >> + 4. Library receives fd
> >> + 5. Library performs FUSE_DEV_IOC_SYNC_INIT ioctl
> >> + 6. Library starts worker threads
> >> + 7. Library sends "proceed" signal to fusermount3
> >> + 8. fusermount3 performs mount() syscall (blocks until INIT completes)
> >> + 9. Worker threads process INIT request
> >> + 10. mount() syscall completes
> >> + 11. fusermount3 exits
> >> +
> >> +
> >> +Implementation Details
> >> +----------------------
> >> +
> >> +Bidirectional Communication:
> >> + - Uses the existing unix socket (_FUSE_COMMFD environment variable)
> >> + - Simple 1-byte protocol for signaling
> >> + - Library signals fusermount3 when ready to proceed with mount
> >> +
> >> +fusermount3 Changes:
> >> + - New --sync-init command-line option
> >> + - Split mount operation into two phases:
> >> + * mount_fuse_prepare(): Opens device, prepares parameters
> >> + * mount_fuse_finish_fsmount(): Performs actual mount() syscall
> >> + - wait_for_signal(): Waits for library to signal readiness
> >> + - struct mount_context: Preserves state between phases
> >> +
> >> +Library Changes:
> >> + - fuse_session_mount_new_api(): Uses new protocol when available
> >> + - Sends "proceed" signal after worker thread is ready
> >> + - Handles both old and new mount protocols for compatibility
> >> +
> >> +
> >> +Backward Compatibility
> >> +----------------------
> >> +
> >> +The implementation maintains full backward compatibility:
> >> + - Old library + new fusermount3: Works (uses traditional flow)
> >> + - New library + old fusermount3: Falls back to traditional flow
> >> + - New library + new fusermount3: Uses sync-init flow when appropriate
> >> +
> >> +
> >> +Error Handling
> >> +--------------
> >> +
> >> +If any step fails during the sync-init flow:
> >> + - fusermount3 closes the fd and exits with error
> >> + - Library detects failure and cleans up
> >> + - No mount is left in inconsistent state
> >> +
> >> +Connection closure:
> >> + - If library closes socket before signaling, fusermount3 detects and exits
> >> + - If fusermount3 crashes, library detects closed socket
> >> +
> >> +
> >> +Security Considerations
> >> +-----------------------
> >> +
> >> +The sync-init protocol does not introduce new security concerns:
> >> + - Uses the same privilege separation as traditional mount
> >> + - Socket communication is already established and trusted
> >> + - No new privileged operations are added
> >> + - File descriptor passing uses existing SCM_RIGHTS mechanism
> >> +
> >> +
> >> +Performance Impact
> >> +------------------
> >> +
> >> +Minimal performance impact:
> >> + - One additional recv() call in fusermount3
> >> + - One additional send() call in library
> >> + - Total overhead: ~2 context switches
> >> + - Only affects mount time, not runtime performance
> >> +
> >> +
> >> +Future Enhancements
> >> +-------------------
> >> +
> >> +Potential improvements:
> >> + - Extended protocol for more complex initialization sequences
> >> + - Support for multiple worker threads coordination
> >> + - Enhanced error reporting through the socket
> >> + - Timeout mechanisms for detecting hung initialization
> >> +
> >> +
> >> +ASCII Workflow Diagrams
> >> +========================
> >> +
> >> +1. Traditional Mount Flow (without --sync-init, async INIT)
> >> +------------------------------------------------------------
> >> +
> >> +Library fusermount3 Kernel
> >> + | | |
> >> + |--- spawn fusermount3 ---->| |
> >> + | | |
> >> + | [open /dev/fuse] |
> >> + | |------- open -------->|
> >> + | |<------ fd ---------- |
> >> + | | |
> >> + | [mount() syscall] |
> >> + | |------ mount -------->|
> >> + | |<----- success ------ | [mount returns immediately]
> >> + | | | [INIT queued in kernel]
> >> + | [send_fd(fd)] |
> >> + |<------- fd --------------| |
> >> + | | |
> >> + | [fusermount3 exits] |
> >> + | |
> >> + | [start worker thread] |
> >> + | [worker reads /dev/fuse] |
> >> + |---------------------------------------- read -->|
> >> + |<--------------------------------------- INIT ---| [dequeued from kernel]
> >> + | |
> >> + | OK: INIT was queued, worker reads it later |
> >> + | Works fine for async INIT |
> >
> > Hmm, looking at this, perhaps it /is/ possible for fuservicemount to
> > employ synchronous init. The fuse server would start that background
> > init-only request handler thread before telling fuservicemount to call
> > mount(). That blocks while the kernel sends FUSE_INIT to the fuse
> > server, it processes everything up to the init request, and returns.
> >
> >> +
> >> +
> >> +1b. Problem: Synchronous INIT without --sync-init
> >> +--------------------------------------------------
> >> +
> >> +Library fusermount3 Kernel
> >> + | | |
> >> + |--- spawn fusermount3 ---->| |
> >> + | | |
> >> + | [open /dev/fuse] |
> >> + | |------- open -------->|
> >> + | |<------ fd ---------- |
> >> + | | |
> >> + | [mount() syscall] |
> >> + | |------ mount -------->|
> >> + | | | [mount BLOCKS waiting for INIT]
> >> + | | (BLOCKED) | [needs worker to process INIT]
> >> + | | |
> >> + | [waiting for fd...] | |
> >> + | | |
> >> + | | |
> >> + | DEADLOCK: mount() waits for INIT response |
> >> + | but worker thread not started yet |
> >> + | because we're waiting for fd |
> >> +
> >> +
> >> +2. Sync-Init Mount Flow (with --sync-init)
> >> +-------------------------------------------
> >> +
> >> +Library fusermount3 Kernel
> >> + | | |
> >> + |--- spawn fusermount3 ---->| |
> >> + | with --sync-init | |
> >> + | | |
> >> + | [open /dev/fuse] |
> >> + | |------- open -------->|
> >> + | |<------ fd ---------- |
> >> + | | |
> >> + | [send_fd(fd)] |
> >> + |<------- fd --------------| |
> >> + | | |
> >> + | [wait_for_signal()] |
> >> + | | (BLOCKED) |
> >> + | | |
> >> + | [ioctl SYNC_INIT] | |
> >> + |---------------------------------------- ioctl -->|
> >> + | |
> >> + | [start worker thread] |
> >> + | [worker ready] |
> >> + | | |
> >> + |--- "proceed" signal ----->| |
> >> + | [signal received] |
> >> + | | |
> >> + | [mount() syscall] |
> >> + | |------ mount -------->|
> >> + | | | [mount blocks]
> >> + | | | [sends INIT]
> >> + |<------------------------------------------------ |
> >> + | | |
> >> + | [worker processes INIT] | |
> >> + |------------------------------------------------->|
> >> + | | | [mount unblocks]
> >> + | |<----- success ------ |
> >> + | | |
> >> + | [fusermount3 exits] |
> >> + | |
> >> + | SUCCESS: Worker ready before mount() |
> >> + | INIT processed synchronously |
> >> +
> >> +
> >> +3. Error Scenario: Library Crashes Before Signaling
> >> +----------------------------------------------------
> >> +
> >> +Library fusermount3 Kernel
> >> + | | |
> >> + |--- spawn fusermount3 ---->| |
> >> + | with --sync-init | |
> >> + | | |
> >> + | [open /dev/fuse] |
> >> + | |------- open -------->|
> >> + | |<------ fd ---------- |
> >> + | | |
> >> + | [send_fd(fd)] |
> >> + |<------- fd --------------| |
> >> + | | |
> >> + | [wait_for_signal()] |
> >> + | | (BLOCKED) |
> >> + | | |
> >> + X [library crashes] | |
> >> + | | |
> >> + | [recv() returns 0] |
> >> + | [socket closed] |
> >> + | | |
> >> + | [cleanup and exit] |
> >> + | X |
> >> + | |
> >> + | RESULT: Clean failure, no mount performed |
> >> +
> >> +
> >> +4. Detailed Function Call Flow
> >> +-------------------------------
> >> +
> >> +Library (lib/fuse_lowlevel.c):
> >> +fuse_session_mount_new_api()
> >> + |
> >> + +-- fuse_kern_mount_prepare() [lib/mount.c]
> >> + | |
> >> + | +-- fuse_mount_fusermount() [lib/mount_util.c]
> >> + | |
> >> + | +-- socketpair() [create comm socket]
> >> + | |
> >> + | +-- fork()
> >> + | |
> >> + | +-- [child] execl("fusermount3", "--sync-init", ...)
> >> + | |
> >> + | +-- [parent] receive_fd() <--- BLOCKS until fd arrives
> >> + | |
> >> + | +-- recvmsg(SCM_RIGHTS)
> >> + | |
> >> + | +-- return fd
> >> + |
> >> + +-- session_start_sync_init() [lib/fuse_lowlevel.c]
> >> + | |
> >> + | +-- ioctl(fd, FUSE_DEV_IOC_SYNC_INIT)
> >> + | |
> >> + | +-- pthread_create(worker_thread)
> >> + | |
> >> + | +-- return
> >> + |
> >> + +-- fuse_fusermount_proceed_mnt(socket) [lib/mount.c] <--- NEW: Bidirectional handshake
> >> + |
> >> + +-- send(socket, "proceed", 1) <--- Signal fusermount3 to proceed
> >> + |
> >> + +-- recv(socket, &status, 1) <--- BLOCKS until mount result arrives
> >> + | |
> >> + | +-- [fusermount3 performs mount and sends status byte]
> >> + |
> >> + +-- if (status != 0) return -1 <--- Mount failed
> >> + |
> >> + +-- return 0 <--- Mount succeeded
> >> +
> >> +
> >> +Utility (util/fusermount.c):
> >> +fusermount3 main() with --sync-init
> >> + |
> >> + +-- mount_fuse_sync_init() [util/fusermount.c]
> >> + |
> >> + +-- mount_fuse_prepare() [util/fusermount.c]
> >> + | |
> >> + | +-- open("/dev/fuse")
> >> + | |
> >> + | +-- check_perm() [util/fusermount.c]
> >> + | |
> >> + | +-- return fd
> >> + |
> >> + +-- send_fd(socket, fd) [util/fusermount.c]
> >> + | |
> >> + | +-- sendmsg(SCM_RIGHTS)
> >> + |
> >> + +-- wait_for_signal(socket) [util/fusermount.c] <--- BLOCKS until library signals
> >> + | |
> >> + | +-- recv(socket, buf, 1)
> >> + | |
> >> + | +-- return 0
> >> + |
> >> + +-- mount_fuse_finish_fsmount() [util/fusermount.c]
> >> + | |
> >> + | +-- fuse_kern_fsmount() [lib/mount_fsmount.c]
> >> + | | |
> >> + | | +-- fsopen("fuse", FSOPEN_CLOEXEC)
> >> + | | | |
> >> + | | | +-- [kernel creates filesystem context]
> >> + | | |
> >> + | | +-- fsconfig(fsfd, SET_STRING, "source", ...)
> >> + | | +-- fsconfig(fsfd, SET_STRING, "fd", fd_value, ...)
> >> + | | +-- fsconfig(fsfd, ...) [apply mount options]
> >> + | | +-- fsconfig(fsfd, CMD_CREATE, ...)
> >> + | | |
> >> + | | +-- fsmount(fsfd, FSMOUNT_CLOEXEC, mount_attrs)
> >> + | | | |
> >> + | | | +-- [kernel sends FUSE_INIT here]
> >> + | | | |
> >> + | | | +-- [worker thread processes INIT]
> >> + | | | |
> >> + | | | +-- [fsmount returns mntfd]
> >> + | | |
> >> + | | +-- move_mount(mntfd, "", AT_FDCWD, target, ...)
> >> + | | | |
> >> + | | | +-- [attach mount to target directory]
> >> + | | | |
> >> + | | | +-- [no blocking - INIT already processed]
> >> + | | |
> >> + | | +-- add_mount() [lib/mount_fsmount.c - update /etc/mtab]
> >> + | | |
> >> + | | +-- return 0 on success, -1 on failure
> >> + | |
> >> + | +-- if mount failed: return -1
> >> + | +-- if mount succeeded: continue
> >> + |
> >> + +-- send_status_byte(socket) [util/fusermount.c] <--- NEW: Send result to library
> >> + | |
> >> + | +-- status = (mount_result == 0) ? 0 : 1
> >> + | +-- send(socket, &status, 1)
> >> + | |
> >> + | +-- return
> >> + |
> >> + +-- return 0
> >> +
> >> +
> >> +Note: The new mount API (fsopen/fsconfig/fsmount/move_mount) is REQUIRED
> >> + for sync-init because fsmount() triggers FUSE_INIT before the mount
> >> + is attached. This allows the worker thread to process INIT before
> >> + move_mount() completes, preventing deadlock.
> >
> > ...and so we don't expose the directory tree to the mountns until we
> > know that FUSE_INIT didn't crash the server.
>
> Added that as well.
>
> >
> >> diff --git a/util/fusermount.c b/util/fusermount.c
> >> index 80b42a594e89cdc2f43824f5e274892522fd8cce..808b4afd89ceb49273c944d43bffe5033e27549b 100644
> >> --- a/util/fusermount.c
> >> +++ b/util/fusermount.c
> >> @@ -957,6 +957,7 @@ static void free_mount_params(struct mount_params *mp)
> >> free(mp->source);
> >> free(mp->type);
> >> free(mp->mnt_opts);
> >> + memset(mp, 0, sizeof(*mp));
> >> }
> >>
> >> /*
> >> @@ -1378,6 +1379,179 @@ static int open_fuse_device(const char *dev)
> >> return fd;
> >> }
> >>
> >> +#ifdef HAVE_NEW_MOUNT_API
> >> +/* Forward declaration from lib/mount_fsmount.c */
> >> +int fuse_kern_fsmount(const char *mnt, unsigned long flags, int blkdev,
> >> + const char *fsname, const char *subtype,
> >> + const char *source_dev, const char *kernel_opts,
> >> + const char *mnt_opts);
> >> +#endif
> >
> > Shouldn't this be included from a header file somewhere?
>
> Fixed, included from mount_i_linux.h
>
> >
> >> +
> >> +/*
> >> + * Context for split mount operation (sync-init mode)
> >> + */
> >> +struct mount_context {
> >> + int fd;
> >> + const char *dev;
> >> + struct stat stbuf;
> >> + char *source;
> >> + char *mnt_opts;
> >> + char *x_opts;
> >> + const char *type;
> >> +};
> >> +
> >> +/*
> >> + * Phase 1: Open device and prepare for mount (sync-init mode)
> >> + * Returns fd on success, -1 on failure
> >> + */
> >> +static int mount_fuse_prepare(const char *mnt, const char *opts,
> >> + struct mount_context *ctx)
> >> +{
> >> + int res;
> >> + int mountpoint_fd = -1;
> >> + char *do_mount_opts = NULL;
> >> + const char *real_mnt = mnt;
> >> +
> >> + memset(ctx, 0, sizeof(*ctx));
> >> + ctx->dev = getenv(FUSE_KERN_DEVICE_ENV) ?: FUSE_DEV;
> >> +
> >> + ctx->fd = open_fuse_device(ctx->dev);
> >> + if (ctx->fd == -1)
> >> + return -1;
> >> +
> >> + drop_privs();
> >> + read_conf();
> >> +
> >> + if (getuid() != 0 && mount_max != -1) {
> >> + int mount_count = count_fuse_fs();
> >> +
> >> + if (mount_count >= mount_max) {
> >> + fprintf(stderr,
> >> + "%s: too many FUSE filesystems mounted; mount_max=N can be set in %s\n",
> >> + progname, FUSE_CONF);
> >> + goto fail_close_fd;
> >> + }
> >> + }
> >
> > /me notes that he's refactored this configuration file related function
> > into fuser_conf.c though that's in the fuse-services v4 that I'll send
> > you soon.
> >
> >> +
> >> + res = extract_x_options(opts, &do_mount_opts, &ctx->x_opts);
> >> + if (res)
> >> + goto fail_close_fd;
> >> +
> >> + res = check_perm(&real_mnt, &ctx->stbuf, &mountpoint_fd);
> >> + restore_privs();
> >> +
> >> + if (mountpoint_fd != -1)
> >> + close(mountpoint_fd);
> >> +
> >> + if (res == -1)
> >> + goto fail_close_fd;
> >> +
> >> + free(do_mount_opts);
> >> + return ctx->fd;
> >> +
> >> +fail_close_fd:
> >> + close(ctx->fd);
> >> + free(do_mount_opts);
> >> + free(ctx->x_opts);
> >> + ctx->fd = -1;
> >> + return -1;
> >> +}
> >> +
> >> +#ifdef HAVE_NEW_MOUNT_API
> >> +/*
> >> + * Phase 2: Perform the actual mount using new mount API (sync-init mode)
> >> + * Returns 0 on success, -1 on failure
> >> + */
> >> +static int mount_fuse_finish_fsmount(const char *mnt, const char *opts,
> >> + struct mount_context *ctx,
> >> + const char **type)
> >> +{
> >> + int res;
> >> + char *do_mount_opts = NULL;
> >> + char *x_prefixed_opts = NULL;
> >> + struct mount_params mp = { .fd = ctx->fd };
> >> + char *final_mnt_opts = NULL;
> >> +
> >> + /* Extract x-options */
> >> + res = extract_x_options(opts, &do_mount_opts, &x_prefixed_opts);
> >> + if (res)
> >> + goto fail;
> >> +
> >> + /* Prepare mount parameters */
> >> + mp.rootmode = ctx->stbuf.st_mode & S_IFMT;
> >> + mp.dev = ctx->dev;
> >
> > I think those could be set in the mp variable definition?
>
> Absolutely.
>
> >
> >> +
> >> + res = prepare_mount(do_mount_opts, &mp);
> >> + if (res == -1)
> >> + goto fail;
> >> +
> >> + /* Merge x-options if running as root */
> >> + final_mnt_opts = mp.mnt_opts;
> >> + if (geteuid() == 0 && ctx->x_opts && strlen(ctx->x_opts) > 0) {
> >> + size_t mnt_opts_len = strlen(mp.mnt_opts);
> >> + size_t x_mnt_opts_len = mnt_opts_len + strlen(ctx->x_opts) + 2;
> >> + char *x_mnt_opts = calloc(1, x_mnt_opts_len);
> >> +
> >> + if (!x_mnt_opts)
> >> + goto fail_free_params;
> >> +
> >> + if (mnt_opts_len) {
> >> + strcpy(x_mnt_opts, mp.mnt_opts);
> >> + strncat(x_mnt_opts, ",", 2);
> >> + }
> >> + strncat(x_mnt_opts, ctx->x_opts,
> >> + x_mnt_opts_len - mnt_opts_len - 2);
> >> +
> >> + final_mnt_opts = x_mnt_opts;
> >> + }
> >
> > Curious, I thought the x- options were edited out by /sbin/mount so fuse
> > would never see them? Does the x- option handling in fusermount.c exist
> > to handle the case where someone passes them directly to the fuse
> > server, aka
> >
> > $ sshfs <whatever> /mnt -o x-systemd-hahaha=1
> >
> > and now you need to ensure that x-systemd-hahaha doesn't get sent to the
> > kernel but does get seen by the fuse server?
>
> https://github.com/libfuse/libfuse/issues/651
>
> So specially added as mount option to to go into mtab/utab and to
> suppress some 3rd party (gnome) actions.
Huh, I wouldn't have thought that would work since /etc/mtab has been
a symlink to /proc/self/mounts for a while now. Of course GNOME
abstracts mount options behind some g_unix_mount_entry_get_options
function call which ... doesn't document where it gets its mount options
from.
--D
> >
> >> +
> >> + /* Use new mount API */
> >> + res = fuse_kern_fsmount(mnt, mp.flags, mp.blkdev,
> >> + mp.fsname, mp.subtype, ctx->dev,
> >> + mp.optbuf, final_mnt_opts);
> >> + if (res == -1)
> >> + goto fail_free_merged;
> >> +
> >> + /* Change to root directory */
> >> + res = chdir("/");
> >> + if (res == -1) {
> >> + fprintf(stderr, "%s: failed to chdir to '/'\n", progname);
> >> + goto fail_free_merged;
> >> + }
> >> +
> >> + /* Store results in context */
> >> + ctx->source = mp.source;
> >> + ctx->type = mp.type;
> >> + ctx->mnt_opts = final_mnt_opts;
> >> + *type = mp.type;
> >> +
> >> + res = 0;
> >> +
> >> + /* Only free what is not assigned to ctx */
> >> + free(mp.fsname);
> >> + free(mp.subtype);
> >> + free(mp.optbuf);
> >> + if (final_mnt_opts != mp.mnt_opts)
> >> + free(mp.mnt_opts);
> >> +
> >> +out:
> >> + free(do_mount_opts);
> >> + free(x_prefixed_opts);
> >> +
> >> + return res;
> >> +
> >> +fail_free_merged:
> >> + if (final_mnt_opts != mp.mnt_opts)
> >> + free(final_mnt_opts);
> >> +fail_free_params:
> >> + free_mount_params(&mp);
> >> +fail:
> >> + res = -1;
> >> + goto out;
> >> +}
> >> +#endif /* HAVE_NEW_MOUNT_API */
> >> +
> >> +
> >> static int mount_fuse(const char *mnt, const char *opts, const char **type)
> >> {
> >> int res;
> >> @@ -1473,6 +1647,75 @@ fail_close_fd:
> >> goto out_free;
> >> }
> >>
> >> +/* Forward declarations for helper functions */
> >> +static int send_fd(int sock_fd, int fd);
> >> +static int wait_for_signal(int sock_fd);
> >> +
> >> +#ifdef HAVE_NEW_MOUNT_API
> >> +/*
> >> + * Perform sync-init mount using new mount API
> >> + * Returns 0 on success, -1 on failure
> >> + */
> >> +static int mount_fuse_sync_init(const char *mnt, const char *opts,
> >> + int cfd, const char **type)
> >> +{
> >> + struct mount_context ctx;
> >> + int fd, res;
> >> + int32_t status, send_res;
> >> +
> >> + /* Phase 1: Open device and prepare */
> >> + fd = mount_fuse_prepare(mnt, opts, &ctx);
> >> + if (fd == -1)
> >> + return -1;
> >> +
> >> + /* Send fd to caller so it can start worker thread */
> >> + res = send_fd(cfd, fd);
> >> + if (res != 0) {
> >> + close(fd);
> >> + free(ctx.x_opts);
> >> + return -1;
> >> + }
> >> +
> >> + /* Wait for caller to signal that worker thread is ready */
> >> + res = wait_for_signal(cfd);
> >> + if (res != 0) {
> >> + close(fd);
> >> + free(ctx.x_opts);
> >> + return -1;
> >> + }
> >> +
> >> + /* Phase 2: Perform the actual mount using new API */
> >> + res = mount_fuse_finish_fsmount(mnt, opts, &ctx, type);
> >> +
> >> + /* Send mount result back to caller (4-byte error code) */
> >> + status = (res == 0) ? 0 : -(int32_t)errno;
> >> + do {
> >> + send_res = send(cfd, &status, sizeof(status), 0);
> >> + } while (send_res == -1 && errno == EINTR);
> >> + if (send_res != sizeof(status)) {
> >> + fprintf(stderr, "%s: failed to send mount status: %s\n",
> >> + progname, strerror(errno));
> >> + }
> >> +
> >> + if (res == -1) {
> >> + close(fd);
> >> + free(ctx.source);
> >> + free(ctx.mnt_opts);
> >> + free(ctx.x_opts);
> >> + return -1;
> >> + }
> >> +
> >> + close(fd);
> >> +
> >> + /* Cleanup */
> >> + free(ctx.source);
> >> + free(ctx.mnt_opts);
> >> + free(ctx.x_opts);
> >> +
> >> + return 0;
> >
> > Err... these cleanups are identical, so I think you can get rid of the
> > "if (res == -1)" chunk and just return res at the end.
>
> Oh yeah, fixed.
>
>
> Thanks,
> Bernd
>
^ permalink raw reply [flat|nested] 59+ messages in thread
* Re: [PATCH 18/19] New mount API: Filter out "user="
2026-03-24 20:01 ` Bernd Schubert
@ 2026-03-24 23:02 ` Darrick J. Wong
0 siblings, 0 replies; 59+ messages in thread
From: Darrick J. Wong @ 2026-03-24 23:02 UTC (permalink / raw)
To: Bernd Schubert
Cc: linux-fsdevel, Miklos Szeredi, Joanne Koong, Bernd Schubert
On Tue, Mar 24, 2026 at 09:01:16PM +0100, Bernd Schubert wrote:
>
>
> On 3/24/26 20:51, Darrick J. Wong wrote:
> > On Mon, Mar 23, 2026 at 06:45:13PM +0100, Bernd Schubert wrote:
> >> From: Bernd Schubert <bschubert@ddn.com>
> >>
> >> This gets added in the fusermount process and kernel then fails
> >> the mount.
> >>
> >> Signed-off-by: Bernd Schubert <bschubert@ddn.com>
> >> ---
> >> lib/mount_fsmount.c | 13 ++++++++++++-
> >> 1 file changed, 12 insertions(+), 1 deletion(-)
> >>
> >> diff --git a/lib/mount_fsmount.c b/lib/mount_fsmount.c
> >> index f1fec790bb80f8815d485a068dc7efdff1746309..76c14cf9a22465160fc6b206ca9b6c9e7300adba 100644
> >> --- a/lib/mount_fsmount.c
> >> +++ b/lib/mount_fsmount.c
> >> @@ -218,6 +218,16 @@ static int is_mount_attr_opt(const char *opt)
> >> strcmp(opt, "symfollow") == 0;
> >> }
> >>
> >> +/**
> >> + * Check if an option is a mount table option (not passed to fsconfig)
> >> + */
> >> +static int is_mtab_only_opt(const char *opt)
> >> +{
> >> + /* These options are for /run/mount/utab only, not for the kernel */
> >> + return strncmp(opt, "user=", 5) == 0 ||
> >> + strcmp(opt, "rw") == 0;
> >
> > Oh, so these are added to the mount options string by get_mnt_opts
> > and now they need to be pulled out so they aren't passed to fsconfig?
> > How'd they get into the argument list in the first place?
>
>
> See the commit message ;) And then check fusermount.c in get_mnt_opts()
Ok, so this is something that you need to filter out.
Reviewed-by: "Darrick J. Wong" <djwong@kernel.org>
--D
>
>
> Cheers,
> Bernd
>
^ permalink raw reply [flat|nested] 59+ messages in thread
* Re: [PATCH 19/19] Add support for sync-init of unprivileged daemons
2026-03-24 21:53 ` Bernd Schubert
@ 2026-03-24 23:13 ` Darrick J. Wong
0 siblings, 0 replies; 59+ messages in thread
From: Darrick J. Wong @ 2026-03-24 23:13 UTC (permalink / raw)
To: Bernd Schubert
Cc: linux-fsdevel, Miklos Szeredi, Joanne Koong, Bernd Schubert
On Tue, Mar 24, 2026 at 10:53:21PM +0100, Bernd Schubert wrote:
>
>
> On 3/24/26 21:21, Darrick J. Wong wrote:
> > On Mon, Mar 23, 2026 at 06:45:14PM +0100, Bernd Schubert wrote:
> >> From: Bernd Schubert <bschubert@ddn.com>
> >>
> >> This makes use of the bidirectional fusermount. Added is
> >> doc/README.mount, which explains the new bidirectional
> >> communication with fusermount.
> >>
> >> Signed-off-by: Bernd Schubert <bschubert@ddn.com>
> >
> > All right, last patch before I go have some lunch and circle back to
> > your recent replies :)
> >
> >> ---
> >> doc/README.mount | 86 ++++++++++++++++++++++++
> >> doc/README.sync-init | 184 +++++++++++++++++++++++++++++++++++++++++++++++++++
> >
> > These new readmes feel like they ought to go at the beginning (or at
> > least a separate patch) to argue for why synchronous init is needed
> > in libfuse? I do appreciate the flow diagrams though.
>
> These new READMEs are kind of used by myself to understand and remember
> the flow graph and reasoning. Given the frequency I jump between
> projects, I prefer to have some files that help me to remember.
> I had asked for AI help to create these flow graphs, simple to give the
> right commands when one is in the middle of the code...
> I can move these files into a separate patch if you prefer.
TBH it would've been great to see this as a documentation-only patch 1
that came before all the actual code changes, as a guide for what I
should expect in the subsequent code changes.
That said ... I got a lot of negative feedback for xfs online fsck for
putting a 100+ page design document at the start of the series and then
it was really hard to get anyone to read the patchset. So I get why
most people don't start with a novella's worth of prose.
> >
> >> lib/fuse_lowlevel.c | 115 ++++++++++++++++++++++++++------
> >> lib/mount.c | 126 ++++++++++++++++++++++++++++++++++-
> >> lib/mount_i_linux.h | 7 ++
> >> util/fusermount.c | 2 -
> >> 6 files changed, 494 insertions(+), 26 deletions(-)
> >>
> >> diff --git a/doc/README.mount b/doc/README.mount
> >> new file mode 100644
> >> index 0000000000000000000000000000000000000000..526382ad8a5f6b405a7cb1927b79bacd6c2c2c5c
> >> --- /dev/null
> >> +++ b/doc/README.mount
> >> @@ -0,0 +1,86 @@
> >> +FUSE Mount API Flowcharts
> >> +=========================
> >> +
> >> +Old Mount API
> >> +-------------
> >> +
> >> +fuse_kern_mount()
> >> + |
> >> + +-- fuse_mount_sys()
> >> + | +-- Try direct mount → mount() syscall
> >> + | +-- On EPERM: fuse_mount_fusermount()
> >> + | +-- socketpair()
> >> + | +-- spawn fusermount3 (no --sync-init)
> >> + | +-- fusermount3: open /dev/fuse, mount(), send fd
> >> + | +-- receive_fd() → return fd
> >> + |
> >> + +-- Worker threads started AFTER mount
> >> + └─> FUSE_INIT asynchronous (queued in kernel)
> >> +
> >> +
> >> +New Mount API - Privileged Mount
> >> +---------------------------------
> >> +
> >> +fuse_session_mount_new_api()
> >> + |
> >> + +-- fuse_kern_mount_prepare() → open /dev/fuse → fd
> >> + |
> >> + +-- session_start_sync_init(se, fd)
> >> + | +-- ioctl(fd, FUSE_DEV_IOC_SYNC_INIT)
> >> + | +-- pthread_create(worker) → ready to process FUSE_INIT
> >> + |
> >> + +-- fuse_kern_fsmount_mo()
> >> + | +-- fsopen/fsconfig/fsmount (BLOCKS until FUSE_INIT completes)
> >> + | +-- Worker processes FUSE_INIT during fsmount()
> >> + | +-- move_mount()
> >> + |
> >> + +-- session_wait_sync_init_completion(se) → pthread_join
> >> + └─> return fd
> >> +
> >> +
> >> +New Mount API - EPERM Fallback (fusermount3 with sync-init)
> >> +------------------------------------------------------------
> >> +
> >> +fuse_session_mount_new_api()
> >> + |
> >> + +-- fuse_kern_mount_prepare() → open /dev/fuse → fd1
> >> + |
> >> + +-- session_start_sync_init(se, fd1)
> >> + | +-- ioctl(fd1, FUSE_DEV_IOC_SYNC_INIT)
> >> + | +-- pthread_create(worker) → ready with fd1
> >> + |
> >> + +-- fuse_kern_fsmount_mo() → EPERM
> >> + |
> >> + +-- *** FALLBACK TO FUSERMOUNT3 WITH SYNC-INIT ***
> >> + |
> >> + +-- session_wait_sync_init_completion(se)
> >> + | +-- pthread_cancel/join → terminate worker with wrong fd1
> >> + |
> >> + +-- close(fd1)
> >> + |
> >> + +-- fuse_mount_fusermount_sync_init() [NEW]
> >> + | +-- socketpair()
> >> + | +-- spawn fusermount3 --sync-init
> >> + | +-- fusermount3: open /dev/fuse → fd2, send fd2
> >> + | +-- receive_fd() → fd2
> >> + | +-- fusermount3 waits for signal
> >> + | └─> return fd2, sock
> >> + |
> >> + +-- session_start_sync_init(se, fd2)
> >> + | +-- ioctl(fd2, FUSE_DEV_IOC_SYNC_INIT)
> >> + | +-- pthread_create(worker) → ready with fd2
> >> + |
> >> + +-- send_proceed_signal(sock) [NEW]
> >> + | +-- send(sock, "\0", 1) → signal fusermount3
> >> + |
> >> + +-- fusermount3: mount() (BLOCKS)
> >> + | +-- Kernel sends FUSE_INIT to fd2
> >> + | +-- Worker processes FUSE_INIT
> >> + | +-- mount() returns
> >> + |
> >> + +-- close(sock)
> >> + |
> >> + +-- session_wait_sync_init_completion(se) → pthread_join
> >> + |
> >> + └─> return fd2
> >> +
> >> diff --git a/doc/README.sync-init b/doc/README.sync-init
> >> new file mode 100644
> >> index 0000000000000000000000000000000000000000..44e47a2eef2c45026abaa19562537eef37f256b9
> >> --- /dev/null
> >> +++ b/doc/README.sync-init
> >> @@ -0,0 +1,184 @@
> >> +FUSE Synchronous vs Asynchronous FUSE_INIT
> >> +============================================
> >> +
> >> +This document explains the difference between asynchronous and synchronous
> >> +FUSE_INIT processing, and when each mode is used.
> >> +
> >> +
> >> +Overview
> >> +--------
> >> +
> >> +FUSE_INIT is the initial handshake between the kernel FUSE module and the
> >> +userspace filesystem daemon. During this handshake, the kernel and daemon
> >> +negotiate capabilities, protocol version, and various feature flags.
> >> +
> >> +Asynchronous FUSE_INIT (Traditional Behavior)
> >> +----------------------------------------------
> >> +
> >> +In the traditional asynchronous mode:
> >> +
> >> +1. mount() syscall completes and returns to caller
> >> +2. Filesystem appears mounted to the system
> >> +3. FUSE daemon starts worker threads
> >> +4. Worker threads process FUSE_INIT request
> >> +5. Filesystem becomes fully operational
> >> +
> >> +Timeline:
> >> + mount() -----> returns
> >> + |
> >> + v
> >> + FUSE_INIT sent
> >> + |
> >> + v
> >> + daemon processes FUSE_INIT
> >> + |
> >> + v
> >> + filesystem ready
> >> +
> >> +Limitations:
> >> +
> >> +1. **No early requests**: The kernel cannot send requests (like getxattr)
> >> + during the mount() syscall. This breaks SELinux, which needs to query
> >> + extended attributes on the root inode immediately upon mounting.
> >> +
> >> +2. **Daemonization timing**: With the old fuse_daemonize() API, the daemon
> >> + must call it AFTER mount, because there's no way to report mount failures
> >> + to the parent process if daemonization happens first.
> >> +
> >> +3. **No custom root inode**: The root inode ID is hardcoded to FUSE_ROOT_ID (1)
> >> + because FUSE_INIT hasn't been processed yet when the mount completes.
> >> +
> >> +4. **Thread startup after mount**: io_uring threads and other worker threads
> >> + can only start after mount() returns, not before.
> >
> > Especially this part which explains why we care about sync init :)
> >
> >> +
> >> +Synchronous FUSE_INIT (New Behavior)
> >> +-------------------------------------
> >> +
> >> +Kernel support: Linux kernel commit dfb84c330794 (v6.18+)
> >> +libfuse support: libfuse 3.19+
> >> +
> >> +In synchronous mode:
> >> +
> >> +1. FUSE daemon opens /dev/fuse
> >> +2. Daemon calls ioctl(fd, FUSE_DEV_IOC_SYNC_INIT)
> >> +3. Daemon starts worker thread
> >> +4. Daemon calls mount() syscall
> >> +5. Kernel sends FUSE_INIT during mount() - mount() blocks
> >> +6. Worker thread processes FUSE_INIT while mount() is blocked
> >> +7. Worker thread may process additional requests (getxattr, etc.)
> >> +8. mount() syscall completes and returns
> >> +9. Filesystem is fully operational
> >> +
> >> +Timeline:
> >> + open /dev/fuse
> >> + |
> >> + v
> >> + ioctl(FUSE_DEV_IOC_SYNC_INIT)
> >> + |
> >> + v
> >> + start worker thread
> >> + |
> >> + v
> >> + mount() -----> blocks
> >> + | |
> >> + | v
> >> + | FUSE_INIT sent
> >> + | |
> >> + | v
> >> + | worker processes FUSE_INIT
> >> + | |
> >> + | v
> >> + | (possible getxattr, etc.)
> >> + | |
> >> + +-------> returns
> >> + |
> >> + v
> >> + filesystem ready
> >> +
> >> +Advantages:
> >> +
> >> +1. **SELinux support**: The kernel can send getxattr requests during mount()
> >> + to query security labels on the root inode.
> >> +
> >> +2. **Early daemonization**: The daemon can fork BEFORE mount using the new
> >> + fuse_daemonize_start()/signal() API, and report mount failures to the
> >> + parent process.
> >> +
> >> +3. **Custom root inode**: The daemon can specify a custom root inode ID
> >> + during FUSE_INIT, before mount() completes.
> >> +
> >> +4. **Thread startup before mount**: io_uring threads and worker threads
> >> + start before mount(), ensuring they're ready to handle requests.
> >> +
> >> +5. **Better error reporting**: Mount failures and initialization errors
> >> + can be properly reported to the parent process when using the new
> >> + daemonization API.
> >> +
> >> +
> >> +When Synchronous FUSE_INIT is Used
> >> +-----------------------------------
> >> +
> >> +libfuse automatically enables synchronous FUSE_INIT when:
> >> +
> >> +1. The application calls fuse_session_want_sync_init(), OR
> >> +2. The new daemonization API is used (fuse_daemonize_start() was called)
> >> +
> >> +Synchronous FUSE_INIT requires:
> >> +- Kernel support (commit dfb84c330794 or later)
> >> +- Worker thread started before mount()
> >> +- ioctl(FUSE_DEV_IOC_SYNC_INIT) succeeds
> >> +
> >> +If the kernel doesn't support synchronous FUSE_INIT, libfuse automatically
> >> +falls back to asynchronous mode.
> >> +
> >> +
> >> +Implementation Details
> >> +----------------------
> >> +
> >> +The synchronous FUSE_INIT implementation uses a worker thread:
> >> +
> >> +- **session_sync_init_worker()**: Thread function that polls /dev/fuse
> >> + and processes FUSE_INIT and any subsequent requests until mount completes.
> >> +
> >> +- **session_start_sync_init()**: Creates the worker thread before mount().
> >> + Calls ioctl(FUSE_DEV_IOC_SYNC_INIT) to enable kernel support.
> >> +
> >> +- **session_wait_sync_init_completion()**: Waits for the worker thread
> >> + to complete after mount() returns. Checks for errors.
> >> +
> >> +The worker thread processes requests in a loop until se->terminate_mount_worker
> >> +is set, which happens after mount() completes successfully.
> >> +
> >> +
> >> +Compatibility
> >> +-------------
> >> +
> >> +Synchronous FUSE_INIT is fully backward compatible:
> >> +
> >> +- Old kernels: ioctl returns ENOTTY, libfuse falls back to async mode
> >> +- Old applications: Continue to work with async FUSE_INIT
> >> +- New applications on old kernels: Graceful fallback to async mode
> >> +- New applications on new kernels: Automatic sync mode when appropriate
> >> +
> >> +
> >> +Example: Enabling Synchronous FUSE_INIT
> >> +----------------------------------------
> >> +
> >> +Explicit request:
> >> + struct fuse_session *se = fuse_session_new(...);
> >> + fuse_session_want_sync_init(se);
> >> + fuse_session_mount(se, mountpoint);
> >> +
> >> +Automatic (with new daemonization API):
> >> + fuse_daemonize_start(0); // Triggers sync init automatically
> >> + fuse_session_mount(se, mountpoint);
> >> +
> >> +
> >> +See Also
> >> +--------
> >> +
> >> +- doc/README.daemonize - New daemonization API documentation
> >> +- doc/README.fusermount - Synchronous FUSE_INIT protocol with fusermount3
> >> +- doc/README.mount - Mount implementation details
> >> +
> >> diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c
> >> index a7293a3898c37c3877eadf965d310ae2aa5cc2d1..da966217ed841744a20bee60de8ae615d1015b47 100644
> >> --- a/lib/fuse_lowlevel.c
> >> +++ b/lib/fuse_lowlevel.c
> >> @@ -41,6 +41,7 @@
> >> #include <assert.h>
> >> #include <sys/file.h>
> >> #include <sys/ioctl.h>
> >> +#include <sys/wait.h>
> >> #include <stdalign.h>
> >> #include <poll.h>
> >>
> >> @@ -4551,6 +4552,8 @@ static int session_wait_sync_init_completion(struct fuse_session *se)
> >> se->init_wakeup_fd = -1;
> >> }
> >>
> >> + se->init_thread = 0;
> >> +
> >> if (se->init_error != 0) {
> >> fuse_log(FUSE_LOG_ERR, "fuse: init worker failed: %d\n", se->init_error);
> >> return -1;
> >> @@ -4564,56 +4567,125 @@ static int session_wait_sync_init_completion(struct fuse_session *se)
> >> return 0;
> >> }
> >>
> >> -/* Only linux supports sync FUSE_INIT so far */
> >> +/*
> >> + * Mount using the new Linux mount API (fsopen/fsconfig/fsmount/move_mount)
> >> + * Sync-init is only supported with the new API, as the mount might hang
> >> + * in case of daemon crash during FUSE_INIT. That also means once the sync init
> >> + * ioctl succeed fallback is not allowed anymore.
> >> + * Returns: fd on success, -1 on failure
> >> + */
> >> static int fuse_session_mount_new_api(struct fuse_session *se,
> >> - const char *mountpoint)
> >> + const char *mountpoint, bool *fall_back)
> >> {
> >> int fd = -1;
> >> + int sock_fd = -1;
> >> + pid_t fusermount_pid = -1;
> >> int res, err;
> >> char *mnt_opts = NULL;
> >> char *mnt_opts_with_fd = NULL;
> >> char fd_opt[32];
> >>
> >> res = fuse_kern_mount_get_base_mnt_opts(se->mo, &mnt_opts);
> >> + err = -EIO;
> >> if (res == -1) {
> >> fuse_log(FUSE_LOG_ERR, "fuse: failed to get base mount options\n");
> >> - err = -EIO;
> >
> > Odd churn in this function...
> >
> >> goto err;
> >> }
> >>
> >> fd = fuse_kern_mount_prepare(mountpoint, se->mo);
> >> if (fd == -1) {
> >> fuse_log(FUSE_LOG_ERR, "Mount preparation failed.\n");
> >> - err = -EIO;
> >> goto err;
> >> }
> >>
> >> - /*
> >> - * Enable synchronous FUSE_INIT and start worker thread, sync init
> >> - * failure is not an error
> >> - */
> >> + *fall_back = true;
> >> se->fd = fd;
> >> err = session_start_sync_init(se, fd);
> >> if (err) {
> >> /* ENOTTY means kernel doesn't support sync init - not an error */
> >> if (err != -ENOTTY)
> >> goto err;
> >> + } else {
> >> + *fall_back = false;
> >> }
> >> +
> >> +
> >> snprintf(fd_opt, sizeof(fd_opt), "fd=%i", fd);
> >> + err = -ENOMEM;
> >> if (fuse_opt_add_opt(&mnt_opts_with_fd, mnt_opts) == -1 ||
> >> fuse_opt_add_opt(&mnt_opts_with_fd, fd_opt) == -1) {
> >> - err = -ENOMEM;
> >> goto err;
> >> }
> >>
> >> + /* Try to mount directly */
> >> err = fuse_kern_fsmount_mo(mountpoint, se->mo, mnt_opts_with_fd);
> >> +
> >> + /* If mount failed with EPERM, fall back to fusermount3 with sync-init */
> >
> >
> > ...since this is the new "actually use bidirectional fusermount3" code
> > mentioned in the commit message.
>
> Here I'm lost what you mean., bidirectional fusermount3 only follows below.
Oh I was just grumbling about the other diff hunks that moved the "err =
-ENOMEM" assignments around.
> >
> >> + if (err < 0 && errno == EPERM) {
> >> + if (se->debug)
> >> + fuse_log(FUSE_LOG_DEBUG,
> >> + "fuse: privileged mount failed with EPERM, falling back to fusermount3\n");
> >> +
> >> + /* Terminate worker thread with wrong fd */
> >> + if (session_wait_sync_init_completion(se) < 0)
> >> + fuse_log(FUSE_LOG_ERR, "fuse: sync init completion failed\n");
> >> +
> >> + /* Close the privileged fd */
> >> + close(fd);
> >> + fd = -1;
> >> + se->fd = -1;
> >> +
> >> + /* Call fusermount3 with --sync-init */
> >> + err = -ENOTSUP;
> >> + fd = mount_fusermount_obtain_fd(mountpoint, se->mo, mnt_opts,
> >> + &sock_fd, &fusermount_pid);
> >> + if (fd < 0) {
> >> + fuse_log(
> >> + FUSE_LOG_ERR,
> >> + "fuse: fusermount3 sync-init failed\n");
> >> + goto err;
> >> + }
> >> +
> >> + /* Start worker thread with correct fd from fusermount3 */
> >> + se->fd = fd;
> >> + err = session_start_sync_init(se, fd);
> >> + if (err) {
> >> + if (err != -ENOTTY) {
> >> + fuse_log(
> >> + FUSE_LOG_ERR,
> >> + "fuse: failed to start sync init worker\n");
> >> + goto err_with_sock;
> >> + }
> >> + } else {
> >> + *fall_back = false;
> >
> > We already set *fall_back to false above, didn't we? I'm slightly
> > confused -- should we set *fall_back=true any time this function returns
> > nonzero?
>
> Already updated, because there was merge conflict since
> session_start_sync_init() doesn't return ENOTTY anymore. fall_back is
> possible as long as the ioctl doesn't succeed.
Oh! Ok. :)
> >
> >> + }
> >> +
> >> + /* Send proceed signal and wait for mount result */
> >> + err = fuse_fusermount_proceed_mnt(sock_fd);
> >> + if (err < 0) {
> >> + err = -EIO;
> >> + goto err_with_sock;
> >> + }
> >> + } else if (err < 0) {
> >> + /* Mount failed with non-EPERM error, bail out */
> >> + goto err;
> >> + }
> >> +
> >> +err_with_sock:
> >> + if (sock_fd >= 0) {
> >> + close(sock_fd);
> >> + /* Reap fusermount3 child process to prevent zombie */
> >> + if (fusermount_pid > 0)
> >> + waitpid(fusermount_pid, NULL, 0);
> >> + }
> >> err:
> >> if (err < 0) {
> >> + /* Close fd first to unblock worker thread */
> >> if (fd >= 0)
> >> close(fd);
> >> fd = -1;
> >> se->fd = -1;
> >> - se->error = -errno;
> >> + se->error = err;
> >> }
> >> /* Wait for synchronous FUSE_INIT to complete */
> >> if (session_wait_sync_init_completion(se) < 0)
> >> @@ -4625,10 +4697,11 @@ err:
> >> }
> >> #else
> >> static int fuse_session_mount_new_api(struct fuse_session *se,
> >> - const char *mountpoint)
> >> + const char *mountpoint, bool *fall_back)
> >> {
> >> (void) se;
> >> (void) mountpoint;
> >> + (void) fall_back;
> >>
> >> return -1;
> >> }
> >> @@ -4638,6 +4711,7 @@ int fuse_session_mount(struct fuse_session *se, const char *_mountpoint)
> >> {
> >> int fd;
> >> char *mountpoint;
> >> + bool fall_back;
> >>
> >> if (_mountpoint == NULL) {
> >> fuse_log(FUSE_LOG_ERR, "Invalid null-ptr mountpoint!\n");
> >> @@ -4681,21 +4755,18 @@ int fuse_session_mount(struct fuse_session *se, const char *_mountpoint)
> >> return 0;
> >> }
> >>
> >> - /* new linux mount api */
> >> - fd = fuse_session_mount_new_api(se, mountpoint);
> >> - if (fd >= 0)
> >> - goto out;
> >> + /* new linux mount api (and sync init) */
> >> + fd = fuse_session_mount_new_api(se, mountpoint, &fall_back);
> >>
> >> /* fall back to old API */
> >> - se->error = 0; /* reset error of new api */
> >> - fd = fuse_kern_mount(mountpoint, se->mo);
> >> - if (fd < 0)
> >> - goto error_out;
> >> + if (fall_back && fd < 0) {
> >> + se->error = 0; /* reset error of new api */
> >> + fd = fuse_kern_mount(mountpoint, se->mo);
> >> + if (fd < 0)
> >> + goto error_out;
> >> + }
> >>
> >> -out:
> >> se->fd = fd;
> >> -
> >> - /* Save mountpoint */
> >> se->mountpoint = mountpoint;
> >>
> >> return 0;
> >> diff --git a/lib/mount.c b/lib/mount.c
> >> index 263b05051c236458b830c40181bce7f494803800..985938ea0be3e1affad19adad527a31ac2ca6034 100644
> >> --- a/lib/mount.c
> >> +++ b/lib/mount.c
> >> @@ -41,6 +41,7 @@
> >> #define FUSERMOUNT_PROG "fusermount3"
> >> #define FUSE_COMMFD_ENV "_FUSE_COMMFD"
> >> #define FUSE_COMMFD2_ENV "_FUSE_COMMFD2"
> >> +#define ARG_FD_ENTRY_SIZE 30
> >
> > Thirty seems a bit much for an integer, especially one that can't go
> > above 1 million. Eh, it's just stack space. :)
>
> I just made it a define. We can change it later, though userspace stack
> space is not that limited.
/me realizes that pthreads gives you 8MB per thread nowadays(!!)
I've clearly been stuck in the kernel too long. :)
> >
> >> enum {
> >> KEY_KERN_FLAG,
> >> @@ -313,7 +314,7 @@ static int setup_auto_unmount(const char *mountpoint, int quiet)
> >> return -1;
> >> }
> >>
> >> - char arg_fd_entry[30];
> >> + char arg_fd_entry[ARG_FD_ENTRY_SIZE];
> >> snprintf(arg_fd_entry, sizeof(arg_fd_entry), "%i", fds[0]);
> >> setenv(FUSE_COMMFD_ENV, arg_fd_entry, 1);
> >> /*
> >> @@ -386,7 +387,7 @@ static int fuse_mount_fusermount(const char *mountpoint, struct mount_opts *mo,
> >> return -1;
> >> }
> >>
> >> - char arg_fd_entry[30];
> >> + char arg_fd_entry[ARG_FD_ENTRY_SIZE];
> >> snprintf(arg_fd_entry, sizeof(arg_fd_entry), "%i", fds[0]);
> >> setenv(FUSE_COMMFD_ENV, arg_fd_entry, 1);
> >> /*
> >> @@ -446,6 +447,127 @@ static int fuse_mount_fusermount(const char *mountpoint, struct mount_opts *mo,
> >> return fd;
> >> }
> >>
> >> +/*
> >> + * Mount using fusermount3 with --sync-init flag for bidirectional fd exchange
> >> + * Used by new mount API when privileged mount fails with EPERM
> >> + *
> >> + * Returns: fd on success, -1 on failure
> >> + * On success, *sock_fd_out contains the socket fd for signaling fusermount3
> >> + */
> >> +int mount_fusermount_obtain_fd(const char *mountpoint, struct mount_opts *mo,
> >> + const char *opts, int *sock_fd_out,
> >> + pid_t *pid_out)
> >> +{
> >> + int fds[2];
> >> + pid_t pid;
> >> + int res;
> >> + char arg_fd_entry[ARG_FD_ENTRY_SIZE];
> >> + posix_spawn_file_actions_t action;
> >> + int fd, status;
> >> +
> >> + (void)mo;
> >> +
> >> + if (!mountpoint) {
> >> + fuse_log(FUSE_LOG_ERR, "fuse: missing mountpoint parameter\n");
> >> + return -1;
> >> + }
> >> +
> >> + res = socketpair(PF_UNIX, SOCK_STREAM, 0, fds);
> >> + if (res == -1) {
> >> + fuse_log(FUSE_LOG_ERR, "Running %s: socketpair() failed: %s\n",
> >> + FUSERMOUNT_PROG, strerror(errno));
> >> + return -1;
> >> + }
> >> +
> >> + snprintf(arg_fd_entry, sizeof(arg_fd_entry), "%i", fds[0]);
> >> + setenv(FUSE_COMMFD_ENV, arg_fd_entry, 1);
> >
> > Oh! /me realizes that FUSE_COMMFD{,2}_ENV can convey different things!
> >
> > If you're trying to get fusermount to *mount* a filesystem, then it's
> > the AF_UNIX socket that is used to pass the /dev/fuse fd to the fuse
> > server and then to trigger the mount.
> >
> > If you pass --auto-unmount/-U then fusermount waits for the socket to
> > close and then unmounts the mount.
> >
> >> + snprintf(arg_fd_entry, sizeof(arg_fd_entry), "%i", fds[1]);
> >> + setenv(FUSE_COMMFD2_ENV, arg_fd_entry, 1);
> >
> > ...and I guess you can pass the fds on the cli instead of goofy
> > environment variables? I wonder if you should be passing them via CLI
> > since you know fusermount supports it. OTOH I don't really care either
> > way ;)
>
> I had added the parameter to fusermount to avoid the env, issue is that
> an old fusermount might be used with a new libfuse. I did that in the
> past myself. For the new mount API and sync init, yeah, we can switch to
> parameter, requires all the new functionality anyway.
<nod>
> >
> >> +
> >> + char const *const argv[] = {
> >> + FUSERMOUNT_PROG,
> >> + "--sync-init",
> >> + "-o", opts ? opts : "",
> >> + "--",
> >> + mountpoint,
> >> + NULL,
> >> + };
> >> +
> >> + posix_spawn_file_actions_init(&action);
> >> + posix_spawn_file_actions_addclose(&action, fds[1]);
> >> + status = fusermount_posix_spawn(&action, argv, &pid);
> >> + posix_spawn_file_actions_destroy(&action);
> >> +
> >> + if (status != 0) {
> >> + close(fds[0]);
> >> + close(fds[1]);
> >> + return -1;
> >> + }
> >> +
> >> + close(fds[0]);
> >> +
> >> + fd = receive_fd(fds[1]);
> >> + if (fd < 0) {
> >> + close(fds[1]);
> >> + waitpid(pid, NULL, 0);
> >> + return -1;
> >> + }
> >> +
> >> + fcntl(fd, F_SETFD, FD_CLOEXEC);
> >> +
> >> + /* Return socket fd for later signaling */
> >> + *sock_fd_out = fds[1];
> >> + *pid_out = pid;
> >> +
> >> + return fd;
> >> +}
> >> +
> >> +/*
> >> + * Send proceed signal to fusermount3 and wait for mount result
> >> + * Returns: 0 on success, -1 on failure
> >> + */
> >> +int fuse_fusermount_proceed_mnt(int sock_fd)
> >> +{
> >> + char buf = '\0';
> >> + ssize_t res;
> >> +
> >> + /* Send proceed signal */
> >> + do {
> >> + res = send(sock_fd, &buf, 1, 0);
> >> + } while (res == -1 && errno == EINTR);
> >
> > I wonder if all the pipe/socket communications ought to have been turned
> > into a bunch of wrappers like what I did for
> > mount_service.c/fuse_service.c?
> >
> > That said, it looks like most of the fusermount/sync-init communcations
> > are single ints so maybe it doesn't matter. The communications for the
> > fuse servers is much more complex and hence needs more structure.
>
> Maybe we can look into that after merging the series and before making a
> 3.19 release? I don't want to make this series any longer than
> absolutely neded.
Yeah, let's do that. I worry about a slight bisection hazard if someone
should land in the middle of upstreaming, but ... who knows how often
anyone really tries to bisect a userspace library.
I may have over-engineered the mount-service part with network byte
ordering and whatnot. It's probably not likely to happen but in theory
you could run a fuse systemd container with a root directory that's
actually a chroot containing Linux for some other architecture (e.g.
ppc32) and dog-slow emulation via qemu binfmt. I don't know why you'd
want to make fuse even slower, but it's at least theoretically possible.
--D
^ permalink raw reply [flat|nested] 59+ messages in thread
* Re: [PATCH 12/19] fuse mount: Support synchronous FUSE_INIT (privileged daemon)
2026-03-24 22:50 ` Darrick J. Wong
@ 2026-03-25 7:52 ` Bernd Schubert
2026-03-25 16:42 ` Darrick J. Wong
0 siblings, 1 reply; 59+ messages in thread
From: Bernd Schubert @ 2026-03-25 7:52 UTC (permalink / raw)
To: Darrick J. Wong, Bernd Schubert
Cc: linux-fsdevel@vger.kernel.org, Miklos Szeredi, Joanne Koong,
Kevin Chen
On 3/24/26 23:50, Darrick J. Wong wrote:
> On Tue, Mar 24, 2026 at 08:42:36PM +0000, Bernd Schubert wrote:
>> On 3/24/26 01:03, Darrick J. Wong wrote:
>>> On Mon, Mar 23, 2026 at 06:45:07PM +0100, Bernd Schubert wrote:
>>>> From: Bernd Schubert <bschubert@ddn.com>
>>>>
>>>> Add synchronous FUSE_INIT processing during mount() to
>>>> enable early daemonization with proper error reporting
>>>> to the parent process.
>>>>
>>>> A new mount thread is needed that handles FUSE_INIT and
>>>> possible other requests at mount time (like getxattr for selinux).
>>>> The kernel sends FUSE_INIT during the mount() syscall. Without a thread
>>>> to process it, mount() blocks forever.
>>>>
>>>> Mount thread lifetime:
>>>> Created before mount() syscall in fuse_start_sync_init_worker()
>>>> Processes requests until se->mount_finished is set (after mount() returns)
>>>> Joined after successful mount in fuse_wait_sync_init_completion()
>>>> Cancelled if mount fails (direct → fusermount3 fallback)
>>>
>>> Hrmm, so the main thread (of the child process after daemonization)
>>> calls mount(), having started a child thread to read and process
>>> FUSE_INIT + any other mount-related fuse requests?
>>>
>>> Later I see an eventfd being used to signal this background thread that
>>> it should exit and (presumably) let the real request processing queues
>>> take over. I'm curious why an eventfd here when pipes were used
>>> earlier?
>>
>> We cannot use eventfds for anything possibly used by BSD - which is
>
> (Yeah, I was wondering about eventfd, but I don't know how quickly
> libfuse features get ported to BSD so for all I know it could be years
> until BSD gets SYNC_INIT.)
>
>> fuse_daemonize_ and fusermount. So far sync-init is only in the new
>> mount API - linux only. We need to switch to a pipe if BSD ever gets that.
>
> I wonder, are the socket communications between fusermount-libfuse
> considered external interface? I'm assuming it's not, and you can
> change it as long as you don't break the other side?
I know that fusermount3 is also used by other userspace implementations
like https://github.com/cberner/fuser
I would need to go through that code to see how it interacts exactly
(had looked into a few months back, but don't remember the details
anymore - that were some readmes would be really helpful). My assumption
is that once we have a released interface in place we cannot easily
change it anymore.
Thanks,
Bernd
^ permalink raw reply [flat|nested] 59+ messages in thread
* Re: [PATCH 12/19] fuse mount: Support synchronous FUSE_INIT (privileged daemon)
2026-03-25 7:52 ` Bernd Schubert
@ 2026-03-25 16:42 ` Darrick J. Wong
0 siblings, 0 replies; 59+ messages in thread
From: Darrick J. Wong @ 2026-03-25 16:42 UTC (permalink / raw)
To: Bernd Schubert
Cc: Bernd Schubert, linux-fsdevel@vger.kernel.org, Miklos Szeredi,
Joanne Koong, Kevin Chen
On Wed, Mar 25, 2026 at 08:52:59AM +0100, Bernd Schubert wrote:
>
>
> On 3/24/26 23:50, Darrick J. Wong wrote:
> > On Tue, Mar 24, 2026 at 08:42:36PM +0000, Bernd Schubert wrote:
> >> On 3/24/26 01:03, Darrick J. Wong wrote:
> >>> On Mon, Mar 23, 2026 at 06:45:07PM +0100, Bernd Schubert wrote:
> >>>> From: Bernd Schubert <bschubert@ddn.com>
> >>>>
> >>>> Add synchronous FUSE_INIT processing during mount() to
> >>>> enable early daemonization with proper error reporting
> >>>> to the parent process.
> >>>>
> >>>> A new mount thread is needed that handles FUSE_INIT and
> >>>> possible other requests at mount time (like getxattr for selinux).
> >>>> The kernel sends FUSE_INIT during the mount() syscall. Without a thread
> >>>> to process it, mount() blocks forever.
> >>>>
> >>>> Mount thread lifetime:
> >>>> Created before mount() syscall in fuse_start_sync_init_worker()
> >>>> Processes requests until se->mount_finished is set (after mount() returns)
> >>>> Joined after successful mount in fuse_wait_sync_init_completion()
> >>>> Cancelled if mount fails (direct → fusermount3 fallback)
> >>>
> >>> Hrmm, so the main thread (of the child process after daemonization)
> >>> calls mount(), having started a child thread to read and process
> >>> FUSE_INIT + any other mount-related fuse requests?
> >>>
> >>> Later I see an eventfd being used to signal this background thread that
> >>> it should exit and (presumably) let the real request processing queues
> >>> take over. I'm curious why an eventfd here when pipes were used
> >>> earlier?
> >>
> >> We cannot use eventfds for anything possibly used by BSD - which is
> >
> > (Yeah, I was wondering about eventfd, but I don't know how quickly
> > libfuse features get ported to BSD so for all I know it could be years
> > until BSD gets SYNC_INIT.)
> >
> >> fuse_daemonize_ and fusermount. So far sync-init is only in the new
> >> mount API - linux only. We need to switch to a pipe if BSD ever gets that.
> >
> > I wonder, are the socket communications between fusermount-libfuse
> > considered external interface? I'm assuming it's not, and you can
> > change it as long as you don't break the other side?
>
> I know that fusermount3 is also used by other userspace implementations
> like https://github.com/cberner/fuser
>
> I would need to go through that code to see how it interacts exactly
> (had looked into a few months back, but don't remember the details
> anymore - that were some readmes would be really helpful). My assumption
> is that once we have a released interface in place we cannot easily
> change it anymore.
Heh. Right, fusermount3 is in a bin directory, so yeah, anyone can exec
it and presumably they won't be happy if it starts speaking gibberish.
With sufficient motivation we could employ the neat systemd hack of
setting xattrs on the socket to proclaim new protocol versions ;)
--D
>
> Thanks,
> Bernd
>
^ permalink raw reply [flat|nested] 59+ messages in thread
* Re: [PATCH 17/19] Make fusermount work bidirectional for sync init
2026-03-24 22:59 ` Darrick J. Wong
@ 2026-03-25 19:48 ` Bernd Schubert
2026-03-25 22:03 ` Darrick J. Wong
0 siblings, 1 reply; 59+ messages in thread
From: Bernd Schubert @ 2026-03-25 19:48 UTC (permalink / raw)
To: Darrick J. Wong
Cc: linux-fsdevel, Miklos Szeredi, Joanne Koong, Bernd Schubert
On 3/24/26 23:59, Darrick J. Wong wrote:
> On Tue, Mar 24, 2026 at 10:24:04PM +0100, Bernd Schubert wrote:
>>
>>
>> On 3/24/26 20:35, Darrick J. Wong wrote:
>>> On Mon, Mar 23, 2026 at 06:45:12PM +0100, Bernd Schubert wrote:
>>>> From: Bernd Schubert <bschubert@ddn.com>
>>>>
>>>> Signed-off-by: Bernd Schubert <bschubert@ddn.com>
>>>> ---
>>>> doc/README.fusermount | 359 ++++++++++++++++++++++++++++++++++++++++++++++++++
>>>> util/fusermount.c | 317 ++++++++++++++++++++++++++++++++++++++++++--
>>>> util/meson.build | 2 +-
>>>> 3 files changed, 665 insertions(+), 13 deletions(-)
>>>>
>>>> diff --git a/doc/README.fusermount b/doc/README.fusermount
>>>> new file mode 100644
>>>> index 0000000000000000000000000000000000000000..54a3bac4f58964a4ed312d6f6bc15606fed1e647
>>>> --- /dev/null
>>>> +++ b/doc/README.fusermount
>>>> @@ -0,0 +1,359 @@
>>>> +Synchronous FUSE_INIT Protocol
>>>> +================================
>>>> +
>>>> +Overview
>>>> +--------
>>>> +
>>>> +The sync-init feature enables the FUSE library to start worker threads and
>>>> +perform initialization ioctl calls BEFORE the actual mount() syscall happens.
>>>> +This is required for the kernel's synchronous FUSE_INIT feature, where the
>>>> +mount() syscall blocks until the FUSE daemon processes the INIT request.
>>>> +
>>>> +Without this feature, there would be a deadlock:
>>>> +- mount() blocks waiting for INIT response
>>>> +- Worker threads can't start because mount() hasn't returned
>>>> +- INIT request can't be processed because worker threads aren't running
>>>> +
>>>> +
>>>> +Protocol Flow
>>>> +-------------
>>>> +
>>>> +Traditional mount flow:
>>>> + 1. Library calls fusermount3
>>>
>>> Heh. I haven't looked much at fusermount until recently. I gather that
>>> fuservicemount has somewhat similar goals to fusermount3? fusermount3
>>> seems to be a helper subprocess that libfuse can invoke on behalf of an
>>> unprivileged fuse server. The helper is responsible for:
>>>
>>> 1) opening /dev/fuse
>>> 2) sending it to the fuse server via the FUSE_COMMFD_ENV fd which is
>>> supposed to be an AF_UNIX socket
>>> 3) calling mount()
>>> 4) waiting for the parent to die
>>> 5) maybe calling unmount()
>>>
>>> and it's really 1, 3, and 5 that need to be privileged, so that's why
>>> it's a setuid program.
>>>
>>>> + 2. fusermount3 opens /dev/fuse
>>>> + 3. fusermount3 performs mount() syscall
>>>> + 4. fusermount3 sends fd to library
>>>> + 5. Library starts worker threads
>>>> + 6. Worker threads process FUSE requests
>>>
>>> Ah, yes. Thanks for adding this description! fuservicemount is I think
>>> an upside-down version of fusermount -- fuservicemount runs in the
>>> user's mount namespace, so it
>>>
>>> 1) connects to a named AF_UNIX socket to start an instance of the fuse
>>> server
>>> 2) opens /dev/fuse and a memfd to pass cli arguments
>>> 3) passes those to the fuse server
>>> 4) the fuse server asks fuservicemount to open resources and pass them
>>> over the socket
>>> 5) the fuse server passes source/type/mount options to fuservicemount
>>> 6) fuservicemount mounts the fs and exits
>>> 7) at some point the user unmounts, so the fuse server exits
>>>
>>>> +Sync-init mount flow:
>>>> + 1. Library calls fusermount3 with --sync-init flag
>>>> + 2. fusermount3 opens /dev/fuse
>>>> + 3. fusermount3 sends fd to library
>>>> + 4. Library receives fd
>>>> + 5. Library performs FUSE_DEV_IOC_SYNC_INIT ioctl
>>>> + 6. Library starts worker threads
>>>> + 7. Library sends "proceed" signal to fusermount3
>>>> + 8. fusermount3 performs mount() syscall (blocks until INIT completes)
>>>> + 9. Worker threads process INIT request
>>>> + 10. mount() syscall completes
>>>> + 11. fusermount3 exits
>>>> +
>>>> +
>>>> +Implementation Details
>>>> +----------------------
>>>> +
>>>> +Bidirectional Communication:
>>>> + - Uses the existing unix socket (_FUSE_COMMFD environment variable)
>>>> + - Simple 1-byte protocol for signaling
>>>> + - Library signals fusermount3 when ready to proceed with mount
>>>> +
>>>> +fusermount3 Changes:
>>>> + - New --sync-init command-line option
>>>> + - Split mount operation into two phases:
>>>> + * mount_fuse_prepare(): Opens device, prepares parameters
>>>> + * mount_fuse_finish_fsmount(): Performs actual mount() syscall
>>>> + - wait_for_signal(): Waits for library to signal readiness
>>>> + - struct mount_context: Preserves state between phases
>>>> +
>>>> +Library Changes:
>>>> + - fuse_session_mount_new_api(): Uses new protocol when available
>>>> + - Sends "proceed" signal after worker thread is ready
>>>> + - Handles both old and new mount protocols for compatibility
>>>> +
>>>> +
>>>> +Backward Compatibility
>>>> +----------------------
>>>> +
>>>> +The implementation maintains full backward compatibility:
>>>> + - Old library + new fusermount3: Works (uses traditional flow)
>>>> + - New library + old fusermount3: Falls back to traditional flow
>>>> + - New library + new fusermount3: Uses sync-init flow when appropriate
>>>> +
>>>> +
>>>> +Error Handling
>>>> +--------------
>>>> +
>>>> +If any step fails during the sync-init flow:
>>>> + - fusermount3 closes the fd and exits with error
>>>> + - Library detects failure and cleans up
>>>> + - No mount is left in inconsistent state
>>>> +
>>>> +Connection closure:
>>>> + - If library closes socket before signaling, fusermount3 detects and exits
>>>> + - If fusermount3 crashes, library detects closed socket
>>>> +
>>>> +
>>>> +Security Considerations
>>>> +-----------------------
>>>> +
>>>> +The sync-init protocol does not introduce new security concerns:
>>>> + - Uses the same privilege separation as traditional mount
>>>> + - Socket communication is already established and trusted
>>>> + - No new privileged operations are added
>>>> + - File descriptor passing uses existing SCM_RIGHTS mechanism
>>>> +
>>>> +
>>>> +Performance Impact
>>>> +------------------
>>>> +
>>>> +Minimal performance impact:
>>>> + - One additional recv() call in fusermount3
>>>> + - One additional send() call in library
>>>> + - Total overhead: ~2 context switches
>>>> + - Only affects mount time, not runtime performance
>>>> +
>>>> +
>>>> +Future Enhancements
>>>> +-------------------
>>>> +
>>>> +Potential improvements:
>>>> + - Extended protocol for more complex initialization sequences
>>>> + - Support for multiple worker threads coordination
>>>> + - Enhanced error reporting through the socket
>>>> + - Timeout mechanisms for detecting hung initialization
>>>> +
>>>> +
>>>> +ASCII Workflow Diagrams
>>>> +========================
>>>> +
>>>> +1. Traditional Mount Flow (without --sync-init, async INIT)
>>>> +------------------------------------------------------------
>>>> +
>>>> +Library fusermount3 Kernel
>>>> + | | |
>>>> + |--- spawn fusermount3 ---->| |
>>>> + | | |
>>>> + | [open /dev/fuse] |
>>>> + | |------- open -------->|
>>>> + | |<------ fd ---------- |
>>>> + | | |
>>>> + | [mount() syscall] |
>>>> + | |------ mount -------->|
>>>> + | |<----- success ------ | [mount returns immediately]
>>>> + | | | [INIT queued in kernel]
>>>> + | [send_fd(fd)] |
>>>> + |<------- fd --------------| |
>>>> + | | |
>>>> + | [fusermount3 exits] |
>>>> + | |
>>>> + | [start worker thread] |
>>>> + | [worker reads /dev/fuse] |
>>>> + |---------------------------------------- read -->|
>>>> + |<--------------------------------------- INIT ---| [dequeued from kernel]
>>>> + | |
>>>> + | OK: INIT was queued, worker reads it later |
>>>> + | Works fine for async INIT |
>>>
>>> Hmm, looking at this, perhaps it /is/ possible for fuservicemount to
>>> employ synchronous init. The fuse server would start that background
>>> init-only request handler thread before telling fuservicemount to call
>>> mount(). That blocks while the kernel sends FUSE_INIT to the fuse
>>> server, it processes everything up to the init request, and returns.
>>>
>>>> +
>>>> +
>>>> +1b. Problem: Synchronous INIT without --sync-init
>>>> +--------------------------------------------------
>>>> +
>>>> +Library fusermount3 Kernel
>>>> + | | |
>>>> + |--- spawn fusermount3 ---->| |
>>>> + | | |
>>>> + | [open /dev/fuse] |
>>>> + | |------- open -------->|
>>>> + | |<------ fd ---------- |
>>>> + | | |
>>>> + | [mount() syscall] |
>>>> + | |------ mount -------->|
>>>> + | | | [mount BLOCKS waiting for INIT]
>>>> + | | (BLOCKED) | [needs worker to process INIT]
>>>> + | | |
>>>> + | [waiting for fd...] | |
>>>> + | | |
>>>> + | | |
>>>> + | DEADLOCK: mount() waits for INIT response |
>>>> + | but worker thread not started yet |
>>>> + | because we're waiting for fd |
>>>> +
>>>> +
>>>> +2. Sync-Init Mount Flow (with --sync-init)
>>>> +-------------------------------------------
>>>> +
>>>> +Library fusermount3 Kernel
>>>> + | | |
>>>> + |--- spawn fusermount3 ---->| |
>>>> + | with --sync-init | |
>>>> + | | |
>>>> + | [open /dev/fuse] |
>>>> + | |------- open -------->|
>>>> + | |<------ fd ---------- |
>>>> + | | |
>>>> + | [send_fd(fd)] |
>>>> + |<------- fd --------------| |
>>>> + | | |
>>>> + | [wait_for_signal()] |
>>>> + | | (BLOCKED) |
>>>> + | | |
>>>> + | [ioctl SYNC_INIT] | |
>>>> + |---------------------------------------- ioctl -->|
>>>> + | |
>>>> + | [start worker thread] |
>>>> + | [worker ready] |
>>>> + | | |
>>>> + |--- "proceed" signal ----->| |
>>>> + | [signal received] |
>>>> + | | |
>>>> + | [mount() syscall] |
>>>> + | |------ mount -------->|
>>>> + | | | [mount blocks]
>>>> + | | | [sends INIT]
>>>> + |<------------------------------------------------ |
>>>> + | | |
>>>> + | [worker processes INIT] | |
>>>> + |------------------------------------------------->|
>>>> + | | | [mount unblocks]
>>>> + | |<----- success ------ |
>>>> + | | |
>>>> + | [fusermount3 exits] |
>>>> + | |
>>>> + | SUCCESS: Worker ready before mount() |
>>>> + | INIT processed synchronously |
>>>> +
>>>> +
>>>> +3. Error Scenario: Library Crashes Before Signaling
>>>> +----------------------------------------------------
>>>> +
>>>> +Library fusermount3 Kernel
>>>> + | | |
>>>> + |--- spawn fusermount3 ---->| |
>>>> + | with --sync-init | |
>>>> + | | |
>>>> + | [open /dev/fuse] |
>>>> + | |------- open -------->|
>>>> + | |<------ fd ---------- |
>>>> + | | |
>>>> + | [send_fd(fd)] |
>>>> + |<------- fd --------------| |
>>>> + | | |
>>>> + | [wait_for_signal()] |
>>>> + | | (BLOCKED) |
>>>> + | | |
>>>> + X [library crashes] | |
>>>> + | | |
>>>> + | [recv() returns 0] |
>>>> + | [socket closed] |
>>>> + | | |
>>>> + | [cleanup and exit] |
>>>> + | X |
>>>> + | |
>>>> + | RESULT: Clean failure, no mount performed |
>>>> +
>>>> +
>>>> +4. Detailed Function Call Flow
>>>> +-------------------------------
>>>> +
>>>> +Library (lib/fuse_lowlevel.c):
>>>> +fuse_session_mount_new_api()
>>>> + |
>>>> + +-- fuse_kern_mount_prepare() [lib/mount.c]
>>>> + | |
>>>> + | +-- fuse_mount_fusermount() [lib/mount_util.c]
>>>> + | |
>>>> + | +-- socketpair() [create comm socket]
>>>> + | |
>>>> + | +-- fork()
>>>> + | |
>>>> + | +-- [child] execl("fusermount3", "--sync-init", ...)
>>>> + | |
>>>> + | +-- [parent] receive_fd() <--- BLOCKS until fd arrives
>>>> + | |
>>>> + | +-- recvmsg(SCM_RIGHTS)
>>>> + | |
>>>> + | +-- return fd
>>>> + |
>>>> + +-- session_start_sync_init() [lib/fuse_lowlevel.c]
>>>> + | |
>>>> + | +-- ioctl(fd, FUSE_DEV_IOC_SYNC_INIT)
>>>> + | |
>>>> + | +-- pthread_create(worker_thread)
>>>> + | |
>>>> + | +-- return
>>>> + |
>>>> + +-- fuse_fusermount_proceed_mnt(socket) [lib/mount.c] <--- NEW: Bidirectional handshake
>>>> + |
>>>> + +-- send(socket, "proceed", 1) <--- Signal fusermount3 to proceed
>>>> + |
>>>> + +-- recv(socket, &status, 1) <--- BLOCKS until mount result arrives
>>>> + | |
>>>> + | +-- [fusermount3 performs mount and sends status byte]
>>>> + |
>>>> + +-- if (status != 0) return -1 <--- Mount failed
>>>> + |
>>>> + +-- return 0 <--- Mount succeeded
>>>> +
>>>> +
>>>> +Utility (util/fusermount.c):
>>>> +fusermount3 main() with --sync-init
>>>> + |
>>>> + +-- mount_fuse_sync_init() [util/fusermount.c]
>>>> + |
>>>> + +-- mount_fuse_prepare() [util/fusermount.c]
>>>> + | |
>>>> + | +-- open("/dev/fuse")
>>>> + | |
>>>> + | +-- check_perm() [util/fusermount.c]
>>>> + | |
>>>> + | +-- return fd
>>>> + |
>>>> + +-- send_fd(socket, fd) [util/fusermount.c]
>>>> + | |
>>>> + | +-- sendmsg(SCM_RIGHTS)
>>>> + |
>>>> + +-- wait_for_signal(socket) [util/fusermount.c] <--- BLOCKS until library signals
>>>> + | |
>>>> + | +-- recv(socket, buf, 1)
>>>> + | |
>>>> + | +-- return 0
>>>> + |
>>>> + +-- mount_fuse_finish_fsmount() [util/fusermount.c]
>>>> + | |
>>>> + | +-- fuse_kern_fsmount() [lib/mount_fsmount.c]
>>>> + | | |
>>>> + | | +-- fsopen("fuse", FSOPEN_CLOEXEC)
>>>> + | | | |
>>>> + | | | +-- [kernel creates filesystem context]
>>>> + | | |
>>>> + | | +-- fsconfig(fsfd, SET_STRING, "source", ...)
>>>> + | | +-- fsconfig(fsfd, SET_STRING, "fd", fd_value, ...)
>>>> + | | +-- fsconfig(fsfd, ...) [apply mount options]
>>>> + | | +-- fsconfig(fsfd, CMD_CREATE, ...)
>>>> + | | |
>>>> + | | +-- fsmount(fsfd, FSMOUNT_CLOEXEC, mount_attrs)
>>>> + | | | |
>>>> + | | | +-- [kernel sends FUSE_INIT here]
>>>> + | | | |
>>>> + | | | +-- [worker thread processes INIT]
>>>> + | | | |
>>>> + | | | +-- [fsmount returns mntfd]
>>>> + | | |
>>>> + | | +-- move_mount(mntfd, "", AT_FDCWD, target, ...)
>>>> + | | | |
>>>> + | | | +-- [attach mount to target directory]
>>>> + | | | |
>>>> + | | | +-- [no blocking - INIT already processed]
>>>> + | | |
>>>> + | | +-- add_mount() [lib/mount_fsmount.c - update /etc/mtab]
>>>> + | | |
>>>> + | | +-- return 0 on success, -1 on failure
>>>> + | |
>>>> + | +-- if mount failed: return -1
>>>> + | +-- if mount succeeded: continue
>>>> + |
>>>> + +-- send_status_byte(socket) [util/fusermount.c] <--- NEW: Send result to library
>>>> + | |
>>>> + | +-- status = (mount_result == 0) ? 0 : 1
>>>> + | +-- send(socket, &status, 1)
>>>> + | |
>>>> + | +-- return
>>>> + |
>>>> + +-- return 0
>>>> +
>>>> +
>>>> +Note: The new mount API (fsopen/fsconfig/fsmount/move_mount) is REQUIRED
>>>> + for sync-init because fsmount() triggers FUSE_INIT before the mount
>>>> + is attached. This allows the worker thread to process INIT before
>>>> + move_mount() completes, preventing deadlock.
>>>
>>> ...and so we don't expose the directory tree to the mountns until we
>>> know that FUSE_INIT didn't crash the server.
>>
>> Added that as well.
>>
>>>
>>>> diff --git a/util/fusermount.c b/util/fusermount.c
>>>> index 80b42a594e89cdc2f43824f5e274892522fd8cce..808b4afd89ceb49273c944d43bffe5033e27549b 100644
>>>> --- a/util/fusermount.c
>>>> +++ b/util/fusermount.c
>>>> @@ -957,6 +957,7 @@ static void free_mount_params(struct mount_params *mp)
>>>> free(mp->source);
>>>> free(mp->type);
>>>> free(mp->mnt_opts);
>>>> + memset(mp, 0, sizeof(*mp));
>>>> }
>>>>
>>>> /*
>>>> @@ -1378,6 +1379,179 @@ static int open_fuse_device(const char *dev)
>>>> return fd;
>>>> }
>>>>
>>>> +#ifdef HAVE_NEW_MOUNT_API
>>>> +/* Forward declaration from lib/mount_fsmount.c */
>>>> +int fuse_kern_fsmount(const char *mnt, unsigned long flags, int blkdev,
>>>> + const char *fsname, const char *subtype,
>>>> + const char *source_dev, const char *kernel_opts,
>>>> + const char *mnt_opts);
>>>> +#endif
>>>
>>> Shouldn't this be included from a header file somewhere?
>>
>> Fixed, included from mount_i_linux.h
>>
>>>
>>>> +
>>>> +/*
>>>> + * Context for split mount operation (sync-init mode)
>>>> + */
>>>> +struct mount_context {
>>>> + int fd;
>>>> + const char *dev;
>>>> + struct stat stbuf;
>>>> + char *source;
>>>> + char *mnt_opts;
>>>> + char *x_opts;
>>>> + const char *type;
>>>> +};
>>>> +
>>>> +/*
>>>> + * Phase 1: Open device and prepare for mount (sync-init mode)
>>>> + * Returns fd on success, -1 on failure
>>>> + */
>>>> +static int mount_fuse_prepare(const char *mnt, const char *opts,
>>>> + struct mount_context *ctx)
>>>> +{
>>>> + int res;
>>>> + int mountpoint_fd = -1;
>>>> + char *do_mount_opts = NULL;
>>>> + const char *real_mnt = mnt;
>>>> +
>>>> + memset(ctx, 0, sizeof(*ctx));
>>>> + ctx->dev = getenv(FUSE_KERN_DEVICE_ENV) ?: FUSE_DEV;
>>>> +
>>>> + ctx->fd = open_fuse_device(ctx->dev);
>>>> + if (ctx->fd == -1)
>>>> + return -1;
>>>> +
>>>> + drop_privs();
>>>> + read_conf();
>>>> +
>>>> + if (getuid() != 0 && mount_max != -1) {
>>>> + int mount_count = count_fuse_fs();
>>>> +
>>>> + if (mount_count >= mount_max) {
>>>> + fprintf(stderr,
>>>> + "%s: too many FUSE filesystems mounted; mount_max=N can be set in %s\n",
>>>> + progname, FUSE_CONF);
>>>> + goto fail_close_fd;
>>>> + }
>>>> + }
>>>
>>> /me notes that he's refactored this configuration file related function
>>> into fuser_conf.c though that's in the fuse-services v4 that I'll send
>>> you soon.
>>>
>>>> +
>>>> + res = extract_x_options(opts, &do_mount_opts, &ctx->x_opts);
>>>> + if (res)
>>>> + goto fail_close_fd;
>>>> +
>>>> + res = check_perm(&real_mnt, &ctx->stbuf, &mountpoint_fd);
>>>> + restore_privs();
>>>> +
>>>> + if (mountpoint_fd != -1)
>>>> + close(mountpoint_fd);
>>>> +
>>>> + if (res == -1)
>>>> + goto fail_close_fd;
>>>> +
>>>> + free(do_mount_opts);
>>>> + return ctx->fd;
>>>> +
>>>> +fail_close_fd:
>>>> + close(ctx->fd);
>>>> + free(do_mount_opts);
>>>> + free(ctx->x_opts);
>>>> + ctx->fd = -1;
>>>> + return -1;
>>>> +}
>>>> +
>>>> +#ifdef HAVE_NEW_MOUNT_API
>>>> +/*
>>>> + * Phase 2: Perform the actual mount using new mount API (sync-init mode)
>>>> + * Returns 0 on success, -1 on failure
>>>> + */
>>>> +static int mount_fuse_finish_fsmount(const char *mnt, const char *opts,
>>>> + struct mount_context *ctx,
>>>> + const char **type)
>>>> +{
>>>> + int res;
>>>> + char *do_mount_opts = NULL;
>>>> + char *x_prefixed_opts = NULL;
>>>> + struct mount_params mp = { .fd = ctx->fd };
>>>> + char *final_mnt_opts = NULL;
>>>> +
>>>> + /* Extract x-options */
>>>> + res = extract_x_options(opts, &do_mount_opts, &x_prefixed_opts);
>>>> + if (res)
>>>> + goto fail;
>>>> +
>>>> + /* Prepare mount parameters */
>>>> + mp.rootmode = ctx->stbuf.st_mode & S_IFMT;
>>>> + mp.dev = ctx->dev;
>>>
>>> I think those could be set in the mp variable definition?
>>
>> Absolutely.
>>
>>>
>>>> +
>>>> + res = prepare_mount(do_mount_opts, &mp);
>>>> + if (res == -1)
>>>> + goto fail;
>>>> +
>>>> + /* Merge x-options if running as root */
>>>> + final_mnt_opts = mp.mnt_opts;
>>>> + if (geteuid() == 0 && ctx->x_opts && strlen(ctx->x_opts) > 0) {
>>>> + size_t mnt_opts_len = strlen(mp.mnt_opts);
>>>> + size_t x_mnt_opts_len = mnt_opts_len + strlen(ctx->x_opts) + 2;
>>>> + char *x_mnt_opts = calloc(1, x_mnt_opts_len);
>>>> +
>>>> + if (!x_mnt_opts)
>>>> + goto fail_free_params;
>>>> +
>>>> + if (mnt_opts_len) {
>>>> + strcpy(x_mnt_opts, mp.mnt_opts);
>>>> + strncat(x_mnt_opts, ",", 2);
>>>> + }
>>>> + strncat(x_mnt_opts, ctx->x_opts,
>>>> + x_mnt_opts_len - mnt_opts_len - 2);
>>>> +
>>>> + final_mnt_opts = x_mnt_opts;
>>>> + }
>>>
>>> Curious, I thought the x- options were edited out by /sbin/mount so fuse
>>> would never see them? Does the x- option handling in fusermount.c exist
>>> to handle the case where someone passes them directly to the fuse
>>> server, aka
>>>
>>> $ sshfs <whatever> /mnt -o x-systemd-hahaha=1
>>>
>>> and now you need to ensure that x-systemd-hahaha doesn't get sent to the
>>> kernel but does get seen by the fuse server?
>>
>> https://github.com/libfuse/libfuse/issues/651
>>
>> So specially added as mount option to to go into mtab/utab and to
>> suppress some 3rd party (gnome) actions.
>
> Huh, I wouldn't have thought that would work since /etc/mtab has been
> a symlink to /proc/self/mounts for a while now. Of course GNOME
> abstracts mount options behind some g_unix_mount_entry_get_options
> function call which ... doesn't document where it gets its mount options
> from.
Problem is that these don't go into /proc/self/mounts at all, but into
/run/mount/utab with recent mount utils. With /etc/mtab not being a
symlink it would have worked as well.
bernd@e7270 ~>cat /run/mount/utab
SRC=/dev/loop0 TARGET=/snap/chromium/3375 ROOT=/ OPTS=x-gdu.hide,x-gvfs-hide
SRC=/dev/loop1 TARGET=/snap/bare/5 ROOT=/ OPTS=x-gdu.hide,x-gvfs-hide
SRC=/dev/loop2 TARGET=/snap/chromium/3390 ROOT=/ OPTS=x-gdu.hide,x-gvfs-hide
...
^ permalink raw reply [flat|nested] 59+ messages in thread
* Re: [PATCH 17/19] Make fusermount work bidirectional for sync init
2026-03-25 19:48 ` Bernd Schubert
@ 2026-03-25 22:03 ` Darrick J. Wong
0 siblings, 0 replies; 59+ messages in thread
From: Darrick J. Wong @ 2026-03-25 22:03 UTC (permalink / raw)
To: Bernd Schubert
Cc: linux-fsdevel, Miklos Szeredi, Joanne Koong, Bernd Schubert
On Wed, Mar 25, 2026 at 08:48:32PM +0100, Bernd Schubert wrote:
>
>
> On 3/24/26 23:59, Darrick J. Wong wrote:
> > On Tue, Mar 24, 2026 at 10:24:04PM +0100, Bernd Schubert wrote:
> >>
> >>
> >> On 3/24/26 20:35, Darrick J. Wong wrote:
> >>> On Mon, Mar 23, 2026 at 06:45:12PM +0100, Bernd Schubert wrote:
> >>>> From: Bernd Schubert <bschubert@ddn.com>
> >>>>
> >>>> Signed-off-by: Bernd Schubert <bschubert@ddn.com>
> >>>> ---
> >>>> doc/README.fusermount | 359 ++++++++++++++++++++++++++++++++++++++++++++++++++
> >>>> util/fusermount.c | 317 ++++++++++++++++++++++++++++++++++++++++++--
> >>>> util/meson.build | 2 +-
> >>>> 3 files changed, 665 insertions(+), 13 deletions(-)
> >>>>
> >>>> diff --git a/doc/README.fusermount b/doc/README.fusermount
> >>>> new file mode 100644
> >>>> index 0000000000000000000000000000000000000000..54a3bac4f58964a4ed312d6f6bc15606fed1e647
> >>>> --- /dev/null
> >>>> +++ b/doc/README.fusermount
> >>>> @@ -0,0 +1,359 @@
> >>>> +Synchronous FUSE_INIT Protocol
> >>>> +================================
> >>>> +
> >>>> +Overview
> >>>> +--------
> >>>> +
> >>>> +The sync-init feature enables the FUSE library to start worker threads and
> >>>> +perform initialization ioctl calls BEFORE the actual mount() syscall happens.
> >>>> +This is required for the kernel's synchronous FUSE_INIT feature, where the
> >>>> +mount() syscall blocks until the FUSE daemon processes the INIT request.
> >>>> +
> >>>> +Without this feature, there would be a deadlock:
> >>>> +- mount() blocks waiting for INIT response
> >>>> +- Worker threads can't start because mount() hasn't returned
> >>>> +- INIT request can't be processed because worker threads aren't running
> >>>> +
> >>>> +
> >>>> +Protocol Flow
> >>>> +-------------
> >>>> +
> >>>> +Traditional mount flow:
> >>>> + 1. Library calls fusermount3
> >>>
> >>> Heh. I haven't looked much at fusermount until recently. I gather that
> >>> fuservicemount has somewhat similar goals to fusermount3? fusermount3
> >>> seems to be a helper subprocess that libfuse can invoke on behalf of an
> >>> unprivileged fuse server. The helper is responsible for:
> >>>
> >>> 1) opening /dev/fuse
> >>> 2) sending it to the fuse server via the FUSE_COMMFD_ENV fd which is
> >>> supposed to be an AF_UNIX socket
> >>> 3) calling mount()
> >>> 4) waiting for the parent to die
> >>> 5) maybe calling unmount()
> >>>
> >>> and it's really 1, 3, and 5 that need to be privileged, so that's why
> >>> it's a setuid program.
> >>>
> >>>> + 2. fusermount3 opens /dev/fuse
> >>>> + 3. fusermount3 performs mount() syscall
> >>>> + 4. fusermount3 sends fd to library
> >>>> + 5. Library starts worker threads
> >>>> + 6. Worker threads process FUSE requests
> >>>
> >>> Ah, yes. Thanks for adding this description! fuservicemount is I think
> >>> an upside-down version of fusermount -- fuservicemount runs in the
> >>> user's mount namespace, so it
> >>>
> >>> 1) connects to a named AF_UNIX socket to start an instance of the fuse
> >>> server
> >>> 2) opens /dev/fuse and a memfd to pass cli arguments
> >>> 3) passes those to the fuse server
> >>> 4) the fuse server asks fuservicemount to open resources and pass them
> >>> over the socket
> >>> 5) the fuse server passes source/type/mount options to fuservicemount
> >>> 6) fuservicemount mounts the fs and exits
> >>> 7) at some point the user unmounts, so the fuse server exits
> >>>
> >>>> +Sync-init mount flow:
> >>>> + 1. Library calls fusermount3 with --sync-init flag
> >>>> + 2. fusermount3 opens /dev/fuse
> >>>> + 3. fusermount3 sends fd to library
> >>>> + 4. Library receives fd
> >>>> + 5. Library performs FUSE_DEV_IOC_SYNC_INIT ioctl
> >>>> + 6. Library starts worker threads
> >>>> + 7. Library sends "proceed" signal to fusermount3
> >>>> + 8. fusermount3 performs mount() syscall (blocks until INIT completes)
> >>>> + 9. Worker threads process INIT request
> >>>> + 10. mount() syscall completes
> >>>> + 11. fusermount3 exits
> >>>> +
> >>>> +
> >>>> +Implementation Details
> >>>> +----------------------
> >>>> +
> >>>> +Bidirectional Communication:
> >>>> + - Uses the existing unix socket (_FUSE_COMMFD environment variable)
> >>>> + - Simple 1-byte protocol for signaling
> >>>> + - Library signals fusermount3 when ready to proceed with mount
> >>>> +
> >>>> +fusermount3 Changes:
> >>>> + - New --sync-init command-line option
> >>>> + - Split mount operation into two phases:
> >>>> + * mount_fuse_prepare(): Opens device, prepares parameters
> >>>> + * mount_fuse_finish_fsmount(): Performs actual mount() syscall
> >>>> + - wait_for_signal(): Waits for library to signal readiness
> >>>> + - struct mount_context: Preserves state between phases
> >>>> +
> >>>> +Library Changes:
> >>>> + - fuse_session_mount_new_api(): Uses new protocol when available
> >>>> + - Sends "proceed" signal after worker thread is ready
> >>>> + - Handles both old and new mount protocols for compatibility
> >>>> +
> >>>> +
> >>>> +Backward Compatibility
> >>>> +----------------------
> >>>> +
> >>>> +The implementation maintains full backward compatibility:
> >>>> + - Old library + new fusermount3: Works (uses traditional flow)
> >>>> + - New library + old fusermount3: Falls back to traditional flow
> >>>> + - New library + new fusermount3: Uses sync-init flow when appropriate
> >>>> +
> >>>> +
> >>>> +Error Handling
> >>>> +--------------
> >>>> +
> >>>> +If any step fails during the sync-init flow:
> >>>> + - fusermount3 closes the fd and exits with error
> >>>> + - Library detects failure and cleans up
> >>>> + - No mount is left in inconsistent state
> >>>> +
> >>>> +Connection closure:
> >>>> + - If library closes socket before signaling, fusermount3 detects and exits
> >>>> + - If fusermount3 crashes, library detects closed socket
> >>>> +
> >>>> +
> >>>> +Security Considerations
> >>>> +-----------------------
> >>>> +
> >>>> +The sync-init protocol does not introduce new security concerns:
> >>>> + - Uses the same privilege separation as traditional mount
> >>>> + - Socket communication is already established and trusted
> >>>> + - No new privileged operations are added
> >>>> + - File descriptor passing uses existing SCM_RIGHTS mechanism
> >>>> +
> >>>> +
> >>>> +Performance Impact
> >>>> +------------------
> >>>> +
> >>>> +Minimal performance impact:
> >>>> + - One additional recv() call in fusermount3
> >>>> + - One additional send() call in library
> >>>> + - Total overhead: ~2 context switches
> >>>> + - Only affects mount time, not runtime performance
> >>>> +
> >>>> +
> >>>> +Future Enhancements
> >>>> +-------------------
> >>>> +
> >>>> +Potential improvements:
> >>>> + - Extended protocol for more complex initialization sequences
> >>>> + - Support for multiple worker threads coordination
> >>>> + - Enhanced error reporting through the socket
> >>>> + - Timeout mechanisms for detecting hung initialization
> >>>> +
> >>>> +
> >>>> +ASCII Workflow Diagrams
> >>>> +========================
> >>>> +
> >>>> +1. Traditional Mount Flow (without --sync-init, async INIT)
> >>>> +------------------------------------------------------------
> >>>> +
> >>>> +Library fusermount3 Kernel
> >>>> + | | |
> >>>> + |--- spawn fusermount3 ---->| |
> >>>> + | | |
> >>>> + | [open /dev/fuse] |
> >>>> + | |------- open -------->|
> >>>> + | |<------ fd ---------- |
> >>>> + | | |
> >>>> + | [mount() syscall] |
> >>>> + | |------ mount -------->|
> >>>> + | |<----- success ------ | [mount returns immediately]
> >>>> + | | | [INIT queued in kernel]
> >>>> + | [send_fd(fd)] |
> >>>> + |<------- fd --------------| |
> >>>> + | | |
> >>>> + | [fusermount3 exits] |
> >>>> + | |
> >>>> + | [start worker thread] |
> >>>> + | [worker reads /dev/fuse] |
> >>>> + |---------------------------------------- read -->|
> >>>> + |<--------------------------------------- INIT ---| [dequeued from kernel]
> >>>> + | |
> >>>> + | OK: INIT was queued, worker reads it later |
> >>>> + | Works fine for async INIT |
> >>>
> >>> Hmm, looking at this, perhaps it /is/ possible for fuservicemount to
> >>> employ synchronous init. The fuse server would start that background
> >>> init-only request handler thread before telling fuservicemount to call
> >>> mount(). That blocks while the kernel sends FUSE_INIT to the fuse
> >>> server, it processes everything up to the init request, and returns.
> >>>
> >>>> +
> >>>> +
> >>>> +1b. Problem: Synchronous INIT without --sync-init
> >>>> +--------------------------------------------------
> >>>> +
> >>>> +Library fusermount3 Kernel
> >>>> + | | |
> >>>> + |--- spawn fusermount3 ---->| |
> >>>> + | | |
> >>>> + | [open /dev/fuse] |
> >>>> + | |------- open -------->|
> >>>> + | |<------ fd ---------- |
> >>>> + | | |
> >>>> + | [mount() syscall] |
> >>>> + | |------ mount -------->|
> >>>> + | | | [mount BLOCKS waiting for INIT]
> >>>> + | | (BLOCKED) | [needs worker to process INIT]
> >>>> + | | |
> >>>> + | [waiting for fd...] | |
> >>>> + | | |
> >>>> + | | |
> >>>> + | DEADLOCK: mount() waits for INIT response |
> >>>> + | but worker thread not started yet |
> >>>> + | because we're waiting for fd |
> >>>> +
> >>>> +
> >>>> +2. Sync-Init Mount Flow (with --sync-init)
> >>>> +-------------------------------------------
> >>>> +
> >>>> +Library fusermount3 Kernel
> >>>> + | | |
> >>>> + |--- spawn fusermount3 ---->| |
> >>>> + | with --sync-init | |
> >>>> + | | |
> >>>> + | [open /dev/fuse] |
> >>>> + | |------- open -------->|
> >>>> + | |<------ fd ---------- |
> >>>> + | | |
> >>>> + | [send_fd(fd)] |
> >>>> + |<------- fd --------------| |
> >>>> + | | |
> >>>> + | [wait_for_signal()] |
> >>>> + | | (BLOCKED) |
> >>>> + | | |
> >>>> + | [ioctl SYNC_INIT] | |
> >>>> + |---------------------------------------- ioctl -->|
> >>>> + | |
> >>>> + | [start worker thread] |
> >>>> + | [worker ready] |
> >>>> + | | |
> >>>> + |--- "proceed" signal ----->| |
> >>>> + | [signal received] |
> >>>> + | | |
> >>>> + | [mount() syscall] |
> >>>> + | |------ mount -------->|
> >>>> + | | | [mount blocks]
> >>>> + | | | [sends INIT]
> >>>> + |<------------------------------------------------ |
> >>>> + | | |
> >>>> + | [worker processes INIT] | |
> >>>> + |------------------------------------------------->|
> >>>> + | | | [mount unblocks]
> >>>> + | |<----- success ------ |
> >>>> + | | |
> >>>> + | [fusermount3 exits] |
> >>>> + | |
> >>>> + | SUCCESS: Worker ready before mount() |
> >>>> + | INIT processed synchronously |
> >>>> +
> >>>> +
> >>>> +3. Error Scenario: Library Crashes Before Signaling
> >>>> +----------------------------------------------------
> >>>> +
> >>>> +Library fusermount3 Kernel
> >>>> + | | |
> >>>> + |--- spawn fusermount3 ---->| |
> >>>> + | with --sync-init | |
> >>>> + | | |
> >>>> + | [open /dev/fuse] |
> >>>> + | |------- open -------->|
> >>>> + | |<------ fd ---------- |
> >>>> + | | |
> >>>> + | [send_fd(fd)] |
> >>>> + |<------- fd --------------| |
> >>>> + | | |
> >>>> + | [wait_for_signal()] |
> >>>> + | | (BLOCKED) |
> >>>> + | | |
> >>>> + X [library crashes] | |
> >>>> + | | |
> >>>> + | [recv() returns 0] |
> >>>> + | [socket closed] |
> >>>> + | | |
> >>>> + | [cleanup and exit] |
> >>>> + | X |
> >>>> + | |
> >>>> + | RESULT: Clean failure, no mount performed |
> >>>> +
> >>>> +
> >>>> +4. Detailed Function Call Flow
> >>>> +-------------------------------
> >>>> +
> >>>> +Library (lib/fuse_lowlevel.c):
> >>>> +fuse_session_mount_new_api()
> >>>> + |
> >>>> + +-- fuse_kern_mount_prepare() [lib/mount.c]
> >>>> + | |
> >>>> + | +-- fuse_mount_fusermount() [lib/mount_util.c]
> >>>> + | |
> >>>> + | +-- socketpair() [create comm socket]
> >>>> + | |
> >>>> + | +-- fork()
> >>>> + | |
> >>>> + | +-- [child] execl("fusermount3", "--sync-init", ...)
> >>>> + | |
> >>>> + | +-- [parent] receive_fd() <--- BLOCKS until fd arrives
> >>>> + | |
> >>>> + | +-- recvmsg(SCM_RIGHTS)
> >>>> + | |
> >>>> + | +-- return fd
> >>>> + |
> >>>> + +-- session_start_sync_init() [lib/fuse_lowlevel.c]
> >>>> + | |
> >>>> + | +-- ioctl(fd, FUSE_DEV_IOC_SYNC_INIT)
> >>>> + | |
> >>>> + | +-- pthread_create(worker_thread)
> >>>> + | |
> >>>> + | +-- return
> >>>> + |
> >>>> + +-- fuse_fusermount_proceed_mnt(socket) [lib/mount.c] <--- NEW: Bidirectional handshake
> >>>> + |
> >>>> + +-- send(socket, "proceed", 1) <--- Signal fusermount3 to proceed
> >>>> + |
> >>>> + +-- recv(socket, &status, 1) <--- BLOCKS until mount result arrives
> >>>> + | |
> >>>> + | +-- [fusermount3 performs mount and sends status byte]
> >>>> + |
> >>>> + +-- if (status != 0) return -1 <--- Mount failed
> >>>> + |
> >>>> + +-- return 0 <--- Mount succeeded
> >>>> +
> >>>> +
> >>>> +Utility (util/fusermount.c):
> >>>> +fusermount3 main() with --sync-init
> >>>> + |
> >>>> + +-- mount_fuse_sync_init() [util/fusermount.c]
> >>>> + |
> >>>> + +-- mount_fuse_prepare() [util/fusermount.c]
> >>>> + | |
> >>>> + | +-- open("/dev/fuse")
> >>>> + | |
> >>>> + | +-- check_perm() [util/fusermount.c]
> >>>> + | |
> >>>> + | +-- return fd
> >>>> + |
> >>>> + +-- send_fd(socket, fd) [util/fusermount.c]
> >>>> + | |
> >>>> + | +-- sendmsg(SCM_RIGHTS)
> >>>> + |
> >>>> + +-- wait_for_signal(socket) [util/fusermount.c] <--- BLOCKS until library signals
> >>>> + | |
> >>>> + | +-- recv(socket, buf, 1)
> >>>> + | |
> >>>> + | +-- return 0
> >>>> + |
> >>>> + +-- mount_fuse_finish_fsmount() [util/fusermount.c]
> >>>> + | |
> >>>> + | +-- fuse_kern_fsmount() [lib/mount_fsmount.c]
> >>>> + | | |
> >>>> + | | +-- fsopen("fuse", FSOPEN_CLOEXEC)
> >>>> + | | | |
> >>>> + | | | +-- [kernel creates filesystem context]
> >>>> + | | |
> >>>> + | | +-- fsconfig(fsfd, SET_STRING, "source", ...)
> >>>> + | | +-- fsconfig(fsfd, SET_STRING, "fd", fd_value, ...)
> >>>> + | | +-- fsconfig(fsfd, ...) [apply mount options]
> >>>> + | | +-- fsconfig(fsfd, CMD_CREATE, ...)
> >>>> + | | |
> >>>> + | | +-- fsmount(fsfd, FSMOUNT_CLOEXEC, mount_attrs)
> >>>> + | | | |
> >>>> + | | | +-- [kernel sends FUSE_INIT here]
> >>>> + | | | |
> >>>> + | | | +-- [worker thread processes INIT]
> >>>> + | | | |
> >>>> + | | | +-- [fsmount returns mntfd]
> >>>> + | | |
> >>>> + | | +-- move_mount(mntfd, "", AT_FDCWD, target, ...)
> >>>> + | | | |
> >>>> + | | | +-- [attach mount to target directory]
> >>>> + | | | |
> >>>> + | | | +-- [no blocking - INIT already processed]
> >>>> + | | |
> >>>> + | | +-- add_mount() [lib/mount_fsmount.c - update /etc/mtab]
> >>>> + | | |
> >>>> + | | +-- return 0 on success, -1 on failure
> >>>> + | |
> >>>> + | +-- if mount failed: return -1
> >>>> + | +-- if mount succeeded: continue
> >>>> + |
> >>>> + +-- send_status_byte(socket) [util/fusermount.c] <--- NEW: Send result to library
> >>>> + | |
> >>>> + | +-- status = (mount_result == 0) ? 0 : 1
> >>>> + | +-- send(socket, &status, 1)
> >>>> + | |
> >>>> + | +-- return
> >>>> + |
> >>>> + +-- return 0
> >>>> +
> >>>> +
> >>>> +Note: The new mount API (fsopen/fsconfig/fsmount/move_mount) is REQUIRED
> >>>> + for sync-init because fsmount() triggers FUSE_INIT before the mount
> >>>> + is attached. This allows the worker thread to process INIT before
> >>>> + move_mount() completes, preventing deadlock.
> >>>
> >>> ...and so we don't expose the directory tree to the mountns until we
> >>> know that FUSE_INIT didn't crash the server.
> >>
> >> Added that as well.
> >>
> >>>
> >>>> diff --git a/util/fusermount.c b/util/fusermount.c
> >>>> index 80b42a594e89cdc2f43824f5e274892522fd8cce..808b4afd89ceb49273c944d43bffe5033e27549b 100644
> >>>> --- a/util/fusermount.c
> >>>> +++ b/util/fusermount.c
> >>>> @@ -957,6 +957,7 @@ static void free_mount_params(struct mount_params *mp)
> >>>> free(mp->source);
> >>>> free(mp->type);
> >>>> free(mp->mnt_opts);
> >>>> + memset(mp, 0, sizeof(*mp));
> >>>> }
> >>>>
> >>>> /*
> >>>> @@ -1378,6 +1379,179 @@ static int open_fuse_device(const char *dev)
> >>>> return fd;
> >>>> }
> >>>>
> >>>> +#ifdef HAVE_NEW_MOUNT_API
> >>>> +/* Forward declaration from lib/mount_fsmount.c */
> >>>> +int fuse_kern_fsmount(const char *mnt, unsigned long flags, int blkdev,
> >>>> + const char *fsname, const char *subtype,
> >>>> + const char *source_dev, const char *kernel_opts,
> >>>> + const char *mnt_opts);
> >>>> +#endif
> >>>
> >>> Shouldn't this be included from a header file somewhere?
> >>
> >> Fixed, included from mount_i_linux.h
> >>
> >>>
> >>>> +
> >>>> +/*
> >>>> + * Context for split mount operation (sync-init mode)
> >>>> + */
> >>>> +struct mount_context {
> >>>> + int fd;
> >>>> + const char *dev;
> >>>> + struct stat stbuf;
> >>>> + char *source;
> >>>> + char *mnt_opts;
> >>>> + char *x_opts;
> >>>> + const char *type;
> >>>> +};
> >>>> +
> >>>> +/*
> >>>> + * Phase 1: Open device and prepare for mount (sync-init mode)
> >>>> + * Returns fd on success, -1 on failure
> >>>> + */
> >>>> +static int mount_fuse_prepare(const char *mnt, const char *opts,
> >>>> + struct mount_context *ctx)
> >>>> +{
> >>>> + int res;
> >>>> + int mountpoint_fd = -1;
> >>>> + char *do_mount_opts = NULL;
> >>>> + const char *real_mnt = mnt;
> >>>> +
> >>>> + memset(ctx, 0, sizeof(*ctx));
> >>>> + ctx->dev = getenv(FUSE_KERN_DEVICE_ENV) ?: FUSE_DEV;
> >>>> +
> >>>> + ctx->fd = open_fuse_device(ctx->dev);
> >>>> + if (ctx->fd == -1)
> >>>> + return -1;
> >>>> +
> >>>> + drop_privs();
> >>>> + read_conf();
> >>>> +
> >>>> + if (getuid() != 0 && mount_max != -1) {
> >>>> + int mount_count = count_fuse_fs();
> >>>> +
> >>>> + if (mount_count >= mount_max) {
> >>>> + fprintf(stderr,
> >>>> + "%s: too many FUSE filesystems mounted; mount_max=N can be set in %s\n",
> >>>> + progname, FUSE_CONF);
> >>>> + goto fail_close_fd;
> >>>> + }
> >>>> + }
> >>>
> >>> /me notes that he's refactored this configuration file related function
> >>> into fuser_conf.c though that's in the fuse-services v4 that I'll send
> >>> you soon.
> >>>
> >>>> +
> >>>> + res = extract_x_options(opts, &do_mount_opts, &ctx->x_opts);
> >>>> + if (res)
> >>>> + goto fail_close_fd;
> >>>> +
> >>>> + res = check_perm(&real_mnt, &ctx->stbuf, &mountpoint_fd);
> >>>> + restore_privs();
> >>>> +
> >>>> + if (mountpoint_fd != -1)
> >>>> + close(mountpoint_fd);
> >>>> +
> >>>> + if (res == -1)
> >>>> + goto fail_close_fd;
> >>>> +
> >>>> + free(do_mount_opts);
> >>>> + return ctx->fd;
> >>>> +
> >>>> +fail_close_fd:
> >>>> + close(ctx->fd);
> >>>> + free(do_mount_opts);
> >>>> + free(ctx->x_opts);
> >>>> + ctx->fd = -1;
> >>>> + return -1;
> >>>> +}
> >>>> +
> >>>> +#ifdef HAVE_NEW_MOUNT_API
> >>>> +/*
> >>>> + * Phase 2: Perform the actual mount using new mount API (sync-init mode)
> >>>> + * Returns 0 on success, -1 on failure
> >>>> + */
> >>>> +static int mount_fuse_finish_fsmount(const char *mnt, const char *opts,
> >>>> + struct mount_context *ctx,
> >>>> + const char **type)
> >>>> +{
> >>>> + int res;
> >>>> + char *do_mount_opts = NULL;
> >>>> + char *x_prefixed_opts = NULL;
> >>>> + struct mount_params mp = { .fd = ctx->fd };
> >>>> + char *final_mnt_opts = NULL;
> >>>> +
> >>>> + /* Extract x-options */
> >>>> + res = extract_x_options(opts, &do_mount_opts, &x_prefixed_opts);
> >>>> + if (res)
> >>>> + goto fail;
> >>>> +
> >>>> + /* Prepare mount parameters */
> >>>> + mp.rootmode = ctx->stbuf.st_mode & S_IFMT;
> >>>> + mp.dev = ctx->dev;
> >>>
> >>> I think those could be set in the mp variable definition?
> >>
> >> Absolutely.
> >>
> >>>
> >>>> +
> >>>> + res = prepare_mount(do_mount_opts, &mp);
> >>>> + if (res == -1)
> >>>> + goto fail;
> >>>> +
> >>>> + /* Merge x-options if running as root */
> >>>> + final_mnt_opts = mp.mnt_opts;
> >>>> + if (geteuid() == 0 && ctx->x_opts && strlen(ctx->x_opts) > 0) {
> >>>> + size_t mnt_opts_len = strlen(mp.mnt_opts);
> >>>> + size_t x_mnt_opts_len = mnt_opts_len + strlen(ctx->x_opts) + 2;
> >>>> + char *x_mnt_opts = calloc(1, x_mnt_opts_len);
> >>>> +
> >>>> + if (!x_mnt_opts)
> >>>> + goto fail_free_params;
> >>>> +
> >>>> + if (mnt_opts_len) {
> >>>> + strcpy(x_mnt_opts, mp.mnt_opts);
> >>>> + strncat(x_mnt_opts, ",", 2);
> >>>> + }
> >>>> + strncat(x_mnt_opts, ctx->x_opts,
> >>>> + x_mnt_opts_len - mnt_opts_len - 2);
> >>>> +
> >>>> + final_mnt_opts = x_mnt_opts;
> >>>> + }
> >>>
> >>> Curious, I thought the x- options were edited out by /sbin/mount so fuse
> >>> would never see them? Does the x- option handling in fusermount.c exist
> >>> to handle the case where someone passes them directly to the fuse
> >>> server, aka
> >>>
> >>> $ sshfs <whatever> /mnt -o x-systemd-hahaha=1
> >>>
> >>> and now you need to ensure that x-systemd-hahaha doesn't get sent to the
> >>> kernel but does get seen by the fuse server?
> >>
> >> https://github.com/libfuse/libfuse/issues/651
> >>
> >> So specially added as mount option to to go into mtab/utab and to
> >> suppress some 3rd party (gnome) actions.
> >
> > Huh, I wouldn't have thought that would work since /etc/mtab has been
> > a symlink to /proc/self/mounts for a while now. Of course GNOME
> > abstracts mount options behind some g_unix_mount_entry_get_options
> > function call which ... doesn't document where it gets its mount options
> > from.
>
> Problem is that these don't go into /proc/self/mounts at all, but into
> /run/mount/utab with recent mount utils. With /etc/mtab not being a
> symlink it would have worked as well.
>
> bernd@e7270 ~>cat /run/mount/utab
> SRC=/dev/loop0 TARGET=/snap/chromium/3375 ROOT=/ OPTS=x-gdu.hide,x-gvfs-hide
> SRC=/dev/loop1 TARGET=/snap/bare/5 ROOT=/ OPTS=x-gdu.hide,x-gvfs-hide
> SRC=/dev/loop2 TARGET=/snap/chromium/3390 ROOT=/ OPTS=x-gdu.hide,x-gvfs-hide
> ...
Ahah, I didn't realize that util-linux is actually still updating mount
table flat files, at least if you feed it magic x- options. Apparently
mount.nfs does too.
/me updates the mountservice branch to call fuse_mnt_add_mount() so that
this continues to work. Thanks for cluing me into that!
--D
^ permalink raw reply [flat|nested] 59+ messages in thread
end of thread, other threads:[~2026-03-25 22:03 UTC | newest]
Thread overview: 59+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-03-23 17:44 [PATCH 00/19] libfuse: Add support for synchronous init Bernd Schubert
2026-03-23 17:44 ` [PATCH 01/19] ci-build: Add environment logging Bernd Schubert
2026-03-23 17:44 ` [PATCH 02/19] Add 'STRCPY' to the checkpatch ignore option Bernd Schubert
2026-03-23 21:03 ` Darrick J. Wong
2026-03-23 17:44 ` [PATCH 03/19] checkpatch.pl: Add _Atomic to $Attribute patttern Bernd Schubert
2026-03-23 21:09 ` Darrick J. Wong
2026-03-23 17:44 ` [PATCH 04/19] Add a new daemonize API Bernd Schubert
2026-03-23 22:28 ` Darrick J. Wong
2026-03-24 17:36 ` Bernd Schubert
2026-03-24 22:20 ` Darrick J. Wong
2026-03-23 17:45 ` [PATCH 05/19] Sync fuse_kernel.h with linux-6.18 Bernd Schubert
2026-03-23 21:16 ` Darrick J. Wong
2026-03-23 17:45 ` [PATCH 06/19] mount.c: Split fuse_mount_sys to prepare privileged sync FUSE_INIT Bernd Schubert
2026-03-23 22:34 ` Darrick J. Wong
2026-03-23 17:45 ` [PATCH 07/19] Add FUSE_MOUNT_FALLBACK_NEEDED define for -2 mount errors Bernd Schubert
2026-03-23 22:36 ` Darrick J. Wong
2026-03-24 18:03 ` Bernd Schubert
2026-03-23 17:45 ` [PATCH 08/19] Refactor mount code / move common functions to mount_util.c Bernd Schubert
2026-03-23 22:40 ` Darrick J. Wong
2026-03-23 17:45 ` [PATCH 09/19] Move mount flags to mount_i.h Bernd Schubert
2026-03-23 22:45 ` Darrick J. Wong
2026-03-24 18:40 ` Bernd Schubert
2026-03-23 17:45 ` [PATCH 10/19] conftest.py: Add more valgrind filter patterns Bernd Schubert
2026-03-23 17:45 ` [PATCH 11/19] Add support for the new linux mount API Bernd Schubert
2026-03-23 23:42 ` Darrick J. Wong
2026-03-24 20:16 ` Bernd Schubert
2026-03-24 22:46 ` Darrick J. Wong
2026-03-23 17:45 ` [PATCH 12/19] fuse mount: Support synchronous FUSE_INIT (privileged daemon) Bernd Schubert
2026-03-24 0:03 ` Darrick J. Wong
2026-03-24 20:42 ` Bernd Schubert
2026-03-24 22:50 ` Darrick J. Wong
2026-03-25 7:52 ` Bernd Schubert
2026-03-25 16:42 ` Darrick J. Wong
2026-03-23 17:45 ` [PATCH 13/19] Add fuse_session_set_debug() to enable debug output without foreground Bernd Schubert
2026-03-24 0:04 ` Darrick J. Wong
2026-03-23 17:45 ` [PATCH 14/19] Move more generic mount code to mount_util.{c,h} Bernd Schubert
2026-03-24 0:06 ` Darrick J. Wong
2026-03-24 20:57 ` Bernd Schubert
2026-03-23 17:45 ` [PATCH 15/19] Split the fusermount do_mount function Bernd Schubert
2026-03-24 0:14 ` Darrick J. Wong
2026-03-24 21:05 ` Bernd Schubert
2026-03-24 22:53 ` Darrick J. Wong
2026-03-23 17:45 ` [PATCH 16/19] fusermount: Refactor extract_x_options Bernd Schubert
2026-03-24 0:18 ` Darrick J. Wong
2026-03-23 17:45 ` [PATCH 17/19] Make fusermount work bidirectional for sync init Bernd Schubert
2026-03-24 19:35 ` Darrick J. Wong
2026-03-24 21:24 ` Bernd Schubert
2026-03-24 22:59 ` Darrick J. Wong
2026-03-25 19:48 ` Bernd Schubert
2026-03-25 22:03 ` Darrick J. Wong
2026-03-23 17:45 ` [PATCH 18/19] New mount API: Filter out "user=" Bernd Schubert
2026-03-24 19:51 ` Darrick J. Wong
2026-03-24 20:01 ` Bernd Schubert
2026-03-24 23:02 ` Darrick J. Wong
2026-03-23 17:45 ` [PATCH 19/19] Add support for sync-init of unprivileged daemons Bernd Schubert
2026-03-24 20:21 ` Darrick J. Wong
2026-03-24 21:53 ` Bernd Schubert
2026-03-24 23:13 ` Darrick J. Wong
2026-03-24 0:19 ` [PATCH 00/19] libfuse: Add support for synchronous init Darrick J. Wong
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox