* [PATCH v2 00/14] libfuse: Add mount service safety checks and tests
@ 2026-09-28 11:02 Bernd Schubert via B4 Relay
2026-09-28 11:02 ` [PATCH v2 01/14] mount_service: move the command line check into arg_in_cmdline() Bernd Schubert via B4 Relay
` (13 more replies)
0 siblings, 14 replies; 26+ messages in thread
From: Bernd Schubert via B4 Relay @ 2026-09-28 11:02 UTC (permalink / raw)
To: fuse-devel; +Cc: Darrick J. Wong, neal, Bernd Schubert, Keerthana KT
That was noticed by AI on comparison to systemd storage provider
and fuse service mount tests were missing as well.
To: fuse-devel@lists.linux.dev
Cc: Darrick J. Wong <djwong@kernel.org>
Cc: neal@gompa.dev
Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
---
Changes in v2:
- Add more parsing logic to detect files to open from the command
line and add admin overrides if the logic failed
- Allow open after mount, but use openat(), which avoids that
files from the mount point are taken
- Add in '-d path' for mkfs.ext4 to copy in files for improved
testing
- Add another sanity commit to limit max arg comand size
(previously https://github.com/libfuse/libfuse/pull/1552)
- Change path to /run/fuse
- Add the documention patch
(previously https://github.com/libfuse/libfuse/pull/1530)
- libfuse PR: https://github.com/libfuse/libfuse/pull/1638
- Link to v1: https://patch.msgid.link/20260925-mount-service-bound-open-v1-0-bbf1a84c7995@bsbernd.com
---
Bernd Schubert (13):
mount_service: move the command line check into arg_in_cmdline()
mount_service: warn about paths not named on the command line
mount_service: refuse paths the user did not name
mount_service: use openat to OPEN paths
util: give fuservicemount3 an absolute build-tree runpath
mount.fuse: free the options on the service mount return path
example/single_file: take no sector size from a regular backing file
test: check which files fuservicemount3 opens for the server
test: check what fuservicemount3 refuses
test: mount the service examples through fuservicemount3
test: run mkfs.ext4 through the service examples
build: move the default service socket directory to /run/fuse
Improve documentation for fuse service mount
Keerthana KT (1):
fuse_service: bound argc and arg len read from the args memfd
.gitignore | 1 +
doc/README.service-mount | 312 ++++++++++++++++++++++
doc/README.service-mount-dev | 456 ++++++++++++++++++++++++++++++++
doc/README.service-mount-flow | 201 ++++++++++++++
doc/fuservicemount3.8 | 164 +++++++++++-
doc/mainpage.dox | 13 +
doc/mount.fuse3.8 | 25 ++
example/service_ll.c | 5 +
example/single_file.c | 3 +-
include/fuse_service.h | 4 +-
include/fuse_service_priv.h | 10 +
lib/fuse_service.c | 43 +++
meson.build | 2 +-
meson_options.txt | 2 +-
test/cases/lib/service-example.sh | 112 ++++++++
test/cases/lib/service.sh | 95 +++++++
test/cases/lib/socket_activate.py | 42 +++
test/cases/mount/service-caps.sh | 20 ++
test/cases/mount/service-check.sh | 38 +++
test/cases/mount/service-hl-mkfs.sh | 8 +
test/cases/mount/service-hl.sh | 7 +
test/cases/mount/service-ll-mkfs.sh | 8 +
test/cases/mount/service-ll.sh | 7 +
test/cases/mount/service-mount-fuse.sh | 31 +++
test/cases/mount/service-mountpoint.sh | 36 +++
test/cases/mount/service-nonroot.sh | 54 ++++
test/cases/mount/service-null.sh | 33 +++
test/cases/mount/service-open-bound.sh | 61 +++++
test/cases/mount/service-server-exit.sh | 22 ++
test/meson.build | 6 +
test/test_fuser_conf.c | 55 ++++
test/test_service.c | 232 ++++++++++++++++
util/fuse.conf | 11 +
util/fuser_conf.c | 99 +++++++
util/fuser_conf.h | 3 +
util/meson.build | 2 +
util/mount.fuse.c | 42 +--
util/mount_service.c | 128 +++++++--
38 files changed, 2351 insertions(+), 42 deletions(-)
---
base-commit: e001ea32a977933236bcd928692c2c022594a41d
change-id: 20260924-mount-service-bound-open-739b16236bfa
Best regards,
--
Bernd Schubert <bernd@bsbernd.com>
^ permalink raw reply [flat|nested] 26+ messages in thread
* [PATCH v2 01/14] mount_service: move the command line check into arg_in_cmdline()
2026-09-28 11:02 [PATCH v2 00/14] libfuse: Add mount service safety checks and tests Bernd Schubert via B4 Relay
@ 2026-09-28 11:02 ` Bernd Schubert via B4 Relay
2026-09-28 11:02 ` [PATCH v2 02/14] mount_service: warn about paths not named on the command line Bernd Schubert via B4 Relay
` (12 subsequent siblings)
13 siblings, 0 replies; 26+ messages in thread
From: Bernd Schubert via B4 Relay @ 2026-09-28 11:02 UTC (permalink / raw)
To: fuse-devel; +Cc: Darrick J. Wong, neal, Bernd Schubert
From: Bernd Schubert <bernd@bsbernd.com>
mount_service_handle_mountpoint_cmd() compared the mount point with
each argument of fuservicemount3 in its own loop. The next patch makes
the same check for OPEN requests, so the loop moves into
arg_in_cmdline(). There is no change in behaviour.
Assisted-by: LLM
Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
Reviewed-by: "Darrick J. Wong" <djwong@kernel.org>
---
util/mount_service.c | 22 +++++++++++++---------
1 file changed, 13 insertions(+), 9 deletions(-)
diff --git a/util/mount_service.c b/util/mount_service.c
index 0b3266309a8a..7549e0b5024f 100644
--- a/util/mount_service.c
+++ b/util/mount_service.c
@@ -758,6 +758,18 @@ static int prepare_bdev(const struct mount_service *mo,
return 0;
}
+static bool arg_in_cmdline(int argc, const char * const argv[],
+ const char *value)
+{
+ int i;
+
+ for (i = 0; i < argc; i++)
+ if (!strcmp(argv[i], value))
+ return true;
+
+ return false;
+}
+
static int mount_service_open_path(const struct mount_service *mo,
mode_t expected_fmt,
struct fuse_service_packet *p, size_t psz)
@@ -1248,8 +1260,6 @@ static int mount_service_handle_mountpoint_cmd(struct mount_service *mo,
container_of(p, struct fuse_service_mountpoint_command, p);
char *mntpt;
mode_t expected_fmt;
- bool foundit = false;
- int i;
if (psz < sizeof_fuse_service_mountpoint_command(1)) {
fprintf(stderr, "%s: mount point command too small\n",
@@ -1289,13 +1299,7 @@ static int mount_service_handle_mountpoint_cmd(struct mount_service *mo,
}
/* Mountpoint must be mentioned in the caller's argument list */
- for (i = 0; i < argc; i++) {
- if (!strcmp(argv[i], oc->value)) {
- foundit = true;
- break;
- }
- }
- if (!foundit) {
+ if (!arg_in_cmdline(argc, argv, oc->value)) {
fprintf(stderr, "%s: mount point must be in command line arguments\n",
mo->msgtag);
return mount_service_send_reply(mo, EINVAL);
--
2.53.0
^ permalink raw reply related [flat|nested] 26+ messages in thread
* [PATCH v2 02/14] mount_service: warn about paths not named on the command line
2026-09-28 11:02 [PATCH v2 00/14] libfuse: Add mount service safety checks and tests Bernd Schubert via B4 Relay
2026-09-28 11:02 ` [PATCH v2 01/14] mount_service: move the command line check into arg_in_cmdline() Bernd Schubert via B4 Relay
@ 2026-09-28 11:02 ` Bernd Schubert via B4 Relay
2026-09-29 2:10 ` Darrick J. Wong
2026-09-28 11:02 ` [PATCH v2 03/14] mount_service: refuse paths the user did not name Bernd Schubert via B4 Relay
` (11 subsequent siblings)
13 siblings, 1 reply; 26+ messages in thread
From: Bernd Schubert via B4 Relay @ 2026-09-28 11:02 UTC (permalink / raw)
To: fuse-devel; +Cc: Darrick J. Wong, neal, Bernd Schubert
From: Bernd Schubert <bernd@bsbernd.com>
In a service mount, the fuse server runs as a systemd service in a
sandbox that has no access to the user's files. The user runs mount,
which starts fuservicemount3, a setuid-root helper. The fuse server
sends requests to the helper over a socket. With an OPEN request, the
server asks the helper to open its backing file, for example the disk
image named on the mount command line. The helper opens the file with
the user's credentials and passes the file descriptor to the server.
fusermount3 opens nothing for the fuse server except /dev/fuse; the
server runs as the user and opens its own files.
The helper opens any path the server sends. An attacker who controlled
the server could use this to read every file the user can read, for
example ~/.ssh/id_ed25519, and the sandbox does not prevent it. The
helper now prints a warning if the user did not name the path when
mounting: as a whole argument, as the value of a name=value option, or
glued to a short option as in "-J/dev/sdb1". The helper splits the
options with fuse_opt_parse(), as the fuse server does, so both see the
same option values. The helper still opens the path, because a server
can take a path in a form that none of these checks recognizes.
Enforced permissions follow up in the next commit.
Assisted-by: LLM
Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
---
util/mount_service.c | 73 +++++++++++++++++++++++++++++++++++++++++++++++-----
1 file changed, 66 insertions(+), 7 deletions(-)
diff --git a/util/mount_service.c b/util/mount_service.c
index 7549e0b5024f..446f37f61916 100644
--- a/util/mount_service.c
+++ b/util/mount_service.c
@@ -770,9 +770,55 @@ static bool arg_in_cmdline(int argc, const char * const argv[],
return false;
}
+struct option_value_match {
+ const char *path;
+ bool found;
+};
+
+/* fuse_opt_parse() callback: set match->found if the path is this option's value */
+static int match_option_value(void *data, const char *arg, int key,
+ struct fuse_args *outargs)
+{
+ struct option_value_match *match = data;
+ const char *value = strchr(arg, '=');
+
+ (void) outargs;
+
+ if (key != FUSE_OPT_KEY_OPT)
+ return 0;
+
+ if (value && !strcmp(value + 1, match->path))
+ match->found = true;
+
+ /* Short option with its value glued on, as in "-J/dev/sdb1" */
+ if (arg[0] == '-' && arg[1] && arg[1] != '-' &&
+ !strcmp(arg + 2, match->path))
+ match->found = true;
+
+ return 0;
+}
+
+/* @return true for the path in "-o name=path", "--name=path" or "-Xpath" */
+static bool option_value_in_cmdline(int argc, const char * const argv[],
+ const char *path)
+{
+ struct option_value_match match = {
+ .path = path,
+ };
+ struct fuse_args args = FUSE_ARGS_INIT(argc, (char **)argv);
+ int ret;
+
+ /* Parse like the fuse server does, so both see the same values */
+ ret = fuse_opt_parse(&args, &match, NULL, match_option_value);
+ fuse_opt_free_args(&args);
+
+ return !ret && match.found;
+}
+
static int mount_service_open_path(const struct mount_service *mo,
mode_t expected_fmt,
- struct fuse_service_packet *p, size_t psz)
+ struct fuse_service_packet *p, size_t psz,
+ int argc, const char * const argv[])
{
const struct fuse_service_open_command *oc =
container_of(p, struct fuse_service_open_command, p);
@@ -800,6 +846,15 @@ static int mount_service_open_path(const struct mount_service *mo,
return mount_service_send_file_error(mo, EINVAL, oc->path);
}
+ /*
+ * The file is opened outside the service sandbox, so report a path
+ * the user did not name.
+ */
+ if (!arg_in_cmdline(argc, argv, oc->path) &&
+ !option_value_in_cmdline(argc, argv, oc->path))
+ fprintf(stderr, "%s: %s: warning: file not in command line arguments\n",
+ mo->msgtag, oc->path);
+
open_flags = ntohl(oc->open_flags) | O_CLOEXEC;
drop_privs();
fd = open(oc->path, open_flags, ntohl(oc->create_mode));
@@ -834,16 +889,18 @@ static int mount_service_open_path(const struct mount_service *mo,
static int mount_service_handle_open_cmd(const struct mount_service *mo,
struct fuse_service_packet *p,
- size_t psz)
+ size_t psz, int argc,
+ const char * const argv[])
{
- return mount_service_open_path(mo, 0, p, psz);
+ return mount_service_open_path(mo, 0, p, psz, argc, argv);
}
static int mount_service_handle_open_bdev_cmd(const struct mount_service *mo,
struct fuse_service_packet *p,
- size_t psz)
+ size_t psz, int argc,
+ const char * const argv[])
{
- return mount_service_open_path(mo, S_IFBLK, p, psz);
+ return mount_service_open_path(mo, S_IFBLK, p, psz, argc, argv);
}
#ifdef HAVE_NEW_MOUNT_API
@@ -1838,10 +1895,12 @@ int mount_service_main(int argc, char *argv[])
switch (ntohl(p->magic)) {
case FUSE_SERVICE_OPEN_CMD:
- ret = mount_service_handle_open_cmd(&mo, p, sz);
+ ret = mount_service_handle_open_cmd(&mo, p, sz,
+ argc, (const char * const *)argv);
break;
case FUSE_SERVICE_OPEN_BDEV_CMD:
- ret = mount_service_handle_open_bdev_cmd(&mo, p, sz);
+ ret = mount_service_handle_open_bdev_cmd(&mo, p, sz,
+ argc, (const char * const *)argv);
break;
case FUSE_SERVICE_FSOPEN_CMD:
ret = mount_service_handle_fsopen_cmd(&mo, p, sz);
--
2.53.0
^ permalink raw reply related [flat|nested] 26+ messages in thread
* [PATCH v2 03/14] mount_service: refuse paths the user did not name
2026-09-28 11:02 [PATCH v2 00/14] libfuse: Add mount service safety checks and tests Bernd Schubert via B4 Relay
2026-09-28 11:02 ` [PATCH v2 01/14] mount_service: move the command line check into arg_in_cmdline() Bernd Schubert via B4 Relay
2026-09-28 11:02 ` [PATCH v2 02/14] mount_service: warn about paths not named on the command line Bernd Schubert via B4 Relay
@ 2026-09-28 11:02 ` Bernd Schubert via B4 Relay
2026-09-29 2:26 ` Darrick J. Wong
2026-09-28 11:02 ` [PATCH v2 04/14] mount_service: use openat to OPEN paths Bernd Schubert via B4 Relay
` (10 subsequent siblings)
13 siblings, 1 reply; 26+ messages in thread
From: Bernd Schubert via B4 Relay @ 2026-09-28 11:02 UTC (permalink / raw)
To: fuse-devel; +Cc: Darrick J. Wong, neal, Bernd Schubert
From: Bernd Schubert <bernd@bsbernd.com>
fuservicemount3 warns about a path that the user did not name on the
command line, but still opens it. A server can take a path in a form
that the helper cannot split, for example "-journal/dev/sdb1" or its
own option syntax. The administrator can now list such paths in
/etc/fuse.conf, per filesystem type:
service_open_path = ext4 /dev/sd*
The pattern is matched with fnmatch() and FNM_PATHNAME, so "*" does not
match "/". A requested path with a "." or ".." component never matches,
because "*" matches "..", and "/dev/*" would then open "/". The helper
now refuses any other path with EPERM.
Assisted-by: LLM
Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
---
doc/fuservicemount3.8 | 16 ++++++++
doc/mount.fuse3.8 | 7 ++++
include/fuse_service.h | 4 +-
test/test_fuser_conf.c | 55 ++++++++++++++++++++++++++++
util/fuse.conf | 11 ++++++
util/fuser_conf.c | 99 ++++++++++++++++++++++++++++++++++++++++++++++++++
util/fuser_conf.h | 3 ++
util/mount_service.c | 13 ++++---
8 files changed, 202 insertions(+), 6 deletions(-)
diff --git a/doc/fuservicemount3.8 b/doc/fuservicemount3.8
index aa2167cb4872..18e285c1ab29 100644
--- a/doc/fuservicemount3.8
+++ b/doc/fuservicemount3.8
@@ -19,6 +19,22 @@ Mount a filesystem using a FUSE server that runs as a socket service.
These servers can be contained using the platform's service management
framework.
+The FUSE server may ask fuservicemount3 to open files on its behalf.
+fuservicemount3 opens a path only in these cases:
+.IP \- 2
+The path is a command line argument, for example /srv/disk.img.
+.IP \- 2
+The path is the value in a key=value option, for example /dev/sdb1 in
+"-o journal_dev=/dev/sdb1".
+.IP \- 2
+The path directly follows a short option, for example /dev/sdb1 in
+"-J/dev/sdb1".
+.IP \- 2
+A service_open_path line in /etc/fuse.conf lists the path for the filesystem
+type.
+.PP
+It refuses any other request with EPERM.
+
The second form checks if there is a FUSE service available for the given
filesystem type.
.SH "AUTHORS"
diff --git a/doc/mount.fuse3.8 b/doc/mount.fuse3.8
index 2e587458a06e..d55c96139d9f 100644
--- a/doc/mount.fuse3.8
+++ b/doc/mount.fuse3.8
@@ -38,6 +38,13 @@ Allow non-root users to specify the \fBallow_other\fP or
\fBallow_root\fP mount options (see below).
.TP
These limits are enforced by the \fBfusermount3\fP helper, so they can be avoided by filesystems that run as root.
+.TP
+\fBservice_open_path = SUBTYPE PATTERN\fP
+Allow \fBfuservicemount3\fP(8) to open paths that match \fIPATTERN\fP for the
+server of a service mount of type \fBfuse.\fISUBTYPE\fR, in addition to the
+paths on the mount command line. \fIPATTERN\fP is an absolute path in which "*"
+does not match "/". A pattern that matches a directory gives the server every
+file below it. The line can be repeated.
.SH OPTIONS
Most of the generic mount options described in \fBmount\fP are
supported (\fBro\fP, \fBrw\fP, \fBsuid\fP, \fBnosuid\fP, \fBdev\fP,
diff --git a/include/fuse_service.h b/include/fuse_service.h
index d6aedea8f0f8..2114e7772bf5 100644
--- a/include/fuse_service.h
+++ b/include/fuse_service.h
@@ -139,6 +139,8 @@ int fuse_service_parse_cmdline_opts(struct fuse_args *args,
/**
* Ask the mount.service helper to open a file on behalf of the fuse server.
+ * The helper refuses a path that the mount command line does not name and
+ * fuse.conf does not list; fuse_service_receive_file() then reports -EPERM.
*
* @param sf service context
* @param path the path to file
@@ -153,7 +155,7 @@ int fuse_service_request_file(const struct fuse_service *sf, const char *path,
/**
* Ask the mount.service helper to open a block device on behalf of the fuse
- * server.
+ * server. The helper refuses the same paths as for a file request.
*
* @param sf service context
* @param path the path to file
diff --git a/test/test_fuser_conf.c b/test/test_fuser_conf.c
index 4d974931cf58..6d95fe932039 100644
--- a/test/test_fuser_conf.c
+++ b/test/test_fuser_conf.c
@@ -105,6 +105,59 @@ static int test_trimmed_options(void)
return 0;
}
+static int test_service_open_path(void)
+{
+ const char *test = "service_open_path";
+
+ if (write_conf("service_open_path = ext4 /dev/sd*\n"
+ "service_open_path = ext4 /dev/nvme*\n"
+ "service_open_path = ext4 relative/path\n"
+ "service_open_path = xfs\t/srv/xfs.img\n"
+ "service_open_path = xfs /srv/img/*\n"
+ " \tservice_open_path = ext4 /srv/indented.img\n"
+ "service_open_path =\x20\n"
+ "service_open_path = ext4\n") == -1)
+ return fail(test, "could not write the config file");
+
+ read_conf(progname);
+
+ if (!service_open_path_listed("ext4", "/dev/sda"))
+ return fail(test, "/dev/sd* did not match /dev/sda");
+ if (!service_open_path_listed("ext4", "/dev/nvme0n1"))
+ return fail(test, "a second ext4 line was not recognised");
+ if (!service_open_path_listed("ext4", "/srv/indented.img"))
+ return fail(test, "an indented line was not recognised");
+ if (service_open_path_listed("ext4", "/dev/sda/x"))
+ return fail(test, "* matched a /");
+ if (service_open_path_listed("xfs", "/dev/sda"))
+ return fail(test, "an ext4 line matched for xfs");
+ if (service_open_path_listed("ext4", "relative/path"))
+ return fail(test, "a relative pattern was accepted");
+ if (!service_open_path_listed("xfs", "/srv/xfs.img"))
+ return fail(test, "a tab-separated line was not recognised");
+ if (!service_open_path_listed("xfs", "/srv/img/a.img"))
+ return fail(test, "/srv/img/* did not match /srv/img/a.img");
+ if (service_open_path_listed("xfs", "/srv/img/..") ||
+ service_open_path_listed("xfs", "/srv/img/."))
+ return fail(test, "a . or .. component was accepted");
+ /* Either line, if stored, would match the empty path */
+ if (service_open_path_listed("", ""))
+ return fail(test, "a line without a subtype was accepted");
+ if (service_open_path_listed("ext4", ""))
+ return fail(test, "a line without a pattern was accepted");
+
+ if (write_conf("\n") == -1)
+ return fail(test, "could not write the config file");
+
+ read_conf(progname);
+
+ if (service_open_path_listed("ext4", "/dev/sda"))
+ return fail(test, "a line survived re-reading the config");
+
+ printf("PASS: %s\n", test);
+ return 0;
+}
+
int main(void)
{
char tempdir[] = "/tmp/test_fuser_conf.XXXXXX";
@@ -123,6 +176,8 @@ int main(void)
goto out_unlink;
if (test_trimmed_options())
goto out_unlink;
+ if (test_service_open_path())
+ goto out_unlink;
printf("All fuse.conf parser tests passed\n");
result = 0;
diff --git a/util/fuse.conf b/util/fuse.conf
index ab048e0347b2..2c182ffa9d6a 100644
--- a/util/fuse.conf
+++ b/util/fuse.conf
@@ -15,3 +15,14 @@
# equals sign).
#mount_max = 1000
+
+
+# service_open_path = <subtype> <pattern> - a FUSE server that runs as a socket
+# service may ask fuservicemount3 to open paths that match <pattern>, in
+# addition to the paths on the mount command line. <subtype> is the filesystem
+# type after "fuse.", <pattern> an absolute path in which "*" does not match
+# "/". The line can be repeated to allow different patterns and subtypes.
+# A pattern that matches a directory gives the server every file below it.
+
+#service_open_path = ext4 /dev/sd*
+#service_open_path = ext4 /dev/nvme*
diff --git a/util/fuser_conf.c b/util/fuser_conf.c
index 12688f6c42b7..5ec9d263ac2f 100644
--- a/util/fuser_conf.c
+++ b/util/fuser_conf.c
@@ -18,6 +18,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
+#include <fnmatch.h>
#include <mntent.h>
#include <unistd.h>
#include <sys/fsuid.h>
@@ -35,6 +36,14 @@ int mount_max = 1000;
static uid_t oldfsuid;
static gid_t oldfsgid;
+struct service_open_path {
+ struct service_open_path *next;
+ char *subtype;
+ char *pattern;
+};
+
+static struct service_open_path *service_open_paths;
+
// Older versions of musl libc don't unescape entries in /etc/mtab
// unescapes octal sequences like \040 in-place
@@ -192,12 +201,100 @@ static void strip_line(char *line)
memmove(line, s, strlen(s)+1);
}
+/*
+ * Store one service_open_path line. For the line
+ * "service_open_path = ext4 /dev/sd*", str is "ext4 /dev/sd*".
+ */
+static void parse_service_open_path(const char *str, int linenum,
+ const char *progname)
+{
+ /* <subtype> ends at the first blank */
+ const size_t subtype_len = strcspn(str, " \t");
+ const char *pattern = str + subtype_len;
+ struct service_open_path *entry;
+
+ /* The rest of the line is <pattern>, blanks inside it included */
+ pattern += strspn(pattern, " \t");
+ /* A relative pattern would depend on each user's working directory */
+ if (!subtype_len || pattern[0] != '/') {
+ fprintf(stderr,
+ "%s: invalid service_open_path in %s at line %i\n",
+ progname, FUSE_CONF, linenum);
+ return;
+ }
+
+ entry = calloc(1, sizeof(*entry));
+ if (entry) {
+ entry->subtype = strndup(str, subtype_len);
+ entry->pattern = strdup(pattern);
+ }
+ /* Going on without the line would refuse paths the admin allowed */
+ if (!entry || !entry->subtype || !entry->pattern) {
+ fprintf(stderr, "%s: failed to allocate memory\n", progname);
+ exit(1);
+ }
+
+ /* Order does not matter, the lookup checks every entry */
+ entry->next = service_open_paths;
+ service_open_paths = entry;
+}
+
+/* The config can be read more than once; drop the lines of the last read */
+static void free_service_open_paths(void)
+{
+ while (service_open_paths) {
+ struct service_open_path *entry = service_open_paths;
+
+ service_open_paths = entry->next;
+ free(entry->subtype);
+ free(entry->pattern);
+ free(entry);
+ }
+}
+
+/* @return true if a path component is "." or "..", as in "/srv/img/.." */
+static bool has_dot_component(const char *path)
+{
+ const char *comp = path;
+
+ for (;;) {
+ const size_t len = strcspn(comp, "/");
+
+ if ((len == 1 && comp[0] == '.') ||
+ (len == 2 && comp[0] == '.' && comp[1] == '.'))
+ return true;
+ if (!comp[len])
+ return false;
+ comp += len + 1;
+ }
+}
+
+bool service_open_path_listed(const char *subtype, const char *path)
+{
+ const struct service_open_path *entry;
+
+ /* "*" also matches "..", which reaches the parent directory */
+ if (has_dot_component(path))
+ return false;
+
+ for (entry = service_open_paths; entry; entry = entry->next)
+ if (!strcmp(entry->subtype, subtype) &&
+ !fnmatch(entry->pattern, path, FNM_PATHNAME))
+ return true;
+
+ return false;
+}
+
static void parse_line(const char *line, int linenum, const char *progname)
{
int tmp;
+ int value_pos = -1;
if (strcmp(line, "user_allow_other") == 0)
user_allow_other = 1;
+ else if (sscanf(line, "service_open_path = %n", &value_pos) == 0 &&
+ value_pos >= 0)
+ parse_service_open_path(line + value_pos, linenum, progname);
else if (sscanf(line, "mount_max = %i", &tmp) == 1) {
if (tmp < -1)
fprintf(stderr,
@@ -216,6 +313,8 @@ void read_conf(const char *progname)
{
FILE *fp = fopen(FUSE_CONF, "r");
+ free_service_open_paths();
+
if (fp != NULL) {
int linenum = 1;
char line[256];
diff --git a/util/fuser_conf.h b/util/fuser_conf.h
index ea58537cc4c2..ccfc58877100 100644
--- a/util/fuser_conf.h
+++ b/util/fuser_conf.h
@@ -8,6 +8,7 @@
#ifndef FUSER_CONF_H_
#define FUSER_CONF_H_
+#include <stdbool.h>
#include <sys/vfs.h>
#include <sys/stat.h>
@@ -40,6 +41,8 @@ int count_fuse_fs(const char *progname);
void read_conf(const char *progname);
+bool service_open_path_listed(const char *subtype, const char *path);
+
void drop_privs(void);
void restore_privs(void);
diff --git a/util/mount_service.c b/util/mount_service.c
index 446f37f61916..b4081d53273e 100644
--- a/util/mount_service.c
+++ b/util/mount_service.c
@@ -847,13 +847,16 @@ static int mount_service_open_path(const struct mount_service *mo,
}
/*
- * The file is opened outside the service sandbox, so report a path
- * the user did not name.
+ * The file is opened outside the service sandbox, so only hand out
+ * what the user named or fuse.conf lists.
*/
if (!arg_in_cmdline(argc, argv, oc->path) &&
- !option_value_in_cmdline(argc, argv, oc->path))
- fprintf(stderr, "%s: %s: warning: file not in command line arguments\n",
- mo->msgtag, oc->path);
+ !option_value_in_cmdline(argc, argv, oc->path) &&
+ !service_open_path_listed(mo->subtype, oc->path)) {
+ fprintf(stderr, "%s: %s: file must be in command line arguments or in %s\n",
+ mo->msgtag, oc->path, FUSE_CONF);
+ return mount_service_send_file_error(mo, EPERM, oc->path);
+ }
open_flags = ntohl(oc->open_flags) | O_CLOEXEC;
drop_privs();
--
2.53.0
^ permalink raw reply related [flat|nested] 26+ messages in thread
* [PATCH v2 04/14] mount_service: use openat to OPEN paths
2026-09-28 11:02 [PATCH v2 00/14] libfuse: Add mount service safety checks and tests Bernd Schubert via B4 Relay
` (2 preceding siblings ...)
2026-09-28 11:02 ` [PATCH v2 03/14] mount_service: refuse paths the user did not name Bernd Schubert via B4 Relay
@ 2026-09-28 11:02 ` Bernd Schubert via B4 Relay
2026-09-29 2:27 ` Darrick J. Wong
2026-09-28 11:02 ` [PATCH v2 05/14] util: give fuservicemount3 an absolute build-tree runpath Bernd Schubert via B4 Relay
` (9 subsequent siblings)
13 siblings, 1 reply; 26+ messages in thread
From: Bernd Schubert via B4 Relay @ 2026-09-28 11:02 UTC (permalink / raw)
To: fuse-devel; +Cc: Darrick J. Wong, neal, Bernd Schubert
From: Bernd Schubert <bernd@bsbernd.com>
The fuse server sends fuservicemount3 a series of requests: OPEN for
its backing file, MNTPT to name the mount point, then MOUNT. The server
chooses the order. For a directory mount point, attach_to_mountpoint()
changes the working directory of the helper to the mount point. The
helper then mounts on ".", so a rename of the path cannot redirect the
mount.
A relative path in an OPEN request after MNTPT resolved inside the
mount point. For "fuservicemount3 disk.img /mnt -t fuse.service_ll",
an OPEN of "disk.img" matched the command line argument, but the helper
opened /mnt/disk.img, not disk.img in the user's working directory. The
helper now opens OPEN paths with openat() on the working directory it
started in.
Assisted-by: LLM
Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
---
util/mount_service.c | 20 +++++++++++++++++++-
1 file changed, 19 insertions(+), 1 deletion(-)
diff --git a/util/mount_service.c b/util/mount_service.c
index b4081d53273e..84e9d831ce03 100644
--- a/util/mount_service.c
+++ b/util/mount_service.c
@@ -84,6 +84,9 @@ struct mount_service {
/* fd for fsopen */
int fsopenfd;
+ /* fd for the initial working directory */
+ int cwdfd;
+
/* did we actually mount successfully? */
bool mounted;
@@ -247,6 +250,18 @@ static int mount_service_init(struct mount_service *mo, int argc, char *argv[])
return -1;
}
+ drop_privs();
+ mo->cwdfd = open(".", O_PATH | O_CLOEXEC);
+ if (mo->cwdfd < 0) {
+ int error = errno;
+
+ restore_privs();
+ fprintf(stderr, "%s: cannot open working directory: %s\n",
+ mo->msgtag, strerror(error));
+ return -1;
+ }
+ restore_privs();
+
return 0;
}
@@ -859,8 +874,9 @@ static int mount_service_open_path(const struct mount_service *mo,
}
open_flags = ntohl(oc->open_flags) | O_CLOEXEC;
+ /* After fchdir to the mountpoint, a relative path would resolve there */
drop_privs();
- fd = open(oc->path, open_flags, ntohl(oc->create_mode));
+ fd = openat(mo->cwdfd, oc->path, open_flags, ntohl(oc->create_mode));
if (fd < 0) {
int error = errno;
@@ -1807,6 +1823,7 @@ static void mount_service_destroy(struct mount_service *mo)
close(mo->fusedevfd);
close(mo->argvfd);
close(mo->fsopenfd);
+ close(mo->cwdfd);
shutdown(mo->sockfd, SHUT_RDWR);
close(mo->sockfd);
@@ -1824,6 +1841,7 @@ static void mount_service_destroy(struct mount_service *mo)
mo->fusedevfd = -1;
mo->mountfd = -1;
mo->fsopenfd = -1;
+ mo->cwdfd = -1;
}
int mount_service_main(int argc, char *argv[])
--
2.53.0
^ permalink raw reply related [flat|nested] 26+ messages in thread
* [PATCH v2 05/14] util: give fuservicemount3 an absolute build-tree runpath
2026-09-28 11:02 [PATCH v2 00/14] libfuse: Add mount service safety checks and tests Bernd Schubert via B4 Relay
` (3 preceding siblings ...)
2026-09-28 11:02 ` [PATCH v2 04/14] mount_service: use openat to OPEN paths Bernd Schubert via B4 Relay
@ 2026-09-28 11:02 ` Bernd Schubert via B4 Relay
2026-09-28 11:02 ` [PATCH v2 06/14] mount.fuse: free the options on the service mount return path Bernd Schubert via B4 Relay
` (8 subsequent siblings)
13 siblings, 0 replies; 26+ messages in thread
From: Bernd Schubert via B4 Relay @ 2026-09-28 11:02 UTC (permalink / raw)
To: fuse-devel; +Cc: Darrick J. Wong, neal, Bernd Schubert
From: Bernd Schubert <bernd@bsbernd.com>
The dynamic loader ignores $ORIGIN runpaths and LD_LIBRARY_PATH for a
setuid program. A build-tree fuservicemount3 made setuid, for example by
"run-tests.py --setuid-helpers", then failed with "libfuse3.so.4: cannot
open shared object file", or loaded the libfuse3.so.4 of the system.
meson removes build_rpath on install, so installed binaries do not change.
Assisted-by: LLM
Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
Reviewed-by: "Darrick J. Wong" <djwong@kernel.org>
---
util/meson.build | 2 ++
1 file changed, 2 insertions(+)
diff --git a/util/meson.build b/util/meson.build
index 28052a65536f..bc266776e153 100644
--- a/util/meson.build
+++ b/util/meson.build
@@ -22,6 +22,8 @@ if private_cfg.get('HAVE_SERVICEMOUNT', false)
link_with: [ libfuse ],
install: true,
install_dir: get_option('sbindir'),
+ # A setuid run ignores the $ORIGIN runpath meson sets
+ build_rpath: join_paths(meson.project_build_root(), 'lib'),
c_args: ['-DFUSE_USE_VERSION=319'] + mount_service_cflags)
endif
--
2.53.0
^ permalink raw reply related [flat|nested] 26+ messages in thread
* [PATCH v2 06/14] mount.fuse: free the options on the service mount return path
2026-09-28 11:02 [PATCH v2 00/14] libfuse: Add mount service safety checks and tests Bernd Schubert via B4 Relay
` (4 preceding siblings ...)
2026-09-28 11:02 ` [PATCH v2 05/14] util: give fuservicemount3 an absolute build-tree runpath Bernd Schubert via B4 Relay
@ 2026-09-28 11:02 ` Bernd Schubert via B4 Relay
2026-09-28 11:02 ` [PATCH v2 07/14] example/single_file: take no sector size from a regular backing file Bernd Schubert via B4 Relay
` (7 subsequent siblings)
13 siblings, 0 replies; 26+ messages in thread
From: Bernd Schubert via B4 Relay @ 2026-09-28 11:02 UTC (permalink / raw)
To: fuse-devel; +Cc: Darrick J. Wong, neal, Bernd Schubert
From: Bernd Schubert <bernd@bsbernd.com>
These leaks were detected by the new tests.
Assisted-by: LLM
Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
Reviewed-by: "Darrick J. Wong" <djwong@kernel.org>
---
util/mount.fuse.c | 42 ++++++++++++++++++++++++++----------------
1 file changed, 26 insertions(+), 16 deletions(-)
diff --git a/util/mount.fuse.c b/util/mount.fuse.c
index c67a0c2b7f2f..1414265fd274 100644
--- a/util/mount.fuse.c
+++ b/util/mount.fuse.c
@@ -56,9 +56,7 @@
#endif
#include "fuse.h"
-#ifdef HAVE_SERVICEMOUNT
-# include "mount_service.h"
-#endif
+#include "mount_service.h"
static char *progname;
@@ -396,6 +394,18 @@ out:
fuse_opt_free_args(&args);
return ret;
}
+#else
+static int try_service_main(const char *argv0, const char *fstype,
+ const char *source, const char *mountpoint,
+ const char *options)
+{
+ (void)argv0;
+ (void)fstype;
+ (void)source;
+ (void)mountpoint;
+ (void)options;
+ return MOUNT_SERVICE_FALLBACK_NEEDED;
+}
#endif
int main(int argc, char *argv[])
@@ -415,6 +425,7 @@ int main(int argc, char *argv[])
int fuse_fd = 0;
int drop_privileges = 0;
char *dev_fd_mountpoint = NULL;
+ int ret;
progname = argv[0];
basename = strrchr(argv[0], '/');
@@ -608,19 +619,17 @@ int main(int argc, char *argv[])
}
#endif
-#ifdef HAVE_SERVICEMOUNT
/*
* Now that we know the desired filesystem type, see if we can find
* a socket service implementing that, if we haven't selected any weird
* options that would prevent that.
*/
if (!pass_fuse_fd && !(setuid_name && setuid_name[0])) {
- int ret = try_service_main(argv[0], type, source, mountpoint,
- options);
+ ret = try_service_main(argv[0], type, source, mountpoint,
+ options);
if (ret != MOUNT_SERVICE_FALLBACK_NEEDED)
- return ret;
+ goto out;
}
-#endif
add_arg(&command, type);
if (source)
@@ -631,17 +640,18 @@ int main(int argc, char *argv[])
add_arg(&command, options);
}
+ execl("/bin/sh", "/bin/sh", "-c", command, NULL);
+ fprintf(stderr, "%s: failed to execute /bin/sh: %s\n", progname,
+ strerror(errno));
+ ret = 1;
+
+out:
+ if (pass_fuse_fd)
+ close(fuse_fd);
free(options);
free(dev_fd_mountpoint);
free(dup_source);
free(setuid_name);
-
- execl("/bin/sh", "/bin/sh", "-c", command, NULL);
- fprintf(stderr, "%s: failed to execute /bin/sh: %s\n", progname,
- strerror(errno));
-
- if (pass_fuse_fd)
- close(fuse_fd);
free(command);
- return 1;
+ return ret;
}
--
2.53.0
^ permalink raw reply related [flat|nested] 26+ messages in thread
* [PATCH v2 07/14] example/single_file: take no sector size from a regular backing file
2026-09-28 11:02 [PATCH v2 00/14] libfuse: Add mount service safety checks and tests Bernd Schubert via B4 Relay
` (5 preceding siblings ...)
2026-09-28 11:02 ` [PATCH v2 06/14] mount.fuse: free the options on the service mount return path Bernd Schubert via B4 Relay
@ 2026-09-28 11:02 ` Bernd Schubert via B4 Relay
2026-09-28 11:02 ` [PATCH v2 08/14] test: check which files fuservicemount3 opens for the server Bernd Schubert via B4 Relay
` (6 subsequent siblings)
13 siblings, 0 replies; 26+ messages in thread
From: Bernd Schubert via B4 Relay @ 2026-09-28 11:02 UTC (permalink / raw)
To: fuse-devel; +Cc: Darrick J. Wong, neal, Bernd Schubert
From: Bernd Schubert <bernd@bsbernd.com>
single_file_configure() took the sector size of a regular backing file
from st_blksize, and refused to start when it was larger than the page
size. For a regular file st_blksize is only a preferred I/O size, and
the file is not opened with O_DIRECT, so any offset works. NFS reports
1 MiB there, so service_ll and service_hl failed on an image on NFS
with "lba size 1048576 smaller than blocksize 4096".
Assisted-by: LLM
Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
Reviewed-by: "Darrick J. Wong" <djwong@kernel.org>
---
example/single_file.c | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/example/single_file.c b/example/single_file.c
index 59dbc6bbac5e..4260ecf3c5f5 100644
--- a/example/single_file.c
+++ b/example/single_file.c
@@ -887,7 +887,8 @@ int single_file_configure(const char *device, const char *filename)
perror(device);
return -1;
}
- lbasize = stbuf.st_blksize;
+ /* A regular file takes any offset; its st_blksize is only an I/O hint */
+ lbasize = S_ISBLK(stbuf.st_mode) ? stbuf.st_blksize : 1;
backing_size = stbuf.st_size;
if (S_ISBLK(stbuf.st_mode)) {
--
2.53.0
^ permalink raw reply related [flat|nested] 26+ messages in thread
* [PATCH v2 08/14] test: check which files fuservicemount3 opens for the server
2026-09-28 11:02 [PATCH v2 00/14] libfuse: Add mount service safety checks and tests Bernd Schubert via B4 Relay
` (6 preceding siblings ...)
2026-09-28 11:02 ` [PATCH v2 07/14] example/single_file: take no sector size from a regular backing file Bernd Schubert via B4 Relay
@ 2026-09-28 11:02 ` Bernd Schubert via B4 Relay
2026-09-29 3:52 ` Darrick J. Wong
2026-09-28 11:02 ` [PATCH v2 09/14] test: check what fuservicemount3 refuses Bernd Schubert via B4 Relay
` (5 subsequent siblings)
13 siblings, 1 reply; 26+ messages in thread
From: Bernd Schubert via B4 Relay @ 2026-09-28 11:02 UTC (permalink / raw)
To: fuse-devel; +Cc: Darrick J. Wong, neal, Bernd Schubert
From: Bernd Schubert <bernd@bsbernd.com>
No test covered the OPEN and OPEN_BDEV commands of fuservicemount3.
test_service is a fuse server that sends one request and prints the
errno it got back. socket_activate.py starts it the way a systemd
socket unit does, so the tests do not depend on systemd.
Assisted-by: LLM
Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
---
.gitignore | 1 +
test/cases/lib/service.sh | 83 ++++++++++++++++
test/cases/lib/socket_activate.py | 42 ++++++++
test/cases/mount/service-open-bound.sh | 61 ++++++++++++
test/meson.build | 6 ++
test/test_service.c | 176 +++++++++++++++++++++++++++++++++
6 files changed, 369 insertions(+)
diff --git a/.gitignore b/.gitignore
index 877c2fe2ba87..ebf9b7cdae47 100644
--- a/.gitignore
+++ b/.gitignore
@@ -42,6 +42,7 @@ TAGS
/test/test_setattr
/test/test_api_30
/test/test_fuser_conf
+/test/test_service
/build/
# run-tests.py output, when pointed at the source tree with --run-dir
/fuse-tests/
diff --git a/test/cases/lib/service.sh b/test/cases/lib/service.sh
new file mode 100644
index 000000000000..d0a0fa0183d0
--- /dev/null
+++ b/test/cases/lib/service.sh
@@ -0,0 +1,83 @@
+# lib/service.sh - start a fuse service server the way a systemd socket unit
+# does, for the fuservicemount3 cases.
+#
+# Sourced after common.sh. The socket has to be in the build-time socket
+# directory, so every caller runs as root.
+
+# service_setup <subtype>
+# Set service_sock. At exit remove the socket and any mount on $TEST_MNT.
+service_setup()
+{
+ service_subtype=$1
+ service_runs=0
+ # A case may set it, to add arguments to the helper command line
+ service_helper_args=()
+ service_sock=$("$FUSE_TEST_BIN_DIR/test_service" socket-path "$1")
+ # Every service script binds its own socket here; removing the
+ # directory would break another script's bind()
+ mkdir -p "$(dirname "$service_sock")"
+ _at_exit "rm -f '$service_sock'"
+ _at_exit "umount -l '$TEST_MNT' 2>/dev/null"
+}
+
+# service_start <log> <program> [args...]
+# Listen on service_sock and run <program> for the first connection, with its
+# output in <log>. Sets service_pid.
+service_start()
+{
+ local log=$1; shift
+
+ rm -f "$service_sock"
+ python3 "$TEST_LIB/socket_activate.py" "$service_sock" "$@" \
+ >"$log" 2>&1 &
+ service_pid=$!
+ _wait_for 10 "grep -q '^listening' '$log'" ||
+ _fail "$service_sock never listened"
+}
+
+# service_wait_exit
+# Reap the server and set service_rc to its exit status. A helper that never
+# connected leaves the activator in accept(), which is a failure.
+service_wait_exit()
+{
+ _wait_for 10 "! kill -0 $service_pid 2>/dev/null" || {
+ kill "$service_pid" 2>/dev/null || true
+ _fail "server (pid $service_pid) did not exit"
+ }
+ service_rc=0
+ wait "$service_pid" || service_rc=$?
+}
+
+# service_mount <source> <mountpoint> <case> [args...]
+# Run fuservicemount3 once against test_service <case> [args...] and reap the
+# server. Sets service_log.
+service_mount()
+{
+ local source=$1 mnt=$2; shift 2
+
+ service_log=$TEST_LOGDIR/fs-$service_runs-$1.out
+ service_runs=$((service_runs + 1))
+ service_start "$service_log" "$FUSE_TEST_BIN_DIR/test_service" "$@"
+
+ # Its exit status depends on the case; the server's line is the verdict.
+ "$FUSE_UTIL_DIR/fuservicemount3" "$source" "$mnt" \
+ -t "fuse.$service_subtype" "${service_helper_args[@]}" || true
+
+ service_wait_exit
+}
+
+# service_result <what>
+# The last server prints one "<what> result: <value>" line, for example
+# "request result: EPERM". Echo <value>; fail on no such line or several.
+service_result()
+{
+ local count
+
+ count=$(grep -c "^$1 result: " "$service_log") || true
+ if [ "$count" != 1 ]; then
+ cat "$service_log" >&2
+ _fail "$service_log: $count \"$1 result:\" lines, want 1"
+ fi
+ # -n and p: print only the line the substitution matched
+ sed -n "s/^$1 result: //p" "$service_log"
+}
diff --git a/test/cases/lib/socket_activate.py b/test/cases/lib/socket_activate.py
new file mode 100755
index 000000000000..c33ca3a61ff2
--- /dev/null
+++ b/test/cases/lib/socket_activate.py
@@ -0,0 +1,42 @@
+#!/usr/bin/env python3
+"""socket_activate.py <socket-path> <program> [args...]
+
+Listen on a SOCK_SEQPACKET socket, accept one connection and exec <program>
+with it, the way systemd starts a socket unit with Accept=yes: the connection
+is fd 3, LISTEN_FDS=1 and LISTEN_PID is the pid that runs <program>.
+
+Prints "listening" once a connect() can succeed.
+"""
+
+import os
+import socket
+import sys
+
+SD_LISTEN_FDS_START = 3
+
+
+def main():
+ if len(sys.argv) < 3:
+ sys.exit(__doc__)
+ path = sys.argv[1]
+ program = sys.argv[2:]
+
+ listener = socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET)
+ listener.bind(path)
+ listener.listen(1)
+ print('listening', flush=True)
+
+ conn, _ = listener.accept()
+ listener.close()
+
+ # conn itself is close-on-exec and goes away with the exec
+ os.dup2(conn.fileno(), SD_LISTEN_FDS_START)
+ os.set_inheritable(SD_LISTEN_FDS_START, True)
+ os.environ['LISTEN_FDS'] = '1'
+ # exec keeps the pid
+ os.environ['LISTEN_PID'] = str(os.getpid())
+ os.execv(program[0], program)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/cases/mount/service-open-bound.sh b/test/cases/mount/service-open-bound.sh
new file mode 100755
index 000000000000..3ef3690a42e9
--- /dev/null
+++ b/test/cases/mount/service-open-bound.sh
@@ -0,0 +1,61 @@
+#!/usr/bin/env bash
+# GROUP: mount
+#
+# fuservicemount3 opens a file for the fuse server only if the path is on its
+# command line.
+
+_fuse_no_mount_needed=1
+. "$TEST_LIB/common.sh"
+
+_require_linux "fuservicemount3"
+_require_root
+_require_fuse_device
+_require_binary util/fuservicemount3
+_require_binary test/test_service
+
+. "$TEST_LIB/service.sh"
+
+# One socket per run, so a parallel run does not connect to this one
+subtype=test-open-$$
+img=$TEST_SRC/img
+
+touch "$img"
+service_setup "$subtype"
+
+# service_open_request <case> <path> <expected errno name, or 0>
+# Mount $img through fuservicemount3 with the server started for <case>.
+service_open_request()
+{
+ local case=$1 path=$2 expected=$3
+
+ service_mount "$img" "$TEST_MNT" "$case" "$path"
+ _assert_eq "$(service_result request)" "$expected" "$case $path"
+}
+
+# $img is on the command line
+service_open_request open "$img" 0
+# Root can read /etc/passwd, so EPERM comes from the command line check
+service_open_request open /etc/passwd EPERM
+# Passes the command line check, fails the block device check
+service_open_request open-bdev "$img" ENOTBLK
+# Not ENOTBLK: OPEN_BDEV gets the command line check first
+service_open_request open-bdev /etc/passwd EPERM
+
+# A path in an option value, as the server's option parser splits it
+journal=$TEST_SRC/journal,img
+touch "$journal"
+service_helper_args=(-o "ro,journal_dev=${journal//,/\\,}")
+service_open_request open "$journal" 0
+# The directory of an option value is not a path the user named
+service_open_request open "$TEST_SRC" EPERM
+service_helper_args=("-J$journal")
+service_open_request open "$journal" 0
+service_helper_args=()
+
+# After MNTPT the helper runs inside $TEST_MNT. A relative path must still
+# resolve in the directory the helper started in.
+cd "$TEST_SRC"
+service_mount img "$TEST_MNT" open-after-mount img
+cd "$OLDPWD"
+_assert_eq "$(service_result request)" 0 "open-after-mount img"
+umount "$TEST_MNT"
diff --git a/test/meson.build b/test/meson.build
index 68e083f7885c..ec48dab44a61 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -54,6 +54,12 @@ if build_utils
c_args: '-DFUSE_CONF="fuse.conf"',
install: false)
endif
+if private_cfg.get('HAVE_SERVICEMOUNT', false)
+ td += executable('test_service', 'test_service.c',
+ include_directories: include_dirs,
+ link_with: [ libfuse ],
+ install: false)
+endif
if meson.is_subproject()
# Skipped rather than run: the tests mount filesystems, which is not
diff --git a/test/test_service.c b/test/test_service.c
new file mode 100644
index 000000000000..0d54df3427a9
--- /dev/null
+++ b/test/test_service.c
@@ -0,0 +1,176 @@
+/*
+ * FUSE: Filesystem in Userspace
+ *
+ * This program can be distributed under the terms of the GNU GPLv2.
+ * See the file GPL2.txt.
+ *
+ * A fuse service server for the service mount tests. Each mode takes one
+ * step against fuservicemount3 and prints the name of the errno that came
+ * back, 0 for success.
+ *
+ * test_service socket-path <subtype>
+ * test_service open <path>
+ * test_service open-bdev <path>
+ * test_service open-after-mount <path>
+ */
+
+#define FUSE_USE_VERSION FUSE_MAKE_VERSION(3, 19)
+
+/* strerrorname_np() */
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#endif
+
+#include "fuse_config.h"
+#include <fuse_lowlevel.h>
+#include <fuse_service.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/stat.h>
+
+static const struct fuse_lowlevel_ops test_service_oper = { };
+
+/* @return "EPERM" and so on, "0" for no error */
+static const char *errno_name(int error)
+{
+ const char *name;
+
+ if (!error)
+ return "0";
+
+ name = strerrorname_np(error);
+ return name ? name : "unknown errno";
+}
+
+/* The first non-option argument is the mount source, not the mountpoint */
+static int skip_source(void *data, const char *arg, int key,
+ struct fuse_args *outargs)
+{
+ bool *source_seen = data;
+
+ (void)arg;
+ (void)outargs;
+
+ if (key == FUSE_OPT_KEY_NONOPT && !*source_seen) {
+ *source_seen = true;
+ return 0;
+ }
+ return 1;
+}
+
+/*
+ * Mount through the helper so that it has a mount point when the file is
+ * requested.
+ *
+ * @return the mounted session, or NULL on failure
+ */
+static struct fuse_session *session_mounted(struct fuse_service *service,
+ const char *argv0)
+{
+ struct fuse_args args = FUSE_ARGS_INIT(0, NULL);
+ struct fuse_cmdline_opts opts = { };
+ struct fuse_session *se = NULL;
+ bool source_seen = false;
+
+ if (fuse_opt_add_arg(&args, argv0) ||
+ fuse_service_append_args(service, &args) ||
+ fuse_opt_parse(&args, &source_seen, NULL, skip_source) ||
+ fuse_service_parse_cmdline_opts(&args, &opts))
+ goto out;
+
+ se = fuse_session_new(&args, &test_service_oper,
+ sizeof(test_service_oper), NULL);
+ if (!se)
+ goto out;
+
+ if (fuse_service_session_mount(service, se, S_IFDIR, &opts)) {
+ fuse_session_destroy(se);
+ se = NULL;
+ }
+
+out:
+ free(opts.mountpoint);
+ fuse_opt_free_args(&args);
+ return se;
+}
+
+/* @return 0 when the result was printed, negative errno otherwise */
+static int request_printed(const struct fuse_service *service,
+ const char *path, bool blockdev)
+{
+ int fd;
+ int ret;
+
+ if (blockdev)
+ ret = fuse_service_request_blockdev(service, path, O_RDONLY,
+ 0, 0, 0);
+ else
+ ret = fuse_service_request_file(service, path, O_RDONLY, 0, 0);
+ if (ret)
+ return ret;
+
+ /* A refusal by the helper is a success return, with -errno in fd */
+ ret = fuse_service_receive_file(service, path, &fd);
+ if (ret)
+ return ret;
+
+ if (fd >= 0) {
+ close(fd);
+ printf("request result: 0\n");
+ } else {
+ printf("request result: %s\n", errno_name(-fd));
+ }
+ fflush(stdout);
+ return 0;
+}
+
+int main(int argc, char *argv[])
+{
+ struct fuse_service *service = NULL;
+ struct fuse_session *se = NULL;
+ bool blockdev = false;
+ int ret = 1;
+
+ if (argc != 3) {
+ fprintf(stderr, "usage: %s socket-path <subtype>\n", argv[0]);
+ fprintf(stderr, " %s open|open-bdev|open-after-mount <path>\n",
+ argv[0]);
+ return 1;
+ }
+
+ if (!strcmp(argv[1], "socket-path")) {
+ printf("%s/%s\n", FUSE_SERVICE_SOCKET_DIR, argv[2]);
+ return 0;
+ }
+
+ if (fuse_service_accept(&service) || !fuse_service_accepted(service)) {
+ fprintf(stderr, "%s: not started as a fuse service\n", argv[0]);
+ return 1;
+ }
+
+ if (!strcmp(argv[1], "open-after-mount")) {
+ se = session_mounted(service, argv[0]);
+ if (!se)
+ goto out;
+ } else if (!strcmp(argv[1], "open-bdev")) {
+ blockdev = true;
+ } else if (strcmp(argv[1], "open")) {
+ fprintf(stderr, "%s: unknown case %s\n", argv[0], argv[1]);
+ goto out;
+ }
+
+ if (request_printed(service, argv[2], blockdev))
+ goto out;
+
+ ret = 0;
+out:
+ fuse_service_send_goodbye(service, ret);
+ fuse_service_destroy(&service);
+ /* Closes /dev/fuse; the test script unmounts */
+ if (se)
+ fuse_session_destroy(se);
+ return ret;
+}
--
2.53.0
^ permalink raw reply related [flat|nested] 26+ messages in thread
* [PATCH v2 09/14] test: check what fuservicemount3 refuses
2026-09-28 11:02 [PATCH v2 00/14] libfuse: Add mount service safety checks and tests Bernd Schubert via B4 Relay
` (7 preceding siblings ...)
2026-09-28 11:02 ` [PATCH v2 08/14] test: check which files fuservicemount3 opens for the server Bernd Schubert via B4 Relay
@ 2026-09-28 11:02 ` Bernd Schubert via B4 Relay
2026-09-29 3:55 ` Darrick J. Wong
2026-09-28 11:02 ` [PATCH v2 10/14] test: mount the service examples through fuservicemount3 Bernd Schubert via B4 Relay
` (4 subsequent siblings)
13 siblings, 1 reply; 26+ messages in thread
From: Bernd Schubert via B4 Relay @ 2026-09-28 11:02 UTC (permalink / raw)
To: fuse-devel; +Cc: Darrick J. Wong, neal, Bernd Schubert
From: Bernd Schubert <bernd@bsbernd.com>
fuservicemount3 runs setuid root and acts on requests from a fuse
server it does not trust. No test covered its checks on the subtype,
the mount point and its file type, fuseblk for a user who is not root,
or a server that exits before its goodbye.
Assisted-by: LLM
Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
---
test/cases/lib/service.sh | 20 ++++--
test/cases/mount/service-caps.sh | 20 ++++++
test/cases/mount/service-check.sh | 38 ++++++++++++
test/cases/mount/service-mountpoint.sh | 36 +++++++++++
test/cases/mount/service-nonroot.sh | 54 +++++++++++++++++
test/cases/mount/service-server-exit.sh | 22 +++++++
test/test_service.c | 104 ++++++++++++++++++++++++--------
7 files changed, 266 insertions(+), 28 deletions(-)
diff --git a/test/cases/lib/service.sh b/test/cases/lib/service.sh
index d0a0fa0183d0..60c6c72820ef 100644
--- a/test/cases/lib/service.sh
+++ b/test/cases/lib/service.sh
@@ -10,6 +10,8 @@ service_setup()
{
service_subtype=$1
service_runs=0
+ # A case may prefix it, to run the helper as another user
+ service_helper=("$FUSE_UTIL_DIR/fuservicemount3")
# A case may set it, to add arguments to the helper command line
service_helper_args=()
service_sock=$("$FUSE_TEST_BIN_DIR/test_service" socket-path "$1")
@@ -33,6 +35,16 @@ service_start()
service_pid=$!
_wait_for 10 "grep -q '^listening' '$log'" ||
_fail "$service_sock never listened"
+ # connect() needs write permission, and a case may run as another user
+ chmod 0666 "$service_sock"
+}
+
+# service_stop
+# Kill an activator that no helper connected to.
+service_stop()
+{
+ kill "$service_pid" 2>/dev/null || true
+ wait "$service_pid" 2>/dev/null || true
}
# service_wait_exit
@@ -50,7 +62,7 @@ service_wait_exit()
# service_mount <source> <mountpoint> <case> [args...]
# Run fuservicemount3 once against test_service <case> [args...] and reap the
-# server. Sets service_log.
+# server. Sets service_log and service_helper_rc.
service_mount()
{
local source=$1 mnt=$2; shift 2
@@ -59,9 +71,9 @@ service_mount()
service_runs=$((service_runs + 1))
service_start "$service_log" "$FUSE_TEST_BIN_DIR/test_service" "$@"
- # Its exit status depends on the case; the server's line is the verdict.
- "$FUSE_UTIL_DIR/fuservicemount3" "$source" "$mnt" \
- -t "fuse.$service_subtype" "${service_helper_args[@]}" || true
+ service_helper_rc=0
+ "${service_helper[@]}" "$source" "$mnt" -t "fuse.$service_subtype" \
+ "${service_helper_args[@]}" || service_helper_rc=$?
service_wait_exit
}
diff --git a/test/cases/mount/service-caps.sh b/test/cases/mount/service-caps.sh
new file mode 100755
index 000000000000..69bc2bdbef8d
--- /dev/null
+++ b/test/cases/mount/service-caps.sh
@@ -0,0 +1,20 @@
+#!/usr/bin/env bash
+# GROUP: mount
+#
+# fuservicemount3 run by root offers the fuse server allow_other and fuseblk.
+
+_fuse_no_mount_needed=1
+. "$TEST_LIB/common.sh"
+
+_require_linux "fuservicemount3"
+_require_root
+_require_fuse_device
+_require_binary util/fuservicemount3
+_require_binary test/test_service
+
+. "$TEST_LIB/service.sh"
+
+service_setup "test-caps-$$"
+
+service_mount "$service_subtype" "$TEST_MNT" caps
+_assert_eq "$(service_result caps)" "allow_other=1 fuseblk=1" "caps as root"
diff --git a/test/cases/mount/service-check.sh b/test/cases/mount/service-check.sh
new file mode 100755
index 000000000000..ec30031d039e
--- /dev/null
+++ b/test/cases/mount/service-check.sh
@@ -0,0 +1,38 @@
+#!/usr/bin/env bash
+# GROUP: mount
+#
+# fuservicemount3 --check succeeds only for a socket named after the subtype,
+# and never for a subtype that is a path.
+
+_fuse_no_mount_needed=1
+. "$TEST_LIB/common.sh"
+
+_require_linux "fuservicemount3"
+_require_root
+_require_binary util/fuservicemount3
+_require_binary test/test_service
+
+. "$TEST_LIB/service.sh"
+
+subtype=test-check-$$
+service_setup "$subtype"
+
+# check_rc <fstype>
+check_rc()
+{
+ local rc=0
+
+ "$FUSE_UTIL_DIR/fuservicemount3" -t "$1" --check || rc=$?
+ echo "$rc"
+}
+
+_assert_eq "$(check_rc "fuse.$subtype")" 1 "no socket"
+
+touch "$service_sock"
+_assert_eq "$(check_rc "fuse.$subtype")" 1 "regular file"
+
+service_start "$TEST_LOGDIR/fs-check.out" "$FUSE_TEST_BIN_DIR/test_service" caps
+_assert_eq "$(check_rc "fuse.$subtype")" 0 "listening socket"
+# The same socket, named through a path
+_assert_eq "$(check_rc "fuse../$subtype")" 1 "subtype ./$subtype"
+service_stop
diff --git a/test/cases/mount/service-mountpoint.sh b/test/cases/mount/service-mountpoint.sh
new file mode 100755
index 000000000000..0df4733389e0
--- /dev/null
+++ b/test/cases/mount/service-mountpoint.sh
@@ -0,0 +1,36 @@
+#!/usr/bin/env bash
+# GROUP: mount
+#
+# The fuse server names the mount point, so fuservicemount3 has to refuse one
+# that is not on its command line, and one of the wrong file type.
+
+_fuse_no_mount_needed=1
+. "$TEST_LIB/common.sh"
+
+_require_linux "fuservicemount3"
+_require_root
+_require_fuse_device
+_require_binary util/fuservicemount3
+_require_binary test/test_service
+
+. "$TEST_LIB/service.sh"
+
+file=$TEST_SRC/file
+
+touch "$file"
+service_setup "test-mntpt-$$"
+
+service_mount "$service_subtype" "$TEST_MNT" mount dir
+_assert_eq "$(service_result mount)" 0 "mount dir on a directory"
+_assert_fstype "$TEST_MNT" "fuse.$service_subtype" fuse
+umount "$TEST_MNT"
+
+service_mount "$service_subtype" "$TEST_MNT" mount-elsewhere "$TEST_SRC"
+_assert_eq "$(service_result mount)" EINVAL \
+ "mount point not on the command line"
+
+service_mount "$service_subtype" "$TEST_MNT" mount file
+_assert_eq "$(service_result mount)" EISDIR "mount file on a directory"
+
+service_mount "$service_subtype" "$file" mount dir
+_assert_eq "$(service_result mount)" ENOTDIR "mount dir on a regular file"
diff --git a/test/cases/mount/service-nonroot.sh b/test/cases/mount/service-nonroot.sh
new file mode 100755
index 000000000000..0c211d0a9965
--- /dev/null
+++ b/test/cases/mount/service-nonroot.sh
@@ -0,0 +1,54 @@
+#!/usr/bin/env bash
+# GROUP: mount
+#
+# fuservicemount3 installed setuid and run by an unprivileged user mounts only
+# on a directory that user can write, and never offers fuseblk.
+
+_fuse_no_mount_needed=1
+. "$TEST_LIB/common.sh"
+
+_require_linux "fuservicemount3"
+# Root installs the setuid copy and the socket
+_require_root
+_require_fuse_device
+_require_binary util/fuservicemount3
+_require_binary test/test_service
+_require_prog setpriv
+_require_prog findmnt
+
+user=nobody
+uid=$(id -u "$user") || _notrun "no user $user"
+gid=$(id -g "$user")
+run_as=(setpriv --reuid="$uid" --regid="$gid" --clear-groups)
+
+helper=$TEST_WORKDIR/fuservicemount3
+case ",$(findmnt -n -o OPTIONS -T "$TEST_WORKDIR")," in
+*,nosuid,*) _notrun "$TEST_WORKDIR is on a nosuid mount" ;;
+esac
+cp "$FUSE_UTIL_DIR/fuservicemount3" "$helper"
+_at_exit "rm -f '$helper'"
+chmod 4755 "$helper"
+"${run_as[@]}" test -x "$helper" || _notrun "$user cannot reach $helper"
+
+. "$TEST_LIB/service.sh"
+
+service_setup "test-nonroot-$$"
+service_helper=("${run_as[@]}" "$helper")
+root_dir=$TEST_WORKDIR/root-mnt
+mkdir -m 0755 "$root_dir"
+
+chown "$uid" "$TEST_MNT"
+service_mount "$service_subtype" "$TEST_MNT" mount dir
+_assert_eq "$(service_result mount)" 0 "mount on a directory $user owns"
+umount "$TEST_MNT"
+
+service_mount "$service_subtype" "$root_dir" mount dir
+_assert_eq "$(service_result mount)" EPERM \
+ "mount on a directory owned by root"
+
+# allow_other depends on user_allow_other in the system fuse.conf
+service_mount "$service_subtype" "$TEST_MNT" caps
+case $(service_result caps) in
+*" fuseblk=0") ;;
+*) _fail "caps as $user: $(service_result caps)" ;;
+esac
diff --git a/test/cases/mount/service-server-exit.sh b/test/cases/mount/service-server-exit.sh
new file mode 100755
index 000000000000..6304f20ff2cb
--- /dev/null
+++ b/test/cases/mount/service-server-exit.sh
@@ -0,0 +1,22 @@
+#!/usr/bin/env bash
+# GROUP: mount
+#
+# A fuse server that exits without a goodbye makes fuservicemount3 fail, and
+# leaves nothing mounted.
+
+_fuse_no_mount_needed=1
+. "$TEST_LIB/common.sh"
+
+_require_linux "fuservicemount3"
+_require_root
+_require_fuse_device
+_require_binary util/fuservicemount3
+_require_binary test/test_service
+
+. "$TEST_LIB/service.sh"
+
+service_setup "test-exit-$$"
+
+service_mount "$service_subtype" "$TEST_MNT" exit-early
+_assert_ne "$service_helper_rc" 0 "fuservicemount3 exit status"
+_assert_eq "$(mountinfo_field "$TEST_MNT" fstype)" "" "$TEST_MNT mounted"
diff --git a/test/test_service.c b/test/test_service.c
index 0d54df3427a9..7bbd14e2650d 100644
--- a/test/test_service.c
+++ b/test/test_service.c
@@ -12,6 +12,10 @@
* test_service open <path>
* test_service open-bdev <path>
* test_service open-after-mount <path>
+ * test_service mount dir|file
+ * test_service mount-elsewhere <mountpoint>
+ * test_service caps
+ * test_service exit-early
*/
#define FUSE_USE_VERSION FUSE_MAKE_VERSION(3, 19)
@@ -62,18 +66,25 @@ static int skip_source(void *data, const char *arg, int key,
}
/*
- * Mount through the helper so that it has a mount point when the file is
- * requested.
+ * Mount through the helper. On success *sep is the mounted session, which
+ * keeps /dev/fuse open until it is destroyed.
*
- * @return the mounted session, or NULL on failure
+ * @param fmt mount point type the helper has to find
+ * @param mountpoint sent in place of the one on the command line, or NULL
+ * @return 0 when the result was printed, -1 otherwise
*/
-static struct fuse_session *session_mounted(struct fuse_service *service,
- const char *argv0)
+static int mount_printed(struct fuse_service *service, const char *argv0,
+ mode_t fmt, const char *mountpoint,
+ struct fuse_session **sep)
{
struct fuse_args args = FUSE_ARGS_INIT(0, NULL);
struct fuse_cmdline_opts opts = { };
- struct fuse_session *se = NULL;
+ struct fuse_session *se;
bool source_seen = false;
+ int printed = -1;
+ int ret;
+
+ *sep = NULL;
if (fuse_opt_add_arg(&args, argv0) ||
fuse_service_append_args(service, &args) ||
@@ -81,20 +92,30 @@ static struct fuse_session *session_mounted(struct fuse_service *service,
fuse_service_parse_cmdline_opts(&args, &opts))
goto out;
+ if (mountpoint) {
+ free(opts.mountpoint);
+ opts.mountpoint = strdup(mountpoint);
+ if (!opts.mountpoint)
+ goto out;
+ }
+
se = fuse_session_new(&args, &test_service_oper,
sizeof(test_service_oper), NULL);
if (!se)
goto out;
- if (fuse_service_session_mount(service, se, S_IFDIR, &opts)) {
+ ret = fuse_service_session_mount(service, se, fmt, &opts);
+ if (ret)
fuse_session_destroy(se);
- se = NULL;
- }
+ else
+ *sep = se;
+ printf("mount result: %s\n", errno_name(-ret));
+ printed = 0;
out:
free(opts.mountpoint);
fuse_opt_free_args(&args);
- return se;
+ return printed;
}
/* @return 0 when the result was printed, negative errno otherwise */
@@ -127,22 +148,40 @@ static int request_printed(const struct fuse_service *service,
return 0;
}
+/* @return S_IFDIR or S_IFREG, 0 for an unknown name */
+static mode_t mount_format(const char *name)
+{
+ if (!strcmp(name, "dir"))
+ return S_IFDIR;
+ if (!strcmp(name, "file"))
+ return S_IFREG;
+ return 0;
+}
+
int main(int argc, char *argv[])
{
struct fuse_service *service = NULL;
struct fuse_session *se = NULL;
- bool blockdev = false;
+ const char *mode;
+ const char *arg;
int ret = 1;
- if (argc != 3) {
+ if (argc != 2 && argc != 3) {
fprintf(stderr, "usage: %s socket-path <subtype>\n", argv[0]);
fprintf(stderr, " %s open|open-bdev|open-after-mount <path>\n",
argv[0]);
+ fprintf(stderr, " %s mount dir|file\n", argv[0]);
+ fprintf(stderr, " %s mount-elsewhere <mountpoint>\n",
+ argv[0]);
+ fprintf(stderr, " %s caps|exit-early\n", argv[0]);
return 1;
}
+ mode = argv[1];
+ /* argv[argc] is NULL */
+ arg = argv[2];
- if (!strcmp(argv[1], "socket-path")) {
- printf("%s/%s\n", FUSE_SERVICE_SOCKET_DIR, argv[2]);
+ if (!strcmp(mode, "socket-path") && arg) {
+ printf("%s/%s\n", FUSE_SERVICE_SOCKET_DIR, arg);
return 0;
}
@@ -151,19 +190,36 @@ int main(int argc, char *argv[])
return 1;
}
- if (!strcmp(argv[1], "open-after-mount")) {
- se = session_mounted(service, argv[0]);
- if (!se)
- goto out;
- } else if (!strcmp(argv[1], "open-bdev")) {
- blockdev = true;
- } else if (strcmp(argv[1], "open")) {
- fprintf(stderr, "%s: unknown case %s\n", argv[0], argv[1]);
- goto out;
+ if (!strcmp(mode, "exit-early")) {
+ /* No goodbye, the helper only sees the connection close */
+ fuse_service_destroy(&service);
+ return 0;
}
- if (request_printed(service, argv[2], blockdev))
+ if (!strcmp(mode, "caps")) {
+ printf("caps result: allow_other=%d fuseblk=%d\n",
+ fuse_service_can_allow_other(service),
+ fuse_service_can_fuseblk(service));
+ } else if (!strcmp(mode, "mount") && arg && mount_format(arg)) {
+ if (mount_printed(service, argv[0], mount_format(arg), NULL,
+ &se))
+ goto out;
+ } else if (!strcmp(mode, "mount-elsewhere") && arg) {
+ if (mount_printed(service, argv[0], S_IFDIR, arg, &se))
+ goto out;
+ } else if (!strcmp(mode, "open-after-mount") && arg) {
+ if (mount_printed(service, argv[0], S_IFDIR, NULL, &se) || !se)
+ goto out;
+ if (request_printed(service, arg, false))
+ goto out;
+ } else if ((!strcmp(mode, "open") || !strcmp(mode, "open-bdev")) &&
+ arg) {
+ if (request_printed(service, arg, !strcmp(mode, "open-bdev")))
+ goto out;
+ } else {
+ fprintf(stderr, "%s: unknown case %s\n", argv[0], mode);
goto out;
+ }
ret = 0;
out:
--
2.53.0
^ permalink raw reply related [flat|nested] 26+ messages in thread
* [PATCH v2 10/14] test: mount the service examples through fuservicemount3
2026-09-28 11:02 [PATCH v2 00/14] libfuse: Add mount service safety checks and tests Bernd Schubert via B4 Relay
` (8 preceding siblings ...)
2026-09-28 11:02 ` [PATCH v2 09/14] test: check what fuservicemount3 refuses Bernd Schubert via B4 Relay
@ 2026-09-28 11:02 ` Bernd Schubert via B4 Relay
2026-09-28 11:02 ` [PATCH v2 11/14] test: run mkfs.ext4 through the service examples Bernd Schubert via B4 Relay
` (3 subsequent siblings)
13 siblings, 0 replies; 26+ messages in thread
From: Bernd Schubert via B4 Relay @ 2026-09-28 11:02 UTC (permalink / raw)
To: fuse-devel; +Cc: Darrick J. Wong, neal, Bernd Schubert
From: Bernd Schubert <bernd@bsbernd.com>
No test ran service_ll, service_hl or null in service mode. No test
checked that mount.fuse3 mounts through a listening service socket, and
runs the filesystem program when the socket refuses the connection.
Assisted-by: LLM
Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
Reviewed-by: "Darrick J. Wong" <djwong@kernel.org>
---
test/cases/lib/service-example.sh | 62 ++++++++++++++++++++++++++++++++++
test/cases/mount/service-hl.sh | 7 ++++
test/cases/mount/service-ll.sh | 7 ++++
test/cases/mount/service-mount-fuse.sh | 31 +++++++++++++++++
test/cases/mount/service-null.sh | 33 ++++++++++++++++++
5 files changed, 140 insertions(+)
diff --git a/test/cases/lib/service-example.sh b/test/cases/lib/service-example.sh
new file mode 100644
index 000000000000..878b379b8b16
--- /dev/null
+++ b/test/cases/lib/service-example.sh
@@ -0,0 +1,62 @@
+# lib/service-example.sh - body for the service_ll / service_hl cases.
+#
+# Caller sets FS_NAME and LAUNCH before sourcing:
+# FS_NAME service_ll | service_hl
+# LAUNCH fuservicemount3 | mount_fuse
+#
+# The file the example serves has to read back as the image, and what is
+# written through the mount has to reach the image.
+
+_fuse_no_mount_needed=1
+. "$TEST_LIB/common.sh"
+
+# Before the gates: a misspelled FS_NAME would otherwise skip as "not built"
+case ${FS_NAME:-} in
+service_ll | service_hl) ;;
+*) _fail "unknown FS_NAME '${FS_NAME:-}'" ;;
+esac
+case ${LAUNCH:-} in
+fuservicemount3 | mount_fuse) ;;
+*) _fail "unknown LAUNCH '${LAUNCH:-}'" ;;
+esac
+
+_require_linux "fuservicemount3"
+_require_root
+_require_fuse_device
+_require_binary util/fuservicemount3
+_require_binary "example/$FS_NAME"
+[ "$LAUNCH" != mount_fuse ] || _require_binary util/mount.fuse3
+
+. "$TEST_LIB/service.sh"
+
+service_setup "test-$FS_NAME-$$"
+img=$TEST_SRC/img
+old=$TEST_TMP/old
+new=$TEST_TMP/new
+
+# The size has to be a multiple of the page size
+head -c 1048576 /dev/urandom >"$old"
+head -c 1048576 /dev/urandom >"$new"
+cp "$old" "$img"
+
+service_start "$TEST_LOGDIR/fs-$FS_NAME.out" "$FUSE_EXAMPLE_DIR/$FS_NAME"
+case $LAUNCH in
+fuservicemount3)
+ "$FUSE_UTIL_DIR/fuservicemount3" "$img" "$TEST_MNT" \
+ -t "fuse.$service_subtype" ||
+ _fail "fuservicemount3 did not mount $FS_NAME"
+ ;;
+mount_fuse)
+ "$FUSE_UTIL_DIR/mount.fuse3" "$service_subtype#$img" "$TEST_MNT" ||
+ _fail "mount.fuse3 did not mount $FS_NAME"
+ ;;
+esac
+
+_assert_fstype "$TEST_MNT" "fuse.$service_subtype" fuse
+_assert_file_eq "$TEST_MNT/single_file" "$old"
+dd if="$new" of="$TEST_MNT/single_file" bs=64k conv=notrunc,fsync status=none
+
+umount "$TEST_MNT"
+service_wait_exit
+_assert_eq "$service_rc" 0 "$FS_NAME exit status"
+_assert_file_eq "$img" "$new"
diff --git a/test/cases/mount/service-hl.sh b/test/cases/mount/service-hl.sh
new file mode 100755
index 000000000000..6db3add3a5d1
--- /dev/null
+++ b/test/cases/mount/service-hl.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+# GROUP: mount
+
+FS_NAME=service_hl
+LAUNCH=fuservicemount3
+
+. "$TEST_LIB/service-example.sh"
diff --git a/test/cases/mount/service-ll.sh b/test/cases/mount/service-ll.sh
new file mode 100755
index 000000000000..036553889f7a
--- /dev/null
+++ b/test/cases/mount/service-ll.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+# GROUP: mount
+
+FS_NAME=service_ll
+LAUNCH=fuservicemount3
+
+. "$TEST_LIB/service-example.sh"
diff --git a/test/cases/mount/service-mount-fuse.sh b/test/cases/mount/service-mount-fuse.sh
new file mode 100755
index 000000000000..6bc95240c772
--- /dev/null
+++ b/test/cases/mount/service-mount-fuse.sh
@@ -0,0 +1,31 @@
+#!/usr/bin/env bash
+# GROUP: mount
+#
+# mount.fuse3 mounts through the service when its socket listens, and execs a
+# program named after the type when the socket refuses the connection.
+
+_fuse_no_mount_needed=1
+. "$TEST_LIB/common.sh"
+
+_require_binary example/hello
+
+FS_NAME=service_ll
+LAUNCH=mount_fuse
+
+. "$TEST_LIB/service-example.sh"
+
+fallback=test-fallback-$$
+service_setup "$fallback"
+
+# Bound but never listening, so connect() gets ECONNREFUSED
+python3 -c 'import socket, sys
+socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET).bind(sys.argv[1])' \
+ "$service_sock"
+
+mkdir "$TEST_TMP/bin"
+ln -s "$FUSE_EXAMPLE_DIR/hello" "$TEST_TMP/bin/$fallback"
+export PATH="$TEST_TMP/bin:$PATH"
+
+fuse_mount_helper "$fallback" >/dev/null
+_assert_listdir "$TEST_MNT" hello
+fuse_umount
diff --git a/test/cases/mount/service-null.sh b/test/cases/mount/service-null.sh
new file mode 100755
index 000000000000..b89107ae3a40
--- /dev/null
+++ b/test/cases/mount/service-null.sh
@@ -0,0 +1,33 @@
+#!/usr/bin/env bash
+# GROUP: mount
+#
+# The null example mounts through fuservicemount3 on a regular file.
+# null has no backing file and mounts on a regular file, so it does not use
+# lib/service-example.sh.
+
+_fuse_no_mount_needed=1
+. "$TEST_LIB/common.sh"
+
+_require_linux "fuservicemount3"
+_require_root
+_require_fuse_device
+_require_binary util/fuservicemount3
+_require_binary example/null
+
+. "$TEST_LIB/service.sh"
+
+service_setup "test-null-$$"
+mnt_file=$TEST_TMP/file
+_at_exit "umount -l '$mnt_file' 2>/dev/null"
+printf 'dummy' >"$mnt_file"
+
+service_start "$TEST_LOGDIR/fs-null.out" "$FUSE_EXAMPLE_DIR/null"
+# null takes no source, only the mount point
+"$FUSE_UTIL_DIR/fuservicemount3" "$mnt_file" -t "fuse.$service_subtype" ||
+ _fail "fuservicemount3 did not mount null"
+
+_check fuse_test_null_roundtrip "$mnt_file"
+
+umount "$mnt_file"
+service_wait_exit
+_assert_eq "$service_rc" 0 "null exit status"
--
2.53.0
^ permalink raw reply related [flat|nested] 26+ messages in thread
* [PATCH v2 11/14] test: run mkfs.ext4 through the service examples
2026-09-28 11:02 [PATCH v2 00/14] libfuse: Add mount service safety checks and tests Bernd Schubert via B4 Relay
` (9 preceding siblings ...)
2026-09-28 11:02 ` [PATCH v2 10/14] test: mount the service examples through fuservicemount3 Bernd Schubert via B4 Relay
@ 2026-09-28 11:02 ` Bernd Schubert via B4 Relay
2026-09-28 11:02 ` [PATCH v2 12/14] fuse_service: bound argc and arg len read from the args memfd Bernd Schubert via B4 Relay
` (2 subsequent siblings)
13 siblings, 0 replies; 26+ messages in thread
From: Bernd Schubert via B4 Relay @ 2026-09-28 11:02 UTC (permalink / raw)
To: fuse-devel; +Cc: Darrick J. Wong, neal, Bernd Schubert
From: Bernd Schubert <bernd@bsbernd.com>
The byte comparison in the service example tests does not show that a
real filesystem tool works on the file service_ll and service_hl serve.
mkfs.ext4 -d writes a filesystem through the mount and copies in files
of different sizes. After umount, e2fsck checks the image and debugfs
reads the files back for comparison.
Assisted-by: LLM
Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
Reviewed-by: "Darrick J. Wong" <djwong@kernel.org>
---
example/service_ll.c | 5 +++
test/cases/lib/service-example.sh | 68 ++++++++++++++++++++++++++++++++-----
test/cases/mount/service-hl-mkfs.sh | 8 +++++
test/cases/mount/service-ll-mkfs.sh | 8 +++++
4 files changed, 80 insertions(+), 9 deletions(-)
diff --git a/example/service_ll.c b/example/service_ll.c
index fd43c40fc15b..7baf47905a33 100644
--- a/example/service_ll.c
+++ b/example/service_ll.c
@@ -43,6 +43,11 @@
*
* mount -t fuse.service_ll /dev/sda /mnt
*
+ * /mnt/single_file then holds the bytes of /dev/sda, so a filesystem can be
+ * created on it:
+ *
+ * mkfs.ext4 /mnt/single_file
+ *
* ## Source code ##
* \include service_ll.c
* \include service_ll.socket
diff --git a/test/cases/lib/service-example.sh b/test/cases/lib/service-example.sh
index 878b379b8b16..c45718aeef20 100644
--- a/test/cases/lib/service-example.sh
+++ b/test/cases/lib/service-example.sh
@@ -1,15 +1,20 @@
# lib/service-example.sh - body for the service_ll / service_hl cases.
#
-# Caller sets FS_NAME and LAUNCH before sourcing:
+# Caller sets FS_NAME and LAUNCH, and optionally CHECK, before sourcing:
# FS_NAME service_ll | service_hl
# LAUNCH fuservicemount3 | mount_fuse
+# CHECK bytes (default) | mkfs
#
-# The file the example serves has to read back as the image, and what is
-# written through the mount has to reach the image.
+# bytes: the file the example serves has to read back as the image, and what
+# is written through the mount has to reach the image.
+# mkfs: mkfs.ext4 -d on that file has to leave an image e2fsck accepts. The
+# files it copied in have to read back unchanged from the image.
_fuse_no_mount_needed=1
. "$TEST_LIB/common.sh"
+CHECK=${CHECK:-bytes}
+
# Before the gates: a misspelled FS_NAME would otherwise skip as "not built"
case ${FS_NAME:-} in
service_ll | service_hl) ;;
@@ -19,6 +24,10 @@ case ${LAUNCH:-} in
fuservicemount3 | mount_fuse) ;;
*) _fail "unknown LAUNCH '${LAUNCH:-}'" ;;
esac
+case $CHECK in
+bytes | mkfs) ;;
+*) _fail "unknown CHECK '$CHECK'" ;;
+esac
_require_linux "fuservicemount3"
_require_root
@@ -26,6 +35,11 @@ _require_fuse_device
_require_binary util/fuservicemount3
_require_binary "example/$FS_NAME"
[ "$LAUNCH" != mount_fuse ] || _require_binary util/mount.fuse3
+if [ "$CHECK" = mkfs ]; then
+ _require_prog mkfs.ext4
+ _require_prog e2fsck
+ _require_prog debugfs
+fi
. "$TEST_LIB/service.sh"
@@ -33,11 +47,25 @@ service_setup "test-$FS_NAME-$$"
img=$TEST_SRC/img
old=$TEST_TMP/old
new=$TEST_TMP/new
+files=$TEST_TMP/files
+dump=$TEST_TMP/dump
# The size has to be a multiple of the page size
-head -c 1048576 /dev/urandom >"$old"
-head -c 1048576 /dev/urandom >"$new"
-cp "$old" "$img"
+case $CHECK in
+bytes)
+ head -c 1048576 /dev/urandom >"$old"
+ head -c 1048576 /dev/urandom >"$new"
+ cp "$old" "$img"
+ ;;
+mkfs)
+ truncate -s 64M "$img"
+ mkdir -p "$files/sub"
+ for size in 0 1 4095 4096 4097 65537 1048576; do
+ head -c "$size" /dev/urandom >"$files/f$size"
+ done
+ echo hello >"$files/sub/small"
+ ;;
+esac
service_start "$TEST_LOGDIR/fs-$FS_NAME.out" "$FUSE_EXAMPLE_DIR/$FS_NAME"
case $LAUNCH in
@@ -53,10 +81,32 @@ mount_fuse)
esac
_assert_fstype "$TEST_MNT" "fuse.$service_subtype" fuse
-_assert_file_eq "$TEST_MNT/single_file" "$old"
-dd if="$new" of="$TEST_MNT/single_file" bs=64k conv=notrunc,fsync status=none
+case $CHECK in
+bytes)
+ _assert_file_eq "$TEST_MNT/single_file" "$old"
+ dd if="$new" of="$TEST_MNT/single_file" bs=64k conv=notrunc,fsync \
+ status=none
+ ;;
+mkfs)
+ # -F: single_file is a regular file, not a block device
+ mkfs.ext4 -F -q -d "$files" "$TEST_MNT/single_file" ||
+ _fail "mkfs.ext4 through $FS_NAME failed"
+ ;;
+esac
umount "$TEST_MNT"
service_wait_exit
_assert_eq "$service_rc" 0 "$FS_NAME exit status"
-_assert_file_eq "$img" "$new"
+case $CHECK in
+bytes)
+ _assert_file_eq "$img" "$new"
+ ;;
+mkfs)
+ e2fsck -fn "$img" || _fail "e2fsck found errors in $img"
+ # debugfs exits 0 even when rdump fails, so diff is the check
+ mkdir "$dump"
+ debugfs -R "rdump / $dump" "$img"
+ diff -r -x lost+found "$files" "$dump" ||
+ _fail "files in $img differ from $files"
+ ;;
+esac
diff --git a/test/cases/mount/service-hl-mkfs.sh b/test/cases/mount/service-hl-mkfs.sh
new file mode 100755
index 000000000000..af552818cd90
--- /dev/null
+++ b/test/cases/mount/service-hl-mkfs.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+# GROUP: mount
+
+FS_NAME=service_hl
+LAUNCH=fuservicemount3
+CHECK=mkfs
+
+. "$TEST_LIB/service-example.sh"
diff --git a/test/cases/mount/service-ll-mkfs.sh b/test/cases/mount/service-ll-mkfs.sh
new file mode 100755
index 000000000000..9908937010de
--- /dev/null
+++ b/test/cases/mount/service-ll-mkfs.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+# GROUP: mount
+
+FS_NAME=service_ll
+LAUNCH=fuservicemount3
+CHECK=mkfs
+
+. "$TEST_LIB/service-example.sh"
--
2.53.0
^ permalink raw reply related [flat|nested] 26+ messages in thread
* [PATCH v2 12/14] fuse_service: bound argc and arg len read from the args memfd
2026-09-28 11:02 [PATCH v2 00/14] libfuse: Add mount service safety checks and tests Bernd Schubert via B4 Relay
` (10 preceding siblings ...)
2026-09-28 11:02 ` [PATCH v2 11/14] test: run mkfs.ext4 through the service examples Bernd Schubert via B4 Relay
@ 2026-09-28 11:02 ` Bernd Schubert via B4 Relay
2026-09-29 2:33 ` Darrick J. Wong
2026-09-28 11:02 ` [PATCH v2 13/14] build: move the default service socket directory to /run/fuse Bernd Schubert via B4 Relay
2026-09-28 11:02 ` [PATCH v2 14/14] Improve documentation for fuse service mount Bernd Schubert via B4 Relay
13 siblings, 1 reply; 26+ messages in thread
From: Bernd Schubert via B4 Relay @ 2026-09-28 11:02 UTC (permalink / raw)
To: fuse-devel; +Cc: Darrick J. Wong, neal, Bernd Schubert, Keerthana KT
From: Keerthana KT <keerthana@labs.digiscrypt.com>
fuse_service_append_args() takes the argument count and each argument
length straight from the args memfd, which this file already treats as
untrusted (see the SO_PASSRIGHTS guard against a malicious mount
helper). Both fields are uint32_t and feed allocation math with no
bound: calloc(memfd_args.argc + existing_args->argc, ...) wraps in
unsigned arithmetic and undersizes the argv array, while
calloc(1, memfd_arg.len + 1) wraps to a zero-size buffer when len is
UINT32_MAX, which the following pread() then overflows.
Nothing bounded the memfd itself, so cap it on both sides with a new
FUSE_SERVICE_MAX_ARGV_SIZE. The mount helper refuses to write a string
that would push the file past the cap, and the server fstat()s the file
and refuses to parse one larger than it. The file size then bounds the
rest: argc cannot exceed the number of iovecs that fit between the
header and the strings, and no string can be longer than the file
holding it. An argc of zero is rejected as well, because only the first
loop iteration assigns argv[0].
Signed-off-by: Keerthana KT <keerthana@labs.digiscrypt.com>
Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
---
include/fuse_service_priv.h | 10 ++++++++++
lib/fuse_service.c | 43 +++++++++++++++++++++++++++++++++++++++++++
util/mount_service.c | 10 ++++++++++
3 files changed, 63 insertions(+)
diff --git a/include/fuse_service_priv.h b/include/fuse_service_priv.h
index 988f7c9251c8..5b1edce4b7a8 100644
--- a/include/fuse_service_priv.h
+++ b/include/fuse_service_priv.h
@@ -23,6 +23,16 @@ struct fuse_service_memfd_argv {
#define FUSE_SERVICE_MAX_CMD_SIZE (65536)
+/*
+ * Upper bound on the whole argv memfd, as opposed to FUSE_SERVICE_MAX_CMD_SIZE
+ * which bounds one socket command. Both sides check it: the mount helper
+ * refuses to write past it, and the fuse server refuses to parse a file larger
+ * than it. Generous next to any real mount(8) invocation, but small enough
+ * that the counts and lengths the server reads out of the file cannot overflow
+ * the allocation math they feed.
+ */
+#define FUSE_SERVICE_MAX_ARGV_SIZE (1048576)
+
#define FUSE_SERVICE_ARGS_MAGIC 0x41524753 /* ARGS */
/* mount.service sends a hello to the server and it replies */
diff --git a/lib/fuse_service.c b/lib/fuse_service.c
index 0a05b3fbc1f2..3991f92f09cb 100644
--- a/lib/fuse_service.c
+++ b/lib/fuse_service.c
@@ -629,8 +629,10 @@ int fuse_service_append_args(struct fuse_service *sf,
struct fuse_args new_args = {
.allocated = 1,
};
+ struct stat statbuf;
char *str = NULL;
off_t memfd_pos = 0;
+ off_t max_argc;
ssize_t received;
unsigned int i;
int ret;
@@ -656,6 +658,34 @@ int fuse_service_append_args(struct fuse_service *sf,
memfd_args.argc = htonl(memfd_args.argc);
memfd_pos += sizeof(memfd_args);
+ ret = fstat(sf->argvfd, &statbuf);
+ if (ret) {
+ int error = errno;
+
+ fuse_log(FUSE_LOG_ERR, "fuse: service args file stat: %s\n",
+ strerror(error));
+ return -error;
+ }
+ if (statbuf.st_size > FUSE_SERVICE_MAX_ARGV_SIZE) {
+ fuse_log(FUSE_LOG_ERR, "fuse: service args file too large\n");
+ return -EBADMSG;
+ }
+
+ /*
+ * The array of argv iovecs sits between the header and the strings, so
+ * the file size bounds argc. Reject a count the file cannot hold: the
+ * sum below is computed in unsigned arithmetic and would otherwise wrap
+ * and undersize the array. argc 0 is rejected as well, because only
+ * the first loop iteration fills argv[0].
+ */
+ max_argc = (statbuf.st_size - (off_t)sizeof(memfd_args)) /
+ (off_t)sizeof(struct fuse_service_memfd_arg);
+ if (memfd_args.argc == 0 || memfd_args.argc > max_argc) {
+ fuse_log(FUSE_LOG_ERR, "fuse: service args file argc %u invalid\n",
+ memfd_args.argc);
+ return -EBADMSG;
+ }
+
/* Allocate a new array of argv string pointers */
new_args.argv = calloc(memfd_args.argc + existing_args->argc,
sizeof(char *));
@@ -722,6 +752,19 @@ int fuse_service_append_args(struct fuse_service *sf,
memfd_arg.len = htonl(memfd_arg.len);
memfd_pos += sizeof(memfd_arg);
+ /*
+ * A string cannot be longer than the file holding it. len
+ * UINT32_MAX would make len + 1 wrap to zero below, handing
+ * calloc() a zero-size buffer for the pread() to overflow.
+ */
+ if (memfd_arg.len >= statbuf.st_size) {
+ fuse_log(FUSE_LOG_ERR,
+ "fuse: service args file argv[%u] len %u too large\n",
+ i, memfd_arg.len);
+ ret = -EBADMSG;
+ goto out_new_args;
+ }
+
/* read arg string from file */
str = calloc(1, memfd_arg.len + 1);
if (!str) {
diff --git a/util/mount_service.c b/util/mount_service.c
index 84e9d831ce03..c715729b2162 100644
--- a/util/mount_service.c
+++ b/util/mount_service.c
@@ -442,6 +442,16 @@ static int mount_service_capture_arg(const struct mount_service *mo,
};
ssize_t written;
+ /*
+ * string_pos already covers the header and the whole array, so this
+ * bounds the entire memfd. The server rejects anything larger.
+ */
+ if (*string_pos + (off_t)string_len > FUSE_SERVICE_MAX_ARGV_SIZE) {
+ fprintf(stderr, "%s: memfd argv[%u] exceeds %d byte limit\n",
+ mo->msgtag, args->argc, FUSE_SERVICE_MAX_ARGV_SIZE);
+ return -1;
+ }
+
written = pwrite(mo->argvfd, string, string_len, *string_pos);
if (written < 0) {
fprintf(stderr, "%s: memfd argv write: %s\n",
--
2.53.0
^ permalink raw reply related [flat|nested] 26+ messages in thread
* [PATCH v2 13/14] build: move the default service socket directory to /run/fuse
2026-09-28 11:02 [PATCH v2 00/14] libfuse: Add mount service safety checks and tests Bernd Schubert via B4 Relay
` (11 preceding siblings ...)
2026-09-28 11:02 ` [PATCH v2 12/14] fuse_service: bound argc and arg len read from the args memfd Bernd Schubert via B4 Relay
@ 2026-09-28 11:02 ` Bernd Schubert via B4 Relay
2026-09-29 2:34 ` Darrick J. Wong
2026-09-28 11:02 ` [PATCH v2 14/14] Improve documentation for fuse service mount Bernd Schubert via B4 Relay
13 siblings, 1 reply; 26+ messages in thread
From: Bernd Schubert via B4 Relay @ 2026-09-28 11:02 UTC (permalink / raw)
To: fuse-devel; +Cc: Darrick J. Wong, neal, Bernd Schubert
From: Bernd Schubert <bernd@bsbernd.com>
/run/fuse sounds better for fuse and then it is also shorter
a few bytes and might reduce 108-byte AF_UNIX path issues, compared
to /run/filesystems.
Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
---
meson.build | 2 +-
meson_options.txt | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/meson.build b/meson.build
index 06e6056ecf21..5e9628b521e7 100644
--- a/meson.build
+++ b/meson.build
@@ -72,7 +72,7 @@ private_cfg.set_quoted('PACKAGE_VERSION', meson.project_version())
service_socket_dir = get_option('service-socket-dir')
service_socket_perms = get_option('service-socket-perms')
if service_socket_dir == ''
- service_socket_dir = '/run/filesystems'
+ service_socket_dir = '/run/fuse'
endif
if service_socket_perms == ''
service_socket_perms = '0220'
diff --git a/meson_options.txt b/meson_options.txt
index 43104290f8b5..e12177fcea30 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -35,7 +35,7 @@ option('sync-init', type: 'combo', choices: ['auto', 'always', 'never'], value:
description: 'Synchronous FUSE_INIT: auto follows fuse_daemonize_early_start(), always and never override it')
option('service-socket-dir', type : 'string', value : '',
- description: 'Where to install fuse server sockets (if empty, /run/filesystems)')
+ description: 'Where to install fuse server sockets (if empty, /run/fuse)')
option('service-socket-perms', type : 'string', value : '',
description: 'Default fuse server socket permissions (if empty, 0220)')
--
2.53.0
^ permalink raw reply related [flat|nested] 26+ messages in thread
* [PATCH v2 14/14] Improve documentation for fuse service mount
2026-09-28 11:02 [PATCH v2 00/14] libfuse: Add mount service safety checks and tests Bernd Schubert via B4 Relay
` (12 preceding siblings ...)
2026-09-28 11:02 ` [PATCH v2 13/14] build: move the default service socket directory to /run/fuse Bernd Schubert via B4 Relay
@ 2026-09-28 11:02 ` Bernd Schubert via B4 Relay
2026-09-29 2:43 ` Darrick J. Wong
13 siblings, 1 reply; 26+ messages in thread
From: Bernd Schubert via B4 Relay @ 2026-09-28 11:02 UTC (permalink / raw)
To: fuse-devel; +Cc: Darrick J. Wong, neal, Bernd Schubert
From: Bernd Schubert <bernd@bsbernd.com>
Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
---
doc/README.service-mount | 312 +++++++++++++++++++++++++++++
doc/README.service-mount-dev | 456 ++++++++++++++++++++++++++++++++++++++++++
doc/README.service-mount-flow | 201 +++++++++++++++++++
doc/fuservicemount3.8 | 150 +++++++++++++-
doc/mainpage.dox | 13 ++
doc/mount.fuse3.8 | 18 ++
6 files changed, 1144 insertions(+), 6 deletions(-)
diff --git a/doc/README.service-mount b/doc/README.service-mount
new file mode 100644
index 000000000000..859db863f3da
--- /dev/null
+++ b/doc/README.service-mount
@@ -0,0 +1,312 @@
+Mounting FUSE filesystems that run as a socket service
+======================================================
+
+This document is for administrators and end users who want to mount a FUSE
+filesystem whose server runs as a sandboxed systemd socket service, rather
+than as a process in the mount caller's own context.
+
+Developers who want to make their FUSE server runnable this way should read
+README.service-mount-dev instead.
+
+
+What a service mount is
+-----------------------
+
+A traditional FUSE filesystem runs as a child of whoever mounts it: it
+inherits that environment, needs mount permission, and can see the caller's
+files. A *service mount* instead keeps the FUSE server running as an
+independent systemd service. When someone mounts the filesystem, a small
+privileged helper (fuservicemount3) connects to the service over a UNIX
+socket, hands it the /dev/fuse device and any backing files it needs, and
+performs the mount on its behalf.
+
+The benefit is isolation. The server can run:
+
+ - as a separate, unprivileged uid/gid (systemd DynamicUser),
+ - with no capabilities at all,
+ - in private mount, network, and pid namespaces,
+ - with a restricted system-call filter,
+
+while still being mountable by an ordinary user. The server never gains mount
+permission and never runs in the caller's environment; the privileged work is
+confined to the fuservicemount3 helper. See example/service_ll@.service for a
+fully locked-down unit.
+
+
+Do I need this?
+---------------
+
+This feature exists for one specific goal: running a FUSE server with strong
+privilege separation, where the server itself is fully unprivileged and
+sandboxed while a separate setuid helper performs the mount. Getting that
+requires the server to be written to the fuse_service_* API (see
+README.service-mount-dev). An existing FUSE program that simply calls
+fuse_main() cannot be mounted this way unmodified: it opens /dev/fuse and
+performs the mount itself, which the sandbox does not allow.
+
+If all you want is to manage an ordinary FUSE filesystem with systemd --
+start/stop, journald logging, cgroup resource limits --
+you do NOT need this feature. Run the filesystem under a plain systemd service
+unit instead, launching it in the foreground so systemd can track it:
+
+ # myfs.service
+ [Service]
+ ExecStart=/usr/bin/myfs ... -f <mountpoint>
+
+systemd-run(1) starts the same thing as a transient unit, without a unit
+file. It passes the command line on as typed, so the filesystem can get any
+number of arguments. A unit file fixes them in its ExecStart= line, and a
+template unit (myfs@.service) takes only one parameter, the instance name.
+Set unit directives with -p:
+
+ sudo systemd-run -p MemoryMax=1G myfs <args> -f <mountpoint>
+
+This also works in the user's own service manager, as long as fusermount3 is
+installed setuid root:
+
+ systemd-run --user -p MemoryMax=1G myfs <args> -f <mountpoint>
+
+That filesystem still mounts the traditional way (through fusermount3) and
+runs in the service's own context; it is not isolated from the mount the way a
+service mount is.
+
+Use a service mount when you specifically want:
+
+ - the filesystem server to run as a separate, unprivileged uid with no
+ mount permission of its own,
+ - it confined to private mount/network/pid namespaces with no capabilities,
+ - the privileged mount work isolated in the fuservicemount3 helper,
+ - on-demand, socket-activated startup.
+
+In short: a plain systemd unit gives you lifecycle management; a service mount
+gives you lifecycle management AND isolation, at the cost of the fuse server
+author adapting the server to the service API.
+
+
+Requirements
+------------
+
+Service mount support is only built when libfuse is configured with systemd
+support:
+
+ - the systemd development headers (libsystemd-dev), and
+ - a known systemd system unit directory.
+
+When both are present, meson defines HAVE_SERVICEMOUNT and builds the
+fuservicemount3 helper. If either is missing, meson prints a warning and the
+feature is left out; mounts then fall back to the traditional path (see
+"Dispatch and fallback" below).
+
+Relevant meson options:
+
+ - service-socket-dir directory that holds the per-filesystem service
+ sockets (default: /run/fuse)
+ - service-socket-perms mode for the socket files (default: 0220)
+ - systemd-system-unit-dir
+ where to install service/socket units (default:
+ taken from the systemd pkg-config file)
+
+fuservicemount3 is installed setuid root, just like fusermount3, so that
+unprivileged users can trigger a mount handled by the service. The setuid
+privilege is what lets it perform the mount; it drops back to the real user
+before connecting to the service socket, so the socket's own permissions are
+what decide who may mount. The default 0220 mode is only a starting point --
+set SocketUser=, SocketGroup= and SocketMode= in the .socket unit to grant the
+intended users access (see "Who establishes the connection" below).
+
+
+Installing a service
+--------------------
+
+Each mountable filesystem type is backed by two systemd units, named after the
+filesystem subtype (the part after "fuse." in the mount type). For a subtype
+"myfs":
+
+ - myfs@.service the sandboxed server (a template, one instance per
+ connection)
+ - myfs.socket the listening socket that activates it
+
+The socket listens on a SOCK_SEQPACKET UNIX socket at
+
+ <service-socket-dir>/<subtype> e.g. /run/fuse/myfs
+
+and is configured with "Accept=yes", so systemd spawns a fresh, isolated
+server instance for every mount request.
+
+The path field (sun_path) of a UNIX socket address holds 108 bytes on Linux,
+so <service-socket-dir>/<subtype> can be at most 107 characters long. The
+default /run/fuse leaves 97 characters for the subtype. If the path is
+longer, mount.fuse3 mounts the traditional way and fuservicemount3 fails with
+"filesystem type name `<subtype>' is too long".
+
+Install the units into the systemd unit directory (usually
+/run/systemd/system or /etc/systemd/system), then:
+
+ systemctl daemon-reload
+ systemctl start myfs.socket
+
+The socket unit can be enabled to start at boot:
+
+ systemctl enable myfs.socket
+
+The example filesystems ship ready-to-adapt units; see
+example/service_ll@.service and example/service_ll.socket(.in).
+
+
+Mounting
+--------
+
+Mount the filesystem with the usual mount(8) syntax, using the type
+"fuse.<subtype>":
+
+ mount -t fuse.myfs <source> <mountpoint> [-o options]
+
+For example:
+
+ mount -t fuse.service_ll /dev/sda /mnt
+
+A block-device-backed filesystem uses "fuseblk.<subtype>" instead.
+
+The same line works from /etc/fstab:
+
+ <source> <mountpoint> fuse.myfs <options> 0 0
+
+The mount is handled by the mount.fuse3 helper, which notices that a service
+socket exists for the type and hands the request to fuservicemount3. Every
+argument except "-t <type>" -- the <source>, the <mountpoint> and the -o
+options -- is forwarded to the running server for parsing.
+
+
+Checking whether a service is available
+---------------------------------------
+
+To test whether a service socket exists for a given filesystem type without
+mounting anything:
+
+ fuservicemount3 -t fuse.myfs --check
+
+It exits 0 if the service socket exists and you may connect to it (write
+permission), non-zero otherwise. systemd starts a server only when
+fuservicemount3 connects, so an existing socket means a mount will get one.
+This relies on RemoveOnStop=yes in the .socket unit; without it, a stopped
+unit leaves a stale socket file behind.
+
+
+Unmounting
+----------
+
+Unmount as you would any FUSE filesystem:
+
+ fusermount3 -u <mountpoint>
+
+or, as a privileged user:
+
+ umount <mountpoint>
+
+
+Dispatch and fallback
+---------------------
+
+When you run "mount -t fuse.myfs ...", the mount.fuse3 helper first checks for
+a service socket for "myfs". The behaviour is:
+
+ - If a socket exists (and no options that are incompatible with service
+ mounts were given), the mount is performed through fuservicemount3 and the
+ running service.
+
+ - If no socket exists or options that are incompatible with service mounts
+ were given, mount.fuse3 transparently falls back to the traditional path:
+ it runs the filesystem server program directly, exactly as it did before
+ service mount support.
+
+So enabling service mount support does not break filesystems that are not set
+up as services; they continue to mount the old way.
+
+A few options force the traditional path and skip the service even when a
+socket is present, because they are meaningless to an already-running,
+isolated server (for example passing a pre-opened FUSE fd, or the
+mount.fuse3 "setuid=USER" option).
+
+
+Security model
+--------------
+
+ - The FUSE server runs under the confinement defined by its .service unit,
+ not under the mount caller's identity, privileges, or namespaces.
+
+ - The server has no access to the caller's filesystem. Anything it needs
+ (the backing device or file, /dev/fuse) is opened by the privileged
+ fuservicemount3 helper and passed to the server over the socket. The
+ server can refuse to accept further passed file descriptors once it has
+ what it needs.
+
+ - fuservicemount3 opens a path for the server only if the mount command
+ line names it: as an argument, as the value in a name=value option, or
+ glued to a short option as in "-J/dev/sdb1". The administrator can allow
+ more paths per filesystem subtype in /etc/fuse.conf:
+
+ service_open_path = ext4 /dev/sd*
+
+ It refuses any other path with EPERM.
+
+ - The only setuid-root component is fuservicemount3, which performs just the
+ mount and the file-descriptor hand-off.
+
+This is the same trust boundary as fusermount3, but with the filesystem
+implementation itself kept out of the privileged and caller-facing paths.
+
+
+Who establishes the connection
+------------------------------
+
+Three parties touch the service socket, but only one dials it:
+
+ - systemd owns and listens on the socket. Starting the .socket unit creates
+ the listening socket at <service-socket-dir>/<subtype>; no server is
+ running yet.
+
+ - fuservicemount3 (the helper) is the socket client. When you mount, it
+ connects to that socket -- after dropping back to your real, unprivileged
+ user id, so the kernel checks the socket's permissions against you, not
+ against root. This is the access-control gate: only users the socket
+ grants connect (write) permission to can mount.
+
+ - systemd accepts the connection and, because the .socket unit uses
+ Accept=yes, starts a fresh per-connection server instance and hands it the
+ already-connected socket. The server never connects or accepts; it
+ inherits the live connection.
+
+So to control who may mount a given filesystem, set SocketUser=, SocketGroup=
+and SocketMode= for its .socket unit, for example with
+"systemctl edit myfs.socket". A chmod or chown on the socket file itself is
+lost when the socket unit restarts, because systemd creates the file anew.
+
+
+Troubleshooting
+---------------
+
+ - "mounts the old way / service is ignored": confirm the socket exists with
+ "fuservicemount3 -t fuse.<subtype> --check", that <service-socket-dir>
+ matches how libfuse was built, and that the .socket unit is started.
+
+ - "fuservicemount3: not found" or permission errors: verify the helper is
+ installed in sbindir and is setuid root.
+
+ - "<path>: file must be in command line arguments or in /etc/fuse.conf":
+ the server asked for a path the mount command line does not name. Add a
+ service_open_path line for it (see "Security model").
+
+ - server-side errors: because the server logs to its own journal, inspect it
+ with "journalctl -u myfs@*" (the example units log to the kernel ring
+ buffer via /dev/ttyprintk, viewable with dmesg).
+
+
+See also
+--------
+
+ README.service-mount-dev writing a FUSE server that runs as a service
+ fuservicemount3(8)
+ mount.fuse3(8)
+ fusermount3(1)
+ mount(8)
+ systemd.socket(5)
diff --git a/doc/README.service-mount-dev b/doc/README.service-mount-dev
new file mode 100644
index 000000000000..c39bcb5e4d51
--- /dev/null
+++ b/doc/README.service-mount-dev
@@ -0,0 +1,456 @@
+Writing a FUSE server that runs as a socket service
+===================================================
+
+This document is for developers who want their FUSE server to be mountable as
+a sandboxed systemd socket service, using the fuse_service_* API declared in
+fuse_service.h. Administrators and users who only want to mount such a
+filesystem should read README.service-mount instead.
+
+The complete working examples referenced throughout are:
+
+ example/service_ll.c low-level API server
+ example/service_hl.c high-level API server
+ example/single_file.c backing-store helper shared by both
+ example/service_ll@.service, example/service_ll.socket.in systemd units
+
+
+The execution model
+--------------------
+
+A service-mount server does not mount anything itself and does not run in the
+mounting user's context. Instead:
+
+ 1. systemd listens on a per-subtype UNIX socket (Accept=yes) and starts one
+ confined instance of your server per incoming mount request.
+
+ 2. The privileged fuservicemount3 helper connects to that socket, opens
+ /dev/fuse, and passes the device fd plus your command-line arguments to
+ the server.
+
+ 3. Your server cannot open files itself (its sandbox has no access to the
+ caller's filesystem or to /dev), so it asks the helper to open any
+ backing files or block devices on its behalf and pass the descriptors
+ back.
+
+ 4. Your server binds the FUSE session to the passed /dev/fuse fd and asks
+ the helper to perform the mount. Requests start flowing immediately.
+
+Everything the server needs from the outside world therefore arrives over the
+socket; the server never needs mount permission and never touches the
+caller's environment.
+
+
+The mount protocol
+------------------
+
+S and H denote the two parties:
+
+ S = the FUSE server -- your binary, one <subtype>@.service instance
+ H = fuservicemount3 -- the setuid-root mount helper; the socket client
+
+Transport:
+
+ - one AF_UNIX SOCK_SEQPACKET socket, created by systemd at
+ <service-socket-dir>/<subtype> (e.g. /run/fuse/myfs)
+ - H connect()s as the real user (that uid gates who may mount); systemd
+ accept()s (Accept=yes) and hands the connected fd to a fresh S, which
+ adopts it in fuse_service_accept()
+ - one message per datagram (sendmsg with MSG_EOR); a passed fd travels as
+ SCM_RIGHTS ancillary data, exactly one fd per message
+ - every multi-byte field is in network byte order; no message exceeds
+ FUSE_SERVICE_MAX_CMD_SIZE (65536 bytes)
+ - after "DOIT" FUSE traffic uses /dev/fuse, not this socket; H keeps
+ serving commands until S sends "BYEE" or closes the socket
+
+Every message begins with a 4-byte magic that spells the quoted tag in ASCII
+(e.g. "OPEN" is 0x4f50454e), so a message is legible in a hex dump. Most tags
+are operation mnemonics (OPEN, BDEV, TYPE, NAME, MNTP, DOIT, BYEE, ...); the
+handshake pair is not: "SAFT" (the HELLO command) and "LAST" (its reply) are
+named after the film "Safety Last!". Structures, verbatim from
+fuse_service_priv.h:
+
+ struct fuse_service_packet { uint32_t magic; };
+
+ struct fuse_service_hello { /* "SAFT" */
+ struct fuse_service_packet p;
+ uint16_t min_version, max_version; /* both 1 */
+ uint32_t flags; /* ALLOW_OTHER 1<<0 | FUSEBLK 1<<1; what H allows */
+ };
+ struct fuse_service_hello_reply { /* "LAST" */
+ struct fuse_service_packet p;
+ uint16_t version, padding; /* version 1 */
+ };
+ struct fuse_service_simple_reply { /* "REPL" */
+ struct fuse_service_packet p;
+ uint32_t error; /* 0, else positive errno */
+ };
+ struct fuse_service_requested_file { /* "FILE", carries one fd */
+ struct fuse_service_packet p;
+ uint32_t error; /* 0, else positive errno and no fd */
+ char path[]; /* echoes the request path; NUL-terminated */
+ };
+ struct fuse_service_open_command { /* "OPEN" file / "BDEV" device */
+ struct fuse_service_packet p;
+ uint32_t open_flags; /* O_* */
+ uint32_t create_mode;
+ uint32_t request_flags; /* QUIET 1<<0 */
+ uint32_t block_size; /* "BDEV" only */
+ char path[];
+ };
+ struct fuse_service_fsopen_command { /* "TYPE" */
+ struct fuse_service_packet p;
+ uint32_t fsopen_flags; /* FUSEBLK 1<<0, set iff fstype is fuseblk */
+ };
+ struct fuse_service_string_command { /* "NAME" / "OPTS" / "MTAB" */
+ struct fuse_service_packet p;
+ char value[];
+ };
+ struct fuse_service_mountpoint_command { /* "MNTP" */
+ struct fuse_service_packet p;
+ uint16_t expected_fmt, padding; /* S_IFDIR / S_IFREG, or 0 */
+ char value[]; /* the mountpoint */
+ };
+ struct fuse_service_mount_command { /* "DOIT" */
+ struct fuse_service_packet p;
+ uint32_t ms_flags; /* MS_* */
+ };
+ struct fuse_service_bye_command { /* "BYEE" */
+ struct fuse_service_packet p;
+ uint32_t exitcode;
+ };
+
+The "argv" descriptor is a memfd; its bytes are one header, then argc entries,
+then the packed argument strings:
+
+ struct fuse_service_memfd_argv { uint32_t magic /* "ARGS" */, argc; };
+ struct fuse_service_memfd_arg { uint32_t pos, len; }; /* x argc */
+
+The whole memfd is at most FUSE_SERVICE_MAX_ARGV_SIZE (1 MiB) and argc is at
+least 1; the server refuses a file that breaks either rule.
+
+Message sequence. "A -> B msg" = A sends msg to B. A bracketed [call] names
+the fuse_service_* function that drives the step; "local:" steps send nothing.
+
+ handshake [fuse_service_accept]
+ H -> S "SAFT" fuse_service_hello
+ S -> H "LAST" fuse_service_hello_reply
+
+ fd handover, both pushed by H unsolicited [fuse_service_accept]
+ H -> S "FILE" fuse_service_requested_file +fd path "argv"
+ H -> S "FILE" fuse_service_requested_file +fd path "fusedev"
+ local: S reads argv out of the memfd [fuse_service_append_args]
+
+ backing store, repeated per file, may be none
+ S -> H "OPEN" / "BDEV" fuse_service_open_command
+ [fuse_service_request_file / fuse_service_request_blockdev]
+ H -> S "FILE" fuse_service_requested_file +fd (or error and no fd)
+ [fuse_service_receive_file]
+ local: setsockopt(SO_PASSRIGHTS, 0) [fuse_service_finish_file_requests]
+
+ mount [fuse_service_session_mount]. S sends each command below; H answers
+ every one with H -> S "REPL" fuse_service_simple_reply, whose
+ nonzero errno aborts the mount.
+ local: bind se to /dev/fd/<fusedev> (fuse_session_mount)
+ S -> H "TYPE" fuse_service_fsopen_command
+ S -> H "NAME" fuse_service_string_command (mtab source)
+ S -> H "MNTP" fuse_service_mountpoint_command
+ S -> H "OPTS" fuse_service_string_command (optional)
+ S -> H "MTAB" fuse_service_string_command (optional)
+ S -> H "DOIT" fuse_service_mount_command (H mounts here)
+
+ shutdown [fuse_service_send_goodbye]
+ S -> H "BYEE" fuse_service_bye_command no reply; S closes socket
+
+README.service-mount-flow follows this sequence through the code of both
+sides, with the checks each side makes.
+
+
+The two entry points
+--------------------
+
+There are two ways to write the server, mirroring the normal libfuse APIs:
+
+ - High-level API: do the service setup, then call fuse_service_main(), the
+ service-aware counterpart of fuse_main(). See example/service_hl.c.
+
+ - Low-level API: do the service setup, create the session yourself, call
+ fuse_service_session_mount(), and run your own event loop. See
+ example/service_ll.c.
+
+Both share the same startup, resource-request, and shutdown sequence.
+
+IMPORTANT: define FUSE_USE_VERSION to at least FUSE_MAKE_VERSION(3, 19) and
+include <fuse_service.h>. Do NOT call fuse_daemonize(): a service must stay in
+the foreground so systemd can track it (fuse_service_session_mount and
+fuse_service_main arrange this for you). Service mounts do not support
+synchronous FUSE_INIT yet: FUSE_INIT reaches the server only after
+fuservicemount3 has performed the mount. Do not call
+fuse_daemonize_early_start() either.
+
+
+API reference
+-------------
+
+The full per-call documentation lives in fuse_service.h (and fuse.h for the
+high-level fuse_service_main). Unless noted, each int-returning call returns 0
+on success or a negative errno. In call order:
+
+ /* startup */
+ int fuse_service_accept(struct fuse_service **sfp);
+ bool fuse_service_accepted(const struct fuse_service *sf);
+ int fuse_service_append_args(struct fuse_service *sf,
+ struct fuse_args *args);
+ int fuse_service_parse_cmdline_opts(struct fuse_args *args,
+ struct fuse_cmdline_opts *opts); /* returns 0 / -1 */
+
+ /* capability negotiation */
+ bool fuse_service_can_allow_other(const struct fuse_service *sf);
+ bool fuse_service_can_fuseblk(const struct fuse_service *sf);
+
+ /* backing files: request, receive each fd, then stop fd passing */
+ int fuse_service_request_file(const struct fuse_service *sf,
+ const char *path, int open_flags, mode_t create_mode,
+ unsigned int request_flags);
+ int fuse_service_request_blockdev(const struct fuse_service *sf,
+ const char *path, int open_flags, mode_t create_mode,
+ unsigned int request_flags, unsigned int block_size);
+ int fuse_service_receive_file(const struct fuse_service *sf,
+ const char *path, int *fdp);
+ int fuse_service_finish_file_requests(const struct fuse_service *sf);
+
+ /* mount */
+ void fuse_service_expect_mount_format(struct fuse_service *sf,
+ mode_t expected_fmt);
+ int fuse_service_session_mount(struct fuse_service *sf,
+ struct fuse_session *se, mode_t expected_fmt,
+ struct fuse_cmdline_opts *opts);
+ int fuse_service_main(struct fuse_service *sf, struct fuse_args *args,
+ const struct fuse_operations *op, void *user_data);
+
+ /* shutdown */
+ int fuse_service_send_goodbye(struct fuse_service *sf, int exitcode);
+ void fuse_service_release(struct fuse_service *sf);
+ void fuse_service_destroy(struct fuse_service **sfp);
+ int fuse_service_exit(int ret);
+
+ #define FUSE_SERVICE_REQUEST_FILE_QUIET (1U << 0)
+
+fuse_service_receive_file sets *fdp to a valid fd (>= 0) or a negated errno
+from the helper's open attempt; the call itself returns nonzero only on a
+socket-level failure. fuse_service_accept always initialises *sfp; test
+fuse_service_accepted (true iff *sfp != NULL) to learn whether the program was
+actually launched as a service.
+
+
+Startup sequence
+----------------
+
+The first thing main() does is accept the service context:
+
+ struct fuse_service *service;
+
+ if (fuse_service_accept(&service))
+ goto error; /* socket/handshake failure */
+
+ if (!fuse_service_accepted(service))
+ goto error; /* not started as a service */
+
+fuse_service_accept() looks for the socket handed to the process by systemd,
+performs the protocol handshake, and receives the argument vector and the
+/dev/fuse fd. It always initialises *service; use fuse_service_accepted() to
+find out whether the program is actually running as a service (it returns
+false, with *service == NULL, when there is no service socket).
+
+The example servers require a service and exit otherwise (their error paths
+run only once the context is valid). A server that also wants to support
+traditional invocation can branch on fuse_service_accepted() and fall back to
+fuse_main() / fuse_session_mount(); in that case do not call the other
+fuse_service_* functions, which assume a valid service context.
+
+Next, fold the service-supplied arguments into the fuse_args built from the
+argc and argv of main(), and parse them:
+
+ struct fuse_args args = FUSE_ARGS_INIT(argc, argv);
+
+ if (fuse_service_append_args(service, &args)) /* add helper's args */
+ goto error;
+
+ if (fuse_opt_parse(&args, &priv, my_opts, my_opt_proc)) /* your opts */
+ goto error;
+
+For the low-level API also extract the common command-line options:
+
+ struct fuse_cmdline_opts opts = { };
+
+ if (fuse_service_parse_cmdline_opts(&args, &opts))
+ goto error;
+
+fuse_service_parse_cmdline_opts() is the service-mount analogue of
+fuse_parse_cmdline(). It does NOT validate the mountpoint; that is the
+helper's job. As usual, a missing -o subtype=/fsname= defaults the subtype to
+the program's basename.
+
+
+Requesting backing files and block devices
+-------------------------------------------
+
+Because the sandbox cannot open files, the server asks the helper to open them
+and send back the descriptor. This is a two-step request/receive pattern (see
+single_file_service_open() in example/single_file.c):
+
+ /* ask the helper to open it */
+ fuse_service_request_file(service, path, open_flags, create_mode, flags);
+ /* or, for a block device: */
+ fuse_service_request_blockdev(service, path, open_flags, create_mode,
+ flags, block_size);
+
+ /* then collect the descriptor */
+ int fd;
+ fuse_service_receive_file(service, path, &fd);
+
+A block_size of 0 leaves the block size of the device unchanged.
+
+On success fd is a valid descriptor. A negative fd is a (negated) errno from
+the helper's open attempt — single_file.c uses this to downgrade an O_RDWR
+request to O_RDONLY when the backing store is read-only. Pass
+FUSE_SERVICE_REQUEST_FILE_QUIET in the request flags to suppress the helper's
+error message when a failure is expected.
+
+The helper opens only a path that the mount command line names or that a
+service_open_path line in /etc/fuse.conf lists, and compares the strings
+exactly; any other path gets fd == -EPERM. Request the path as the user wrote
+it, not a canonicalized or rebuilt form. A relative path resolves against the
+directory mount was run in.
+
+Once you have every descriptor you need, close the door on further fd passing:
+
+ fuse_service_finish_file_requests(service);
+
+This tells the kernel to reject any additional descriptors on the socket
+(via SO_PASSRIGHTS where available), so a compromised or malicious helper
+cannot smuggle in more fds afterwards.
+
+
+Capability negotiation
+----------------------
+
+During the handshake the helper advertises what it is willing to do. Query it
+before relying on those behaviours:
+
+ fuse_service_can_allow_other(service) /* may honour -o allow_other */
+ fuse_service_can_fuseblk(service) /* may mount a fuseblk filesystem */
+
+
+Mounting
+--------
+
+fuservicemount3 mounts on a directory or on a regular file, and the kernel
+gives the filesystem root the type of the mountpoint. Every access to the
+root fails with EIO when the root your server reports has a different type.
+To make fuservicemount3 refuse such a mountpoint before it mounts, pass the
+type of your root (S_IFDIR or S_IFREG):
+
+ fuse_service_expect_mount_format(service, S_IFDIR);
+
+This call is optional. Without it, fuservicemount3 does not check the root
+type. A low-level server can instead pass the type as the third argument of
+fuse_service_session_mount().
+
+High-level API — hand off to fuse_service_main(), which builds the operations,
+performs the mount, and runs the loop:
+
+ ret = fuse_service_main(service, &args, &my_oper, NULL);
+
+Low-level API — create the session, install signal handlers, then mount:
+
+ se = fuse_session_new(&args, &my_ll_oper, sizeof(my_ll_oper), NULL);
+ ...
+ fuse_set_signal_handlers(se);
+
+ if (fuse_service_session_mount(service, se, S_IFDIR, &opts))
+ goto error;
+
+ fuse_session_loop(se); /* or fuse_session_loop_mt(se, config) */
+
+fuse_service_session_mount() binds the session to the passed /dev/fuse fd and
+asks the helper to mount the filesystem. It forces foreground operation and
+chdir("/") so you do not need (and must not) call fuse_daemonize(). After it
+returns successfully the kernel is already routing requests to your server, so
+enter your event loop promptly.
+
+
+Shutdown
+--------
+
+Tell the helper you are leaving, releasing and destroying the service context:
+
+ fuse_service_send_goodbye(service, exitcode); /* report exit status */
+ fuse_service_release(service); /* free socket-side state */
+ ...
+ fuse_service_destroy(&service); /* free the context */
+
+ return fuse_service_exit(ret); /* map ret to an exit code */
+
+In the examples, send_goodbye is sent once mounting has succeeded and the loop
+is about to start, and again on the error paths; fuse_service_exit() at the end
+of main() converts the server's return value into the exit status systemd
+expects. fuse_service_destroy() takes a pointer to the pointer and clears it.
+
+
+The systemd units
+-----------------
+
+Ship two units per filesystem, named after the subtype (the part after
+"fuse." in the mount type). For subtype "myfs":
+
+ myfs.socket — the listening socket. It must use SOCK_SEQPACKET, accept
+ each connection, and listen at the configured service
+ socket directory under the subtype name. The example
+ socket file is processed by meson, which substitutes the
+ build-time values:
+
+ [Socket]
+ ListenSequentialPacket=@FUSE_SERVICE_SOCKET_DIR_RAW@/myfs
+ Accept=yes
+ SocketMode=@FUSE_SERVICE_SOCKET_PERMS@
+ RemoveOnStop=yes
+
+ [Install]
+ WantedBy=sockets.target
+
+ myfs@.service — the server template, one instance per connection. Set
+ ExecStart to your binary and lock the unit down as tightly
+ as the filesystem allows. example/service_ll@.service is a
+ good starting point: DynamicUser, no capabilities, private
+ mount/network/pid namespaces, a @system-service syscall
+ filter, and OOMPolicy=continue so the filesystem is not
+ torn down under memory pressure.
+
+The socket name must match the subtype your server reports (via the program
+basename or -o subtype=), because that is the name fuservicemount3 looks for
+under the service socket directory.
+
+
+Building and installing
+-----------------------
+
+Build a server the usual way, linking against fuse3 and including the new
+header:
+
+ gcc -Wall single_file.c service_ll.c \
+ `pkg-config fuse3 --cflags --libs` -o service_ll
+
+Install the binary, point ExecStart at it, install the .service and .socket
+units into the systemd unit directory, then "systemctl daemon-reload" and
+"systemctl start myfs.socket". From there the filesystem is mounted exactly as
+described in README.service-mount.
+
+
+See also
+--------
+
+ fuse_service.h the full fuse_service_* API reference (Doxygen)
+ README.service-mount installing and mounting a service filesystem
+ README.service-mount-flow the mount sequence through the code
+ example/service_ll.c, example/service_hl.c, example/single_file.c
+ systemd.service(5), systemd.socket(5), systemd.exec(5)
diff --git a/doc/README.service-mount-flow b/doc/README.service-mount-flow
new file mode 100644
index 000000000000..bd5c4dee8b10
--- /dev/null
+++ b/doc/README.service-mount-flow
@@ -0,0 +1,201 @@
+How a service mount is set up
+=============================
+
+How a fuse server, the libfuse service code, and the fuservicemount3 mount
+helper set up a mount together: who talks to whom, over which transport, and
+where each side checks what the other sent. README.service-mount-dev
+describes the protocol and the API; this file follows the code.
+
+
+Participants
+------------
+
+ - fuservicemount3 util/mount_service.c separate process, setuid
+ root, spawned by
+ mount.fuse3 or run by hand
+ - libfuse service code lib/fuse_service.c linked into the fuse server
+ - fuse server example/service_ll.c same process as the library
+
+The flow graph below puts the process boundary between its first two
+columns. lib/fuse_service.c is compiled into the fuse server, so placing
+fuservicemount3 between the other two would draw two boundaries where there
+is one.
+
+Transports:
+
+ - AF_UNIX SOCK_SEQPACKET between fuservicemount3 and lib/fuse_service.c.
+ fuservicemount3 calls connect(); the fuse server receives the already
+ connected socket as SD_LISTEN_FDS_START via systemd socket activation
+ (Accept=yes), so fuse_service_accept() never calls accept(2).
+ - SCM_RIGHTS on that socket for the argv memfd, /dev/fuse, and each file
+ the server asks for with "OPEN" or "BDEV".
+
+
+Overview
+--------
+
+fuservicemount3 runs as root and does everything that needs privilege. The
+fuse server runs sandboxed and only asks. "Entry points" and "Flow" below show
+the steps and the main checks.
+
+legend: ---> request, data or file descriptor; <--- reply or request back
+
+ user: mount -t fuse.<subtype> <image> <mountpoint>
+ |
+ v
+ mount.fuse3 -> fuservicemount3 fuse server
+ (setuid root, trusted) (systemd service, sandboxed)
+ ----------------------------------------------- -----------------------------
+ connect /run/fuse/<subtype> -----------> systemd starts the server
+ hello, argv memfd, /dev/fuse -----------> fuse_service_accept()
+ parse the arguments
+ path named on the command line <----------- OPEN <image>
+ or listed in fuse.conf?
+ yes: open as the user, send fd -----------> keep the fd as backing store
+ no: EPERM fuse_service_finish_file_requests()
+ mount point on the command line? <----------- MNTP <mountpoint>
+ open it as the user and pin it
+ mount the fuse filesystem <----------- DOIT
+ exit <----------- BYEE
+ | serve FUSE requests from the
+ v kernel until umount
+ mount(8) returns
+
+
+Entry points
+------------
+
+Two binaries link the same mount_service.c (util/meson.build) and both funnel
+into mount_service_main(). mount(8) only ever execs mount.fuse3;
+fuservicemount3 is reached by direct invocation or because mount.fuse3
+spawned it, which is also the only place a second process appears. The last
+step below, mount_service_connect(), is where the socket to the fuse server
+is created and connected.
+
+ mount(8) -t fuse.<subtype> <source> <mountpoint> [-o opts]
+ | execs /sbin/mount.fuse3 user, directly:
+ | fuservicemount3 <source> <mountpoint> -t fuse.<subtype>
+ v v
+ /sbin/mount.fuse3 /sbin/fuservicemount3
+ util/mount.fuse.c main() util/fuservicemount.c main()
+ | strips "fuse." / "fuseblk." | also spawned by mount.fuse3
+ | no setuid=, no drop_privileges | when not root, see below
+ | -> try_service_main() |
+ v | exactly "-t FSTYPE --check"?
+ try_service_main() | exit 0/1, mounts nothing
+ no socket, no write access, or |
+ name too long -> FALLBACK_NEEDED |
+ +- getuid() != 0 |
+ | spawn fuservicemount3 ---+ a SECOND process starts here;
+ | fails -> FALLBACK_NEEDED | the parent waitpid()s
+ | | and returns its exit status
+ +- getuid() == 0 |
+ mount_service_main() +-> mount_service_main()
+ | |
+ +----------------+----------------+
+ v
+ mount_service_main() util/mount_service.c
+ read fuse.conf as the user
+ mount_service_init()
+ subtype from the fstype, reject a '/' in it
+ open the working directory as the user
+ mount_service_connect()
+ connect to /run/fuse/<subtype> as the user
+ path longer than sun_path -> exit failure
+ send buffer too small, no socket, or no
+ listener -> MOUNT_SERVICE_FALLBACK_NEEDED
+ |
+ v
+ a systemd .socket unit with Accept=yes is listening on
+ /run/fuse/<subtype>; it accepts the connection and spawns the
+ fuse server, handing it the connected fd as SD_LISTEN_FDS_START.
+ example/ carries units for the examples (null, service_ll, service_hl:
+ *.socket.in configured into *.socket, plus *@.service), none of which
+ meson installs. The socket directory comes from the meson option
+ service-socket-dir, built into FUSE_SERVICE_SOCKET_DIR (fuse_config.h).
+ |
+ v
+ continues in the three-column graph below
+
+MOUNT_SERVICE_FALLBACK_NEEDED on the mount.fuse3 path makes main() in
+mount.fuse3 run the filesystem server program itself, as it did before
+service mounts: /bin/sh -c "<subtype> [<source>] <mountpoint> [-o <options>]".
+
+
+Flow
+----
+
+The horizontal rule across the middle is the phase boundary. Above it the
+server is blocked inside its single fuse_service_accept() call and
+fuservicemount3 does the pushing (hello, argv memfd, /dev/fuse). Below it
+that call has returned, the server drives every step, and each socket message
+travels the other way: the server sends a command, fuservicemount3 replies.
+
+The server sends its "OPEN" and "BDEV" requests before
+fuse_service_finish_file_requests(), and the mount commands from
+fuse_service_session_mount() after it.
+
+ fuservicemount3 (separate process) # lib/fuse_service.c | fuse server
+ util/mount_service.c # (linked into the server) | example/service_ll.c
+ setuid root, or mount.fuse3 as root # |
+==========================================+============================================+========================
+ # one process |
+ mount_service_connect() done # systemd started the fuse server |
+ (see "Entry points" above) # with the connected fd |
+ # |
+ # | main()
+ # fuse_service_accept() <------------------+-- fuse_service_accept()
+ # check the socket fd from systemd |
+ # |
+ mount_service_send_hello() --------------+-> negotiate_hello() |
+ "SAFT": versions and flags # bad magic, version or flags -> error |
+ "LAST" hello reply <------------------+-- reply with the chosen version |
+ # |
+ mount_service_capture_args() # |
+ copy argv into a memfd # |
+ memfd larger than # |
+ FUSE_SERVICE_MAX_ARGV_SIZE # |
+ -> exit failure # |
+ mount_service_send_required_files() # |
+ "FILE" + argv memfd -------------------+-> fuse_service_receive_file(ARGV) |
+ "FILE" + /dev/fuse --------------------+-> fuse_service_receive_file(FUSEDEV) |
+-- fuse_service_accept() returns ---------+-- traffic direction reverses --------------+-- server drives below --
+ main loop: while (running) # |
+ mount_service_receive_command() # fuse_service_append_args() <-------------+-- fuse_service_append_args()
+ command larger than # read the arguments from the memfd |
+ FUSE_SERVICE_MAX_CMD_SIZE # memfd larger than |
+ -> exit failure # FUSE_SERVICE_MAX_ARGV_SIZE, |
+ # argc 0 or more than the memfd holds, |
+ # argument longer than the memfd |
+ # -> -EBADMSG | fuse_opt_parse()
+ # fuse_service_parse_cmdline_opts() <------+-- fuse_service_parse_cmdline_opts()
+ # |
+ "OPEN" / "BDEV" <---------------------+-- fuse_service_request_file() <-----------+-- single_file_service_open()
+ not on the command line and not # or fuse_service_request_blockdev() |
+ listed in fuse.conf -> EPERM # |
+ open it as the user # |
+ "FILE" + fd, or -errno --------------+-> fuse_service_receive_file(path) |
+ # |
+ # fuse_service_finish_file_requests() <----+-- fuse_service_finish_file_requests()
+ # no more fds accepted | fuse_session_new()
+ # |
+ # fuse_service_session_mount() <-----------+-- fuse_service_session_mount()
+ "TYPE" <------------------------------+-- one command per mount parameter |
+ fuseblk, not root -> EPERM # |
+ "NAME" <------------------------------+-- |
+ "MNTP" <------------------------------+-- |
+ not on the command line -> EINVAL # |
+ open it as the user and pin it # |
+ "OPTS" <------------------------------+-- |
+ allow_other/allow_root, not root, # |
+ no user_allow_other -> EPERM # |
+ "MTAB" <------------------------------+-- |
+ "DOIT" <------------------------------+-- |
+ not root: limit mounts, reject # |
+ unsafe flags, check mount point # |
+ mount the fuse filesystem # |
+ "REPL" after each command -------------+-> error -> -errno to the caller |
+ # |
+ "BYEE" <------------------------------+-- fuse_service_send_goodbye() <-----------+-- fuse_service_send_goodbye(0)
+ exit # | fuse_session_loop[_mt]()
+ # | serves until umount
diff --git a/doc/fuservicemount3.8 b/doc/fuservicemount3.8
index 18e285c1ab29..fa2358f3cfed 100644
--- a/doc/fuservicemount3.8
+++ b/doc/fuservicemount3.8
@@ -16,9 +16,17 @@ fuservicemount3 \- mount a FUSE filesystem that runs as a system socket service
.SH DESCRIPTION
Mount a filesystem using a FUSE server that runs as a socket service.
-These servers can be contained using the platform's service management
-framework.
-
+Unlike a traditional FUSE filesystem, which runs in the mount caller's
+context, such a server runs as an independent, sandboxed systemd service.
+\fBfuservicemount3\fP connects to the per-type service socket, hands the
+running server the \fI/dev/fuse\fP device and any backing files it needs,
+and performs the mount on its behalf. These servers can therefore be
+contained using the platform's service management framework.
+.PP
+\fBfuservicemount3\fP is installed setuid root so that unprivileged users
+can mount filesystems handled by a service. It is normally invoked
+indirectly by \fBmount.fuse3\fP(8), not run directly.
+.PP
The FUSE server may ask fuservicemount3 to open files on its behalf.
fuservicemount3 opens a path only in these cases:
.IP \- 2
@@ -34,15 +42,145 @@ A service_open_path line in /etc/fuse.conf lists the path for the filesystem
type.
.PP
It refuses any other request with EPERM.
-
-The second form checks if there is a FUSE service available for the given
-filesystem type.
+.PP
+The second form checks whether a FUSE service is available for the given
+filesystem type, without mounting anything.
+.SH FILESYSTEM REQUIREMENTS
+This is not a transparent wrapper for arbitrary FUSE programs. Only a
+filesystem whose server is written to the libfuse service API can be mounted
+this way. A conventional server calls \fBfuse_main\fP(3), which opens
+\fI/dev/fuse\fP and performs the mount itself. The service sandbox does not
+permit this, so such a server cannot be used unmodified.
+.PP
+A service-capable server instead:
+.IP \- 2
+accepts the listening socket that systemd hands it, and receives its
+arguments and the \fI/dev/fuse\fP descriptor over that socket, rather than
+opening the device itself;
+.IP \- 2
+asks the helper to open any backing files or block devices on its behalf,
+because its sandbox has no direct filesystem access; and
+.IP \- 2
+lets the helper perform the mount, staying in the foreground under systemd.
+.PP
+The server binary, its \fB@.service\fP unit, and its \fB.socket\fP unit must
+all be installed before the type can be mounted. See \fBEXAMPLES\fP below, the
+\fIservice_ll.c\fP and \fIservice_hl.c\fP example servers, the
+\fI<fuse_service.h>\fP header, and the \fIREADME.service-mount-dev\fP document
+for how to build one.
+.SH OPTIONS
+.TP
+.B source
+The filesystem source, passed on to the running server (for example a
+backing device or file). May be empty.
+.TP
+.B mountpoint
+Where to mount the filesystem.
+.TP
+.BI -t " fstype"
+The filesystem type, of the form \fBfuse.\fIsubtype\fR or
+\fBfuseblk.\fIsubtype\fR. The \fIsubtype\fR selects the service socket.
+.TP
+.BI -o " options"
+Mount options to forward to the server.
+.TP
+.B --check
+Only test whether a service socket exists for the type given with \fB-t\fP
+and whether the calling user may connect to it; do not mount. Exit status is
+zero if both hold, non-zero otherwise.
+.SH FILES
+.TP
+.I /run/fuse/<subtype>
+The default location of the per-filesystem service socket. The directory is
+configurable at build time (meson option \fBservice-socket-dir\fP).
+.SH EXAMPLES
+A complete walk-through using the \fIservice_ll\fP example filesystem that
+ships with libfuse.
+.SS "What it is for"
+\fIservice_ll\fP exports a single file or block device as a one-file
+filesystem. Running it as a service keeps the server inside a systemd sandbox
+\(em its own unprivileged user, private namespaces, and no capabilities \(em
+while still letting a permitted user mount it with an ordinary \fBmount\fP
+command. The privileged work (opening the backing device and performing the
+mount) is done only by the setuid \fBfuservicemount3\fP helper. The
+same recipe applies to any server written with the libfuse service API.
+.SS "Setting up the service (administrator, once)"
+The source for this example ships with the libfuse distribution in its
+\fIexample\fP directory: \fIservice_ll.c\fP together with its helper
+\fIsingle_file.c\fP make up the server, and \fIservice_ll@.service\fP and
+\fIservice_ll.socket\fP are its systemd units. From that directory, build the
+server and install the binary on the root filesystem:
+.PP
+.RS
+.nf
+gcc -Wall single_file.c service_ll.c $(pkg-config fuse3 --cflags --libs) -o service_ll
+sudo install -m 0755 service_ll /usr/local/sbin/service_ll
+.fi
+.RE
+.PP
+libfuse provides two systemd units for this example: \fIservice_ll@.service\fP
+(the sandboxed server) and \fIservice_ll.socket\fP (the activation socket,
+already configured to listen at \fI/run/fuse/service_ll\fP). Edit the
+service unit's \fBExecStart\fP to point at the installed binary:
+.PP
+.RS
+.nf
+ExecStart=/usr/local/sbin/service_ll
+.fi
+.RE
+.PP
+Install both units, reload systemd, and start the socket:
+.PP
+.RS
+.nf
+sudo cp service_ll@.service service_ll.socket /run/systemd/system/
+sudo systemctl daemon-reload
+sudo systemctl start service_ll.socket
+.fi
+.RE
+.PP
+Only the socket is running now; systemd starts a fresh, isolated server
+instance on demand for each mount.
+.SS "Mounting and using it"
+Confirm a service is available for the type (this prints nothing; the exit
+status is the answer):
+.PP
+.RS
+.nf
+fuservicemount3 -t fuse.service_ll --check && echo available
+.fi
+.RE
+.PP
+Mount it, passing the backing device or file as the source. Run this as root
+or from an \fI/etc/fstab\fP entry that permits the mount:
+.PP
+.RS
+.nf
+mount -t fuse.service_ll /dev/sda /mnt
+.fi
+.RE
+.PP
+\fBmount.fuse3\fP(8) notices the service, \fBfuservicemount3\fP opens
+\fI/dev/sda\fP and performs the mount, and the data appears under \fI/mnt\fP.
+Unmount it like any other FUSE filesystem:
+.PP
+.RS
+.nf
+fusermount3 -u /mnt
+.fi
+.RE
+.PP
+For the full hardened unit files and further detail, see the
+\fIservice_ll@.service\fP and \fIservice_ll.socket\fP files shipped with
+libfuse and the \fIREADME.service-mount\fP document.
.SH "AUTHORS"
.LP
The author of the fuse socket service code is Darrick J. Wong <djwong@kernel.org>.
Debian GNU/Linux distribution.
.SH SEE ALSO
+.BR mount.fuse3 (8)
.BR fusermount3 (1)
.BR fusermount (1)
.BR mount (8)
.BR fuse (4)
+.BR systemd.socket (5)
diff --git a/doc/mainpage.dox b/doc/mainpage.dox
index 36ba3bcba268..9de96e1410ab 100644
--- a/doc/mainpage.dox
+++ b/doc/mainpage.dox
@@ -28,6 +28,19 @@ separate set of API functions.
The high-level API that is primarily specified in fuse.h. The
low-level API that is primarily documented in fuse_lowlevel.h.
+## Running a filesystem as a systemd service ##
+
+A FUSE server can also be run as a sandboxed, socket-activated systemd
+service rather than as a child of the mounting process. In this model the
+server runs under its own unprivileged identity and in private namespaces,
+while a small setuid helper (fuservicemount3) performs the mount on its
+behalf. Servers use the service API in fuse_service.h; the service_hl.c and
+service_ll.c examples show the high- and low-level variants.
+
+The README.service-mount and README.service-mount-dev files in the source
+*doc* directory document this feature for administrators and filesystem
+authors respectively.
+
## Examples ##
FUSE comes with several examples in the <a
diff --git a/doc/mount.fuse3.8 b/doc/mount.fuse3.8
index d55c96139d9f..b3c959aad63c 100644
--- a/doc/mount.fuse3.8
+++ b/doc/mount.fuse3.8
@@ -231,6 +231,23 @@ Switch to \fBUSER\fP and its primary group before launching the FUSE file system
\fBdrop_privileges\fP
Perform setup of the FUSE file descriptor and mounting the file system before launching the FUSE file system process. \fBmount.fuse3\fP requires privilege to do so, i.e. must be run as root or at least with \fBCAP_SYS_ADMIN\fP and \fBCAP_SETPCAP\fP. It will launch the file system process fully unprivileged, i.e. without \fBcapabilities\fP(7) and \fBprctl\fP(2) flags set up such that privileges can't be reacquired (e.g. via setuid or fscaps binaries). This reduces risk in the event of the FUSE file system process getting compromised by malicious file system data. Because the file system program is launched after privileges have been dropped, it and the libraries it links against must reside at a path the unprivileged process can resolve: every directory component must be searchable without elevated privileges.
+.SH SERVICE MOUNTS
+If libfuse was built with service mount support, \fBmount.fuse3\fP can mount
+filesystems whose server runs as a sandboxed systemd socket service instead of
+as a child of the mounting process. When you mount a type \fBfuse.\fIsubtype\fR
+(or \fBfuseblk.\fIsubtype\fR), \fBmount.fuse3\fP first checks for a service
+socket for that subtype. If one exists, the mount is handed to
+\fBfuservicemount3\fP(8) and performed by the already-running, isolated server;
+all arguments except \fB-t\fP \fItype\fR are forwarded to it.
+.PP
+If no service socket exists, \fBmount.fuse3\fP transparently falls back to the
+traditional behaviour and runs the filesystem server program directly, so
+filesystems that are not set up as services are unaffected. Some options that
+are incompatible with an already-running server (such as passing a pre-opened
+FUSE file descriptor, or \fBsetuid=USER\fP) also force the traditional path.
+.PP
+See the libfuse \fIREADME.service-mount\fP document for details on installing
+and using service-mounted filesystems.
.SH FUSE MODULES (STACKING)
Modules are filesystem stacking support to high level API. Filesystem modules can be built into libfuse or loaded from shared object
.SS "iconv"
@@ -276,5 +293,6 @@ Debian GNU/Linux distribution.
.SH SEE ALSO
.BR fusermount3 (1)
.BR fusermount (1)
+.BR fuservicemount3 (8)
.BR mount (8)
.BR fuse (4)
--
2.53.0
^ permalink raw reply related [flat|nested] 26+ messages in thread
* Re: [PATCH v2 02/14] mount_service: warn about paths not named on the command line
2026-09-28 11:02 ` [PATCH v2 02/14] mount_service: warn about paths not named on the command line Bernd Schubert via B4 Relay
@ 2026-09-29 2:10 ` Darrick J. Wong
2026-09-30 11:06 ` Bernd Schubert
0 siblings, 1 reply; 26+ messages in thread
From: Darrick J. Wong @ 2026-09-29 2:10 UTC (permalink / raw)
To: bernd; +Cc: fuse-devel, neal
On Mon, Sep 28, 2026 at 01:02:04PM +0200, Bernd Schubert via B4 Relay wrote:
> From: Bernd Schubert <bernd@bsbernd.com>
>
> In a service mount, the fuse server runs as a systemd service in a
> sandbox that has no access to the user's files. The user runs mount,
> which starts fuservicemount3, a setuid-root helper. The fuse server
> sends requests to the helper over a socket. With an OPEN request, the
> server asks the helper to open its backing file, for example the disk
> image named on the mount command line. The helper opens the file with
> the user's credentials and passes the file descriptor to the server.
> fusermount3 opens nothing for the fuse server except /dev/fuse; the
> server runs as the user and opens its own files.
>
> The helper opens any path the server sends. An attacker who controlled
> the server could use this to read every file the user can read, for
> example ~/.ssh/id_ed25519, and the sandbox does not prevent it. The
> helper now prints a warning if the user did not name the path when
> mounting: as a whole argument, as the value of a name=value option, or
> glued to a short option as in "-J/dev/sdb1". The helper splits the
> options with fuse_opt_parse(), as the fuse server does, so both see the
> same option values. The helper still opens the path, because a server
> can take a path in a form that none of these checks recognizes.
>
> Enforced permissions follow up in the next commit.
>
> Assisted-by: LLM
> Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
I wonder if there are any fuse servers out there that take parameters
like:
-o bdevs=/dev/sda:/dev/sdb,otheroption=whatever
but ... let's let them come out of the woodwork?
I think this is a good addition :)
Reviewed-by: "Darrick J. Wong" <djwong@kernel.org>
--D
> ---
> util/mount_service.c | 73 +++++++++++++++++++++++++++++++++++++++++++++++-----
> 1 file changed, 66 insertions(+), 7 deletions(-)
>
> diff --git a/util/mount_service.c b/util/mount_service.c
> index 7549e0b5024f..446f37f61916 100644
> --- a/util/mount_service.c
> +++ b/util/mount_service.c
> @@ -770,9 +770,55 @@ static bool arg_in_cmdline(int argc, const char * const argv[],
> return false;
> }
>
> +struct option_value_match {
> + const char *path;
> + bool found;
> +};
> +
> +/* fuse_opt_parse() callback: set match->found if the path is this option's value */
> +static int match_option_value(void *data, const char *arg, int key,
> + struct fuse_args *outargs)
> +{
> + struct option_value_match *match = data;
> + const char *value = strchr(arg, '=');
> +
> + (void) outargs;
> +
> + if (key != FUSE_OPT_KEY_OPT)
> + return 0;
> +
> + if (value && !strcmp(value + 1, match->path))
> + match->found = true;
> +
> + /* Short option with its value glued on, as in "-J/dev/sdb1" */
> + if (arg[0] == '-' && arg[1] && arg[1] != '-' &&
> + !strcmp(arg + 2, match->path))
> + match->found = true;
> +
> + return 0;
> +}
> +
> +/* @return true for the path in "-o name=path", "--name=path" or "-Xpath" */
> +static bool option_value_in_cmdline(int argc, const char * const argv[],
> + const char *path)
> +{
> + struct option_value_match match = {
> + .path = path,
> + };
> + struct fuse_args args = FUSE_ARGS_INIT(argc, (char **)argv);
> + int ret;
> +
> + /* Parse like the fuse server does, so both see the same values */
> + ret = fuse_opt_parse(&args, &match, NULL, match_option_value);
> + fuse_opt_free_args(&args);
> +
> + return !ret && match.found;
> +}
> +
> static int mount_service_open_path(const struct mount_service *mo,
> mode_t expected_fmt,
> - struct fuse_service_packet *p, size_t psz)
> + struct fuse_service_packet *p, size_t psz,
> + int argc, const char * const argv[])
> {
> const struct fuse_service_open_command *oc =
> container_of(p, struct fuse_service_open_command, p);
> @@ -800,6 +846,15 @@ static int mount_service_open_path(const struct mount_service *mo,
> return mount_service_send_file_error(mo, EINVAL, oc->path);
> }
>
> + /*
> + * The file is opened outside the service sandbox, so report a path
> + * the user did not name.
> + */
> + if (!arg_in_cmdline(argc, argv, oc->path) &&
> + !option_value_in_cmdline(argc, argv, oc->path))
> + fprintf(stderr, "%s: %s: warning: file not in command line arguments\n",
> + mo->msgtag, oc->path);
> +
> open_flags = ntohl(oc->open_flags) | O_CLOEXEC;
> drop_privs();
> fd = open(oc->path, open_flags, ntohl(oc->create_mode));
> @@ -834,16 +889,18 @@ static int mount_service_open_path(const struct mount_service *mo,
>
> static int mount_service_handle_open_cmd(const struct mount_service *mo,
> struct fuse_service_packet *p,
> - size_t psz)
> + size_t psz, int argc,
> + const char * const argv[])
> {
> - return mount_service_open_path(mo, 0, p, psz);
> + return mount_service_open_path(mo, 0, p, psz, argc, argv);
> }
>
> static int mount_service_handle_open_bdev_cmd(const struct mount_service *mo,
> struct fuse_service_packet *p,
> - size_t psz)
> + size_t psz, int argc,
> + const char * const argv[])
> {
> - return mount_service_open_path(mo, S_IFBLK, p, psz);
> + return mount_service_open_path(mo, S_IFBLK, p, psz, argc, argv);
> }
>
> #ifdef HAVE_NEW_MOUNT_API
> @@ -1838,10 +1895,12 @@ int mount_service_main(int argc, char *argv[])
>
> switch (ntohl(p->magic)) {
> case FUSE_SERVICE_OPEN_CMD:
> - ret = mount_service_handle_open_cmd(&mo, p, sz);
> + ret = mount_service_handle_open_cmd(&mo, p, sz,
> + argc, (const char * const *)argv);
> break;
> case FUSE_SERVICE_OPEN_BDEV_CMD:
> - ret = mount_service_handle_open_bdev_cmd(&mo, p, sz);
> + ret = mount_service_handle_open_bdev_cmd(&mo, p, sz,
> + argc, (const char * const *)argv);
> break;
> case FUSE_SERVICE_FSOPEN_CMD:
> ret = mount_service_handle_fsopen_cmd(&mo, p, sz);
>
> --
> 2.53.0
>
>
>
^ permalink raw reply [flat|nested] 26+ messages in thread
* Re: [PATCH v2 03/14] mount_service: refuse paths the user did not name
2026-09-28 11:02 ` [PATCH v2 03/14] mount_service: refuse paths the user did not name Bernd Schubert via B4 Relay
@ 2026-09-29 2:26 ` Darrick J. Wong
2026-09-30 11:46 ` Bernd Schubert
0 siblings, 1 reply; 26+ messages in thread
From: Darrick J. Wong @ 2026-09-29 2:26 UTC (permalink / raw)
To: bernd; +Cc: fuse-devel, neal
On Mon, Sep 28, 2026 at 01:02:05PM +0200, Bernd Schubert via B4 Relay wrote:
> From: Bernd Schubert <bernd@bsbernd.com>
>
> fuservicemount3 warns about a path that the user did not name on the
> command line, but still opens it. A server can take a path in a form
> that the helper cannot split, for example "-journal/dev/sdb1" or its
> own option syntax. The administrator can now list such paths in
> /etc/fuse.conf, per filesystem type:
>
> service_open_path = ext4 /dev/sd*
>
> The pattern is matched with fnmatch() and FNM_PATHNAME, so "*" does not
> match "/". A requested path with a "." or ".." component never matches,
> because "*" matches "..", and "/dev/*" would then open "/". The helper
> now refuses any other path with EPERM.
>
> Assisted-by: LLM
> Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
> ---
> doc/fuservicemount3.8 | 16 ++++++++
> doc/mount.fuse3.8 | 7 ++++
> include/fuse_service.h | 4 +-
> test/test_fuser_conf.c | 55 ++++++++++++++++++++++++++++
> util/fuse.conf | 11 ++++++
> util/fuser_conf.c | 99 ++++++++++++++++++++++++++++++++++++++++++++++++++
> util/fuser_conf.h | 3 ++
> util/mount_service.c | 13 ++++---
> 8 files changed, 202 insertions(+), 6 deletions(-)
>
> diff --git a/doc/fuservicemount3.8 b/doc/fuservicemount3.8
> index aa2167cb4872..18e285c1ab29 100644
> --- a/doc/fuservicemount3.8
> +++ b/doc/fuservicemount3.8
> @@ -19,6 +19,22 @@ Mount a filesystem using a FUSE server that runs as a socket service.
> These servers can be contained using the platform's service management
> framework.
>
> +The FUSE server may ask fuservicemount3 to open files on its behalf.
> +fuservicemount3 opens a path only in these cases:
> +.IP \- 2
> +The path is a command line argument, for example /srv/disk.img.
> +.IP \- 2
> +The path is the value in a key=value option, for example /dev/sdb1 in
> +"-o journal_dev=/dev/sdb1".
> +.IP \- 2
> +The path directly follows a short option, for example /dev/sdb1 in
> +"-J/dev/sdb1".
> +.IP \- 2
> +A service_open_path line in /etc/fuse.conf lists the path for the filesystem
> +type.
> +.PP
> +It refuses any other request with EPERM.
> +
> The second form checks if there is a FUSE service available for the given
> filesystem type.
> .SH "AUTHORS"
> diff --git a/doc/mount.fuse3.8 b/doc/mount.fuse3.8
> index 2e587458a06e..d55c96139d9f 100644
> --- a/doc/mount.fuse3.8
> +++ b/doc/mount.fuse3.8
> @@ -38,6 +38,13 @@ Allow non-root users to specify the \fBallow_other\fP or
> \fBallow_root\fP mount options (see below).
> .TP
> These limits are enforced by the \fBfusermount3\fP helper, so they can be avoided by filesystems that run as root.
> +.TP
> +\fBservice_open_path = SUBTYPE PATTERN\fP
> +Allow \fBfuservicemount3\fP(8) to open paths that match \fIPATTERN\fP for the
> +server of a service mount of type \fBfuse.\fISUBTYPE\fR, in addition to the
> +paths on the mount command line. \fIPATTERN\fP is an absolute path in which "*"
> +does not match "/". A pattern that matches a directory gives the server every
> +file below it. The line can be repeated.
> .SH OPTIONS
> Most of the generic mount options described in \fBmount\fP are
> supported (\fBro\fP, \fBrw\fP, \fBsuid\fP, \fBnosuid\fP, \fBdev\fP,
> diff --git a/include/fuse_service.h b/include/fuse_service.h
> index d6aedea8f0f8..2114e7772bf5 100644
> --- a/include/fuse_service.h
> +++ b/include/fuse_service.h
> @@ -139,6 +139,8 @@ int fuse_service_parse_cmdline_opts(struct fuse_args *args,
>
> /**
> * Ask the mount.service helper to open a file on behalf of the fuse server.
> + * The helper refuses a path that the mount command line does not name and
> + * fuse.conf does not list; fuse_service_receive_file() then reports -EPERM.
> *
> * @param sf service context
> * @param path the path to file
> @@ -153,7 +155,7 @@ int fuse_service_request_file(const struct fuse_service *sf, const char *path,
>
> /**
> * Ask the mount.service helper to open a block device on behalf of the fuse
> - * server.
> + * server. The helper refuses the same paths as for a file request.
> *
> * @param sf service context
> * @param path the path to file
> diff --git a/test/test_fuser_conf.c b/test/test_fuser_conf.c
> index 4d974931cf58..6d95fe932039 100644
> --- a/test/test_fuser_conf.c
> +++ b/test/test_fuser_conf.c
> @@ -105,6 +105,59 @@ static int test_trimmed_options(void)
> return 0;
> }
>
> +static int test_service_open_path(void)
> +{
> + const char *test = "service_open_path";
> +
> + if (write_conf("service_open_path = ext4 /dev/sd*\n"
> + "service_open_path = ext4 /dev/nvme*\n"
> + "service_open_path = ext4 relative/path\n"
> + "service_open_path = xfs\t/srv/xfs.img\n"
> + "service_open_path = xfs /srv/img/*\n"
> + " \tservice_open_path = ext4 /srv/indented.img\n"
> + "service_open_path =\x20\n"
> + "service_open_path = ext4\n") == -1)
> + return fail(test, "could not write the config file");
> +
> + read_conf(progname);
> +
> + if (!service_open_path_listed("ext4", "/dev/sda"))
> + return fail(test, "/dev/sd* did not match /dev/sda");
> + if (!service_open_path_listed("ext4", "/dev/nvme0n1"))
> + return fail(test, "a second ext4 line was not recognised");
> + if (!service_open_path_listed("ext4", "/srv/indented.img"))
> + return fail(test, "an indented line was not recognised");
> + if (service_open_path_listed("ext4", "/dev/sda/x"))
> + return fail(test, "* matched a /");
> + if (service_open_path_listed("xfs", "/dev/sda"))
> + return fail(test, "an ext4 line matched for xfs");
> + if (service_open_path_listed("ext4", "relative/path"))
> + return fail(test, "a relative pattern was accepted");
> + if (!service_open_path_listed("xfs", "/srv/xfs.img"))
> + return fail(test, "a tab-separated line was not recognised");
> + if (!service_open_path_listed("xfs", "/srv/img/a.img"))
> + return fail(test, "/srv/img/* did not match /srv/img/a.img");
> + if (service_open_path_listed("xfs", "/srv/img/..") ||
> + service_open_path_listed("xfs", "/srv/img/."))
> + return fail(test, "a . or .. component was accepted");
> + /* Either line, if stored, would match the empty path */
> + if (service_open_path_listed("", ""))
> + return fail(test, "a line without a subtype was accepted");
> + if (service_open_path_listed("ext4", ""))
> + return fail(test, "a line without a pattern was accepted");
> +
> + if (write_conf("\n") == -1)
> + return fail(test, "could not write the config file");
> +
> + read_conf(progname);
> +
> + if (service_open_path_listed("ext4", "/dev/sda"))
> + return fail(test, "a line survived re-reading the config");
> +
> + printf("PASS: %s\n", test);
> + return 0;
> +}
> +
> int main(void)
> {
> char tempdir[] = "/tmp/test_fuser_conf.XXXXXX";
> @@ -123,6 +176,8 @@ int main(void)
> goto out_unlink;
> if (test_trimmed_options())
> goto out_unlink;
> + if (test_service_open_path())
> + goto out_unlink;
>
> printf("All fuse.conf parser tests passed\n");
> result = 0;
> diff --git a/util/fuse.conf b/util/fuse.conf
> index ab048e0347b2..2c182ffa9d6a 100644
> --- a/util/fuse.conf
> +++ b/util/fuse.conf
> @@ -15,3 +15,14 @@
> # equals sign).
>
> #mount_max = 1000
> +
> +
> +# service_open_path = <subtype> <pattern> - a FUSE server that runs as a socket
> +# service may ask fuservicemount3 to open paths that match <pattern>, in
> +# addition to the paths on the mount command line. <subtype> is the filesystem
> +# type after "fuse.", <pattern> an absolute path in which "*" does not match
> +# "/". The line can be repeated to allow different patterns and subtypes.
> +# A pattern that matches a directory gives the server every file below it.
> +
> +#service_open_path = ext4 /dev/sd*
> +#service_open_path = ext4 /dev/nvme*
> diff --git a/util/fuser_conf.c b/util/fuser_conf.c
> index 12688f6c42b7..5ec9d263ac2f 100644
> --- a/util/fuser_conf.c
> +++ b/util/fuser_conf.c
> @@ -18,6 +18,7 @@
> #include <stdio.h>
> #include <stdlib.h>
> #include <errno.h>
> +#include <fnmatch.h>
> #include <mntent.h>
> #include <unistd.h>
> #include <sys/fsuid.h>
> @@ -35,6 +36,14 @@ int mount_max = 1000;
> static uid_t oldfsuid;
> static gid_t oldfsgid;
>
> +struct service_open_path {
> + struct service_open_path *next;
> + char *subtype;
> + char *pattern;
> +};
> +
> +static struct service_open_path *service_open_paths;
> +
> // Older versions of musl libc don't unescape entries in /etc/mtab
>
> // unescapes octal sequences like \040 in-place
> @@ -192,12 +201,100 @@ static void strip_line(char *line)
> memmove(line, s, strlen(s)+1);
> }
>
> +/*
> + * Store one service_open_path line. For the line
> + * "service_open_path = ext4 /dev/sd*", str is "ext4 /dev/sd*".
> + */
> +static void parse_service_open_path(const char *str, int linenum,
> + const char *progname)
> +{
> + /* <subtype> ends at the first blank */
> + const size_t subtype_len = strcspn(str, " \t");
> + const char *pattern = str + subtype_len;
> + struct service_open_path *entry;
> +
> + /* The rest of the line is <pattern>, blanks inside it included */
> + pattern += strspn(pattern, " \t");
> + /* A relative pattern would depend on each user's working directory */
> + if (!subtype_len || pattern[0] != '/') {
> + fprintf(stderr,
> + "%s: invalid service_open_path in %s at line %i\n",
> + progname, FUSE_CONF, linenum);
> + return;
> + }
> +
> + entry = calloc(1, sizeof(*entry));
> + if (entry) {
> + entry->subtype = strndup(str, subtype_len);
> + entry->pattern = strdup(pattern);
> + }
> + /* Going on without the line would refuse paths the admin allowed */
> + if (!entry || !entry->subtype || !entry->pattern) {
> + fprintf(stderr, "%s: failed to allocate memory\n", progname);
> + exit(1);
> + }
> +
> + /* Order does not matter, the lookup checks every entry */
> + entry->next = service_open_paths;
> + service_open_paths = entry;
> +}
> +
> +/* The config can be read more than once; drop the lines of the last read */
> +static void free_service_open_paths(void)
> +{
> + while (service_open_paths) {
> + struct service_open_path *entry = service_open_paths;
> +
> + service_open_paths = entry->next;
> + free(entry->subtype);
> + free(entry->pattern);
> + free(entry);
> + }
Might want to null out service_open_paths here to avoid a UAF in case
this function ever gets used anywhere other than exit.
> +}
> +
> +/* @return true if a path component is "." or "..", as in "/srv/img/.." */
> +static bool has_dot_component(const char *path)
> +{
> + const char *comp = path;
> +
> + for (;;) {
> + const size_t len = strcspn(comp, "/");
> +
> + if ((len == 1 && comp[0] == '.') ||
> + (len == 2 && comp[0] == '.' && comp[1] == '.'))
> + return true;
> + if (!comp[len])
> + return false;
> + comp += len + 1;
> + }
> +}
> +
> +bool service_open_path_listed(const char *subtype, const char *path)
> +{
> + const struct service_open_path *entry;
> +
> + /* "*" also matches "..", which reaches the parent directory */
> + if (has_dot_component(path))
> + return false;
> +
> + for (entry = service_open_paths; entry; entry = entry->next)
> + if (!strcmp(entry->subtype, subtype) &&
Would you consider allowing "*" for the subtype in the config file?
e.g.
service_open_path = * /proc/cpuinfo
So that we could (say) allowlist things like /proc/pressure that would
allow a fuse server to monitor memory stalls in the calling process'
namespaces and perhaps drop its caches?
(I don't know if PSI info is really useful for anyone, it's just a
thought I had while reading this patch.)
The code changes look good to me, modulo that question above. :)
Thanks for expanding this!
--D
> + !fnmatch(entry->pattern, path, FNM_PATHNAME))
> + return true;
> +
> + return false;
> +}
> +
> static void parse_line(const char *line, int linenum, const char *progname)
> {
> int tmp;
> + int value_pos = -1;
>
> if (strcmp(line, "user_allow_other") == 0)
> user_allow_other = 1;
> + else if (sscanf(line, "service_open_path = %n", &value_pos) == 0 &&
> + value_pos >= 0)
> + parse_service_open_path(line + value_pos, linenum, progname);
> else if (sscanf(line, "mount_max = %i", &tmp) == 1) {
> if (tmp < -1)
> fprintf(stderr,
> @@ -216,6 +313,8 @@ void read_conf(const char *progname)
> {
> FILE *fp = fopen(FUSE_CONF, "r");
>
> + free_service_open_paths();
> +
> if (fp != NULL) {
> int linenum = 1;
> char line[256];
> diff --git a/util/fuser_conf.h b/util/fuser_conf.h
> index ea58537cc4c2..ccfc58877100 100644
> --- a/util/fuser_conf.h
> +++ b/util/fuser_conf.h
> @@ -8,6 +8,7 @@
> #ifndef FUSER_CONF_H_
> #define FUSER_CONF_H_
>
> +#include <stdbool.h>
> #include <sys/vfs.h>
> #include <sys/stat.h>
>
> @@ -40,6 +41,8 @@ int count_fuse_fs(const char *progname);
>
> void read_conf(const char *progname);
>
> +bool service_open_path_listed(const char *subtype, const char *path);
> +
> void drop_privs(void);
> void restore_privs(void);
>
> diff --git a/util/mount_service.c b/util/mount_service.c
> index 446f37f61916..b4081d53273e 100644
> --- a/util/mount_service.c
> +++ b/util/mount_service.c
> @@ -847,13 +847,16 @@ static int mount_service_open_path(const struct mount_service *mo,
> }
>
> /*
> - * The file is opened outside the service sandbox, so report a path
> - * the user did not name.
> + * The file is opened outside the service sandbox, so only hand out
> + * what the user named or fuse.conf lists.
> */
> if (!arg_in_cmdline(argc, argv, oc->path) &&
> - !option_value_in_cmdline(argc, argv, oc->path))
> - fprintf(stderr, "%s: %s: warning: file not in command line arguments\n",
> - mo->msgtag, oc->path);
> + !option_value_in_cmdline(argc, argv, oc->path) &&
> + !service_open_path_listed(mo->subtype, oc->path)) {
> + fprintf(stderr, "%s: %s: file must be in command line arguments or in %s\n",
> + mo->msgtag, oc->path, FUSE_CONF);
> + return mount_service_send_file_error(mo, EPERM, oc->path);
> + }
>
> open_flags = ntohl(oc->open_flags) | O_CLOEXEC;
> drop_privs();
>
> --
> 2.53.0
>
>
>
^ permalink raw reply [flat|nested] 26+ messages in thread
* Re: [PATCH v2 04/14] mount_service: use openat to OPEN paths
2026-09-28 11:02 ` [PATCH v2 04/14] mount_service: use openat to OPEN paths Bernd Schubert via B4 Relay
@ 2026-09-29 2:27 ` Darrick J. Wong
0 siblings, 0 replies; 26+ messages in thread
From: Darrick J. Wong @ 2026-09-29 2:27 UTC (permalink / raw)
To: bernd; +Cc: fuse-devel, neal
On Mon, Sep 28, 2026 at 01:02:06PM +0200, Bernd Schubert via B4 Relay wrote:
> From: Bernd Schubert <bernd@bsbernd.com>
>
> The fuse server sends fuservicemount3 a series of requests: OPEN for
> its backing file, MNTPT to name the mount point, then MOUNT. The server
> chooses the order. For a directory mount point, attach_to_mountpoint()
> changes the working directory of the helper to the mount point. The
> helper then mounts on ".", so a rename of the path cannot redirect the
> mount.
>
> A relative path in an OPEN request after MNTPT resolved inside the
> mount point. For "fuservicemount3 disk.img /mnt -t fuse.service_ll",
> an OPEN of "disk.img" matched the command line argument, but the helper
> opened /mnt/disk.img, not disk.img in the user's working directory. The
> helper now opens OPEN paths with openat() on the working directory it
> started in.
>
> Assisted-by: LLM
> Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
Looks good!
Reviewed-by: "Darrick J. Wong" <djwong@kernel.org>
--D
> ---
> util/mount_service.c | 20 +++++++++++++++++++-
> 1 file changed, 19 insertions(+), 1 deletion(-)
>
> diff --git a/util/mount_service.c b/util/mount_service.c
> index b4081d53273e..84e9d831ce03 100644
> --- a/util/mount_service.c
> +++ b/util/mount_service.c
> @@ -84,6 +84,9 @@ struct mount_service {
> /* fd for fsopen */
> int fsopenfd;
>
> + /* fd for the initial working directory */
> + int cwdfd;
> +
> /* did we actually mount successfully? */
> bool mounted;
>
> @@ -247,6 +250,18 @@ static int mount_service_init(struct mount_service *mo, int argc, char *argv[])
> return -1;
> }
>
> + drop_privs();
> + mo->cwdfd = open(".", O_PATH | O_CLOEXEC);
> + if (mo->cwdfd < 0) {
> + int error = errno;
> +
> + restore_privs();
> + fprintf(stderr, "%s: cannot open working directory: %s\n",
> + mo->msgtag, strerror(error));
> + return -1;
> + }
> + restore_privs();
> +
> return 0;
> }
>
> @@ -859,8 +874,9 @@ static int mount_service_open_path(const struct mount_service *mo,
> }
>
> open_flags = ntohl(oc->open_flags) | O_CLOEXEC;
> + /* After fchdir to the mountpoint, a relative path would resolve there */
> drop_privs();
> - fd = open(oc->path, open_flags, ntohl(oc->create_mode));
> + fd = openat(mo->cwdfd, oc->path, open_flags, ntohl(oc->create_mode));
> if (fd < 0) {
> int error = errno;
>
> @@ -1807,6 +1823,7 @@ static void mount_service_destroy(struct mount_service *mo)
> close(mo->fusedevfd);
> close(mo->argvfd);
> close(mo->fsopenfd);
> + close(mo->cwdfd);
> shutdown(mo->sockfd, SHUT_RDWR);
> close(mo->sockfd);
>
> @@ -1824,6 +1841,7 @@ static void mount_service_destroy(struct mount_service *mo)
> mo->fusedevfd = -1;
> mo->mountfd = -1;
> mo->fsopenfd = -1;
> + mo->cwdfd = -1;
> }
>
> int mount_service_main(int argc, char *argv[])
>
> --
> 2.53.0
>
>
>
^ permalink raw reply [flat|nested] 26+ messages in thread
* Re: [PATCH v2 12/14] fuse_service: bound argc and arg len read from the args memfd
2026-09-28 11:02 ` [PATCH v2 12/14] fuse_service: bound argc and arg len read from the args memfd Bernd Schubert via B4 Relay
@ 2026-09-29 2:33 ` Darrick J. Wong
0 siblings, 0 replies; 26+ messages in thread
From: Darrick J. Wong @ 2026-09-29 2:33 UTC (permalink / raw)
To: bernd; +Cc: fuse-devel, neal, Keerthana KT
On Mon, Sep 28, 2026 at 01:02:14PM +0200, Bernd Schubert via B4 Relay wrote:
> From: Keerthana KT <keerthana@labs.digiscrypt.com>
>
> fuse_service_append_args() takes the argument count and each argument
> length straight from the args memfd, which this file already treats as
> untrusted (see the SO_PASSRIGHTS guard against a malicious mount
> helper). Both fields are uint32_t and feed allocation math with no
> bound: calloc(memfd_args.argc + existing_args->argc, ...) wraps in
> unsigned arithmetic and undersizes the argv array, while
> calloc(1, memfd_arg.len + 1) wraps to a zero-size buffer when len is
> UINT32_MAX, which the following pread() then overflows.
>
> Nothing bounded the memfd itself, so cap it on both sides with a new
> FUSE_SERVICE_MAX_ARGV_SIZE. The mount helper refuses to write a string
> that would push the file past the cap, and the server fstat()s the file
> and refuses to parse one larger than it. The file size then bounds the
> rest: argc cannot exceed the number of iovecs that fit between the
> header and the strings, and no string can be longer than the file
> holding it. An argc of zero is rejected as well, because only the first
> loop iteration assigns argv[0].
>
> Signed-off-by: Keerthana KT <keerthana@labs.digiscrypt.com>
> Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
> ---
> include/fuse_service_priv.h | 10 ++++++++++
> lib/fuse_service.c | 43 +++++++++++++++++++++++++++++++++++++++++++
> util/mount_service.c | 10 ++++++++++
> 3 files changed, 63 insertions(+)
>
> diff --git a/include/fuse_service_priv.h b/include/fuse_service_priv.h
> index 988f7c9251c8..5b1edce4b7a8 100644
> --- a/include/fuse_service_priv.h
> +++ b/include/fuse_service_priv.h
> @@ -23,6 +23,16 @@ struct fuse_service_memfd_argv {
>
> #define FUSE_SERVICE_MAX_CMD_SIZE (65536)
>
> +/*
> + * Upper bound on the whole argv memfd, as opposed to FUSE_SERVICE_MAX_CMD_SIZE
> + * which bounds one socket command. Both sides check it: the mount helper
> + * refuses to write past it, and the fuse server refuses to parse a file larger
> + * than it. Generous next to any real mount(8) invocation, but small enough
> + * that the counts and lengths the server reads out of the file cannot overflow
> + * the allocation math they feed.
> + */
> +#define FUSE_SERVICE_MAX_ARGV_SIZE (1048576)
sysconf(_SC_ARG_MAX) ?
> +
> #define FUSE_SERVICE_ARGS_MAGIC 0x41524753 /* ARGS */
>
> /* mount.service sends a hello to the server and it replies */
> diff --git a/lib/fuse_service.c b/lib/fuse_service.c
> index 0a05b3fbc1f2..3991f92f09cb 100644
> --- a/lib/fuse_service.c
> +++ b/lib/fuse_service.c
> @@ -629,8 +629,10 @@ int fuse_service_append_args(struct fuse_service *sf,
> struct fuse_args new_args = {
> .allocated = 1,
> };
> + struct stat statbuf;
> char *str = NULL;
> off_t memfd_pos = 0;
> + off_t max_argc;
> ssize_t received;
> unsigned int i;
> int ret;
> @@ -656,6 +658,34 @@ int fuse_service_append_args(struct fuse_service *sf,
> memfd_args.argc = htonl(memfd_args.argc);
> memfd_pos += sizeof(memfd_args);
>
> + ret = fstat(sf->argvfd, &statbuf);
> + if (ret) {
> + int error = errno;
> +
> + fuse_log(FUSE_LOG_ERR, "fuse: service args file stat: %s\n",
> + strerror(error));
> + return -error;
> + }
> + if (statbuf.st_size > FUSE_SERVICE_MAX_ARGV_SIZE) {
> + fuse_log(FUSE_LOG_ERR, "fuse: service args file too large\n");
> + return -EBADMSG;
> + }
> +
> + /*
> + * The array of argv iovecs sits between the header and the strings, so
> + * the file size bounds argc. Reject a count the file cannot hold: the
> + * sum below is computed in unsigned arithmetic and would otherwise wrap
> + * and undersize the array. argc 0 is rejected as well, because only
> + * the first loop iteration fills argv[0].
> + */
> + max_argc = (statbuf.st_size - (off_t)sizeof(memfd_args)) /
> + (off_t)sizeof(struct fuse_service_memfd_arg);
> + if (memfd_args.argc == 0 || memfd_args.argc > max_argc) {
> + fuse_log(FUSE_LOG_ERR, "fuse: service args file argc %u invalid\n",
> + memfd_args.argc);
> + return -EBADMSG;
> + }
> +
> /* Allocate a new array of argv string pointers */
> new_args.argv = calloc(memfd_args.argc + existing_args->argc,
> sizeof(char *));
> @@ -722,6 +752,19 @@ int fuse_service_append_args(struct fuse_service *sf,
> memfd_arg.len = htonl(memfd_arg.len);
> memfd_pos += sizeof(memfd_arg);
>
> + /*
> + * A string cannot be longer than the file holding it. len
> + * UINT32_MAX would make len + 1 wrap to zero below, handing
> + * calloc() a zero-size buffer for the pread() to overflow.
> + */
> + if (memfd_arg.len >= statbuf.st_size) {
You ought to check that (memfd_arg.pos + memfd_arg.len) doesn't exceed
the file size, since this won't catch a correctly sized file with a
garbage memfd_arg array.
--D
> + fuse_log(FUSE_LOG_ERR,
> + "fuse: service args file argv[%u] len %u too large\n",
> + i, memfd_arg.len);
> + ret = -EBADMSG;
> + goto out_new_args;
> + }
> +
> /* read arg string from file */
> str = calloc(1, memfd_arg.len + 1);
> if (!str) {
> diff --git a/util/mount_service.c b/util/mount_service.c
> index 84e9d831ce03..c715729b2162 100644
> --- a/util/mount_service.c
> +++ b/util/mount_service.c
> @@ -442,6 +442,16 @@ static int mount_service_capture_arg(const struct mount_service *mo,
> };
> ssize_t written;
>
> + /*
> + * string_pos already covers the header and the whole array, so this
> + * bounds the entire memfd. The server rejects anything larger.
> + */
> + if (*string_pos + (off_t)string_len > FUSE_SERVICE_MAX_ARGV_SIZE) {
> + fprintf(stderr, "%s: memfd argv[%u] exceeds %d byte limit\n",
> + mo->msgtag, args->argc, FUSE_SERVICE_MAX_ARGV_SIZE);
> + return -1;
> + }
> +
> written = pwrite(mo->argvfd, string, string_len, *string_pos);
> if (written < 0) {
> fprintf(stderr, "%s: memfd argv write: %s\n",
>
> --
> 2.53.0
>
>
>
^ permalink raw reply [flat|nested] 26+ messages in thread
* Re: [PATCH v2 13/14] build: move the default service socket directory to /run/fuse
2026-09-28 11:02 ` [PATCH v2 13/14] build: move the default service socket directory to /run/fuse Bernd Schubert via B4 Relay
@ 2026-09-29 2:34 ` Darrick J. Wong
0 siblings, 0 replies; 26+ messages in thread
From: Darrick J. Wong @ 2026-09-29 2:34 UTC (permalink / raw)
To: bernd; +Cc: fuse-devel, neal
On Mon, Sep 28, 2026 at 01:02:15PM +0200, Bernd Schubert via B4 Relay wrote:
> From: Bernd Schubert <bernd@bsbernd.com>
>
> /run/fuse sounds better for fuse and then it is also shorter
> a few bytes and might reduce 108-byte AF_UNIX path issues, compared
> to /run/filesystems.
>
> Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
I thought about this once or twice, but then decided that there aren't
any filesystem names that come close to 91 bytes. That said, it's at
least less typing. :)
Reviewed-by: "Darrick J. Wong" <djwong@kernel.org>
--D
> ---
> meson.build | 2 +-
> meson_options.txt | 2 +-
> 2 files changed, 2 insertions(+), 2 deletions(-)
>
> diff --git a/meson.build b/meson.build
> index 06e6056ecf21..5e9628b521e7 100644
> --- a/meson.build
> +++ b/meson.build
> @@ -72,7 +72,7 @@ private_cfg.set_quoted('PACKAGE_VERSION', meson.project_version())
> service_socket_dir = get_option('service-socket-dir')
> service_socket_perms = get_option('service-socket-perms')
> if service_socket_dir == ''
> - service_socket_dir = '/run/filesystems'
> + service_socket_dir = '/run/fuse'
> endif
> if service_socket_perms == ''
> service_socket_perms = '0220'
> diff --git a/meson_options.txt b/meson_options.txt
> index 43104290f8b5..e12177fcea30 100644
> --- a/meson_options.txt
> +++ b/meson_options.txt
> @@ -35,7 +35,7 @@ option('sync-init', type: 'combo', choices: ['auto', 'always', 'never'], value:
> description: 'Synchronous FUSE_INIT: auto follows fuse_daemonize_early_start(), always and never override it')
>
> option('service-socket-dir', type : 'string', value : '',
> - description: 'Where to install fuse server sockets (if empty, /run/filesystems)')
> + description: 'Where to install fuse server sockets (if empty, /run/fuse)')
>
> option('service-socket-perms', type : 'string', value : '',
> description: 'Default fuse server socket permissions (if empty, 0220)')
>
> --
> 2.53.0
>
>
>
^ permalink raw reply [flat|nested] 26+ messages in thread
* Re: [PATCH v2 14/14] Improve documentation for fuse service mount
2026-09-28 11:02 ` [PATCH v2 14/14] Improve documentation for fuse service mount Bernd Schubert via B4 Relay
@ 2026-09-29 2:43 ` Darrick J. Wong
2026-09-30 13:09 ` Bernd Schubert
0 siblings, 1 reply; 26+ messages in thread
From: Darrick J. Wong @ 2026-09-29 2:43 UTC (permalink / raw)
To: bernd; +Cc: fuse-devel, neal
On Mon, Sep 28, 2026 at 01:02:16PM +0200, Bernd Schubert via B4 Relay wrote:
> From: Bernd Schubert <bernd@bsbernd.com>
>
> Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
> ---
> doc/README.service-mount | 312 +++++++++++++++++++++++++++++
> doc/README.service-mount-dev | 456 ++++++++++++++++++++++++++++++++++++++++++
> doc/README.service-mount-flow | 201 +++++++++++++++++++
> doc/fuservicemount3.8 | 150 +++++++++++++-
> doc/mainpage.dox | 13 ++
> doc/mount.fuse3.8 | 18 ++
> 6 files changed, 1144 insertions(+), 6 deletions(-)
>
> diff --git a/doc/README.service-mount b/doc/README.service-mount
> new file mode 100644
> index 000000000000..859db863f3da
> --- /dev/null
> +++ b/doc/README.service-mount
> @@ -0,0 +1,312 @@
> +Mounting FUSE filesystems that run as a socket service
> +======================================================
> +
> +This document is for administrators and end users who want to mount a FUSE
> +filesystem whose server runs as a sandboxed systemd socket service, rather
> +than as a process in the mount caller's own context.
> +
> +Developers who want to make their FUSE server runnable this way should read
> +README.service-mount-dev instead.
> +
> +
> +What a service mount is
> +-----------------------
> +
> +A traditional FUSE filesystem runs as a child of whoever mounts it: it
> +inherits that environment, needs mount permission, and can see the caller's
> +files. A *service mount* instead keeps the FUSE server running as an
> +independent systemd service. When someone mounts the filesystem, a small
> +privileged helper (fuservicemount3) connects to the service over a UNIX
> +socket, hands it the /dev/fuse device and any backing files it needs, and
> +performs the mount on its behalf.
> +
> +The benefit is isolation. The server can run:
> +
> + - as a separate, unprivileged uid/gid (systemd DynamicUser),
> + - with no capabilities at all,
> + - in private mount, network, and pid namespaces,
> + - with a restricted system-call filter,
> +
> +while still being mountable by an ordinary user. The server never gains mount
> +permission and never runs in the caller's environment; the privileged work is
> +confined to the fuservicemount3 helper. See example/service_ll@.service for a
> +fully locked-down unit.
> +
> +
> +Do I need this?
> +---------------
> +
> +This feature exists for one specific goal: running a FUSE server with strong
> +privilege separation, where the server itself is fully unprivileged and
> +sandboxed while a separate setuid helper performs the mount. Getting that
> +requires the server to be written to the fuse_service_* API (see
> +README.service-mount-dev). An existing FUSE program that simply calls
> +fuse_main() cannot be mounted this way unmodified: it opens /dev/fuse and
> +performs the mount itself, which the sandbox does not allow.
> +
> +If all you want is to manage an ordinary FUSE filesystem with systemd --
> +start/stop, journald logging, cgroup resource limits --
> +you do NOT need this feature. Run the filesystem under a plain systemd service
> +unit instead, launching it in the foreground so systemd can track it:
> +
> + # myfs.service
> + [Service]
> + ExecStart=/usr/bin/myfs ... -f <mountpoint>
> +
> +systemd-run(1) starts the same thing as a transient unit, without a unit
> +file. It passes the command line on as typed, so the filesystem can get any
> +number of arguments. A unit file fixes them in its ExecStart= line, and a
> +template unit (myfs@.service) takes only one parameter, the instance name.
> +Set unit directives with -p:
> +
> + sudo systemd-run -p MemoryMax=1G myfs <args> -f <mountpoint>
> +
> +This also works in the user's own service manager, as long as fusermount3 is
> +installed setuid root:
> +
> + systemd-run --user -p MemoryMax=1G myfs <args> -f <mountpoint>
> +
> +That filesystem still mounts the traditional way (through fusermount3) and
> +runs in the service's own context; it is not isolated from the mount the way a
> +service mount is.
> +
> +Use a service mount when you specifically want:
> +
> + - the filesystem server to run as a separate, unprivileged uid with no
> + mount permission of its own,
> + - it confined to private mount/network/pid namespaces with no capabilities,
> + - the privileged mount work isolated in the fuservicemount3 helper,
> + - on-demand, socket-activated startup.
> +
> +In short: a plain systemd unit gives you lifecycle management; a service mount
> +gives you lifecycle management AND isolation, at the cost of the fuse server
> +author adapting the server to the service API.
"...the fuse server author needing to adapt..."
Everything below here looked ok to me, though I admit that there's a lot
of documentation so I may have missed some fine details.
--D
> +
> +
> +Requirements
> +------------
> +
> +Service mount support is only built when libfuse is configured with systemd
> +support:
> +
> + - the systemd development headers (libsystemd-dev), and
> + - a known systemd system unit directory.
> +
> +When both are present, meson defines HAVE_SERVICEMOUNT and builds the
> +fuservicemount3 helper. If either is missing, meson prints a warning and the
> +feature is left out; mounts then fall back to the traditional path (see
> +"Dispatch and fallback" below).
> +
> +Relevant meson options:
> +
> + - service-socket-dir directory that holds the per-filesystem service
> + sockets (default: /run/fuse)
> + - service-socket-perms mode for the socket files (default: 0220)
> + - systemd-system-unit-dir
> + where to install service/socket units (default:
> + taken from the systemd pkg-config file)
> +
> +fuservicemount3 is installed setuid root, just like fusermount3, so that
> +unprivileged users can trigger a mount handled by the service. The setuid
> +privilege is what lets it perform the mount; it drops back to the real user
> +before connecting to the service socket, so the socket's own permissions are
> +what decide who may mount. The default 0220 mode is only a starting point --
> +set SocketUser=, SocketGroup= and SocketMode= in the .socket unit to grant the
> +intended users access (see "Who establishes the connection" below).
> +
> +
> +Installing a service
> +--------------------
> +
> +Each mountable filesystem type is backed by two systemd units, named after the
> +filesystem subtype (the part after "fuse." in the mount type). For a subtype
> +"myfs":
> +
> + - myfs@.service the sandboxed server (a template, one instance per
> + connection)
> + - myfs.socket the listening socket that activates it
> +
> +The socket listens on a SOCK_SEQPACKET UNIX socket at
> +
> + <service-socket-dir>/<subtype> e.g. /run/fuse/myfs
> +
> +and is configured with "Accept=yes", so systemd spawns a fresh, isolated
> +server instance for every mount request.
> +
> +The path field (sun_path) of a UNIX socket address holds 108 bytes on Linux,
> +so <service-socket-dir>/<subtype> can be at most 107 characters long. The
> +default /run/fuse leaves 97 characters for the subtype. If the path is
> +longer, mount.fuse3 mounts the traditional way and fuservicemount3 fails with
> +"filesystem type name `<subtype>' is too long".
> +
> +Install the units into the systemd unit directory (usually
> +/run/systemd/system or /etc/systemd/system), then:
> +
> + systemctl daemon-reload
> + systemctl start myfs.socket
> +
> +The socket unit can be enabled to start at boot:
> +
> + systemctl enable myfs.socket
> +
> +The example filesystems ship ready-to-adapt units; see
> +example/service_ll@.service and example/service_ll.socket(.in).
> +
> +
> +Mounting
> +--------
> +
> +Mount the filesystem with the usual mount(8) syntax, using the type
> +"fuse.<subtype>":
> +
> + mount -t fuse.myfs <source> <mountpoint> [-o options]
> +
> +For example:
> +
> + mount -t fuse.service_ll /dev/sda /mnt
> +
> +A block-device-backed filesystem uses "fuseblk.<subtype>" instead.
> +
> +The same line works from /etc/fstab:
> +
> + <source> <mountpoint> fuse.myfs <options> 0 0
> +
> +The mount is handled by the mount.fuse3 helper, which notices that a service
> +socket exists for the type and hands the request to fuservicemount3. Every
> +argument except "-t <type>" -- the <source>, the <mountpoint> and the -o
> +options -- is forwarded to the running server for parsing.
> +
> +
> +Checking whether a service is available
> +---------------------------------------
> +
> +To test whether a service socket exists for a given filesystem type without
> +mounting anything:
> +
> + fuservicemount3 -t fuse.myfs --check
> +
> +It exits 0 if the service socket exists and you may connect to it (write
> +permission), non-zero otherwise. systemd starts a server only when
> +fuservicemount3 connects, so an existing socket means a mount will get one.
> +This relies on RemoveOnStop=yes in the .socket unit; without it, a stopped
> +unit leaves a stale socket file behind.
> +
> +
> +Unmounting
> +----------
> +
> +Unmount as you would any FUSE filesystem:
> +
> + fusermount3 -u <mountpoint>
> +
> +or, as a privileged user:
> +
> + umount <mountpoint>
> +
> +
> +Dispatch and fallback
> +---------------------
> +
> +When you run "mount -t fuse.myfs ...", the mount.fuse3 helper first checks for
> +a service socket for "myfs". The behaviour is:
> +
> + - If a socket exists (and no options that are incompatible with service
> + mounts were given), the mount is performed through fuservicemount3 and the
> + running service.
> +
> + - If no socket exists or options that are incompatible with service mounts
> + were given, mount.fuse3 transparently falls back to the traditional path:
> + it runs the filesystem server program directly, exactly as it did before
> + service mount support.
> +
> +So enabling service mount support does not break filesystems that are not set
> +up as services; they continue to mount the old way.
> +
> +A few options force the traditional path and skip the service even when a
> +socket is present, because they are meaningless to an already-running,
> +isolated server (for example passing a pre-opened FUSE fd, or the
> +mount.fuse3 "setuid=USER" option).
> +
> +
> +Security model
> +--------------
> +
> + - The FUSE server runs under the confinement defined by its .service unit,
> + not under the mount caller's identity, privileges, or namespaces.
> +
> + - The server has no access to the caller's filesystem. Anything it needs
> + (the backing device or file, /dev/fuse) is opened by the privileged
> + fuservicemount3 helper and passed to the server over the socket. The
> + server can refuse to accept further passed file descriptors once it has
> + what it needs.
> +
> + - fuservicemount3 opens a path for the server only if the mount command
> + line names it: as an argument, as the value in a name=value option, or
> + glued to a short option as in "-J/dev/sdb1". The administrator can allow
> + more paths per filesystem subtype in /etc/fuse.conf:
> +
> + service_open_path = ext4 /dev/sd*
> +
> + It refuses any other path with EPERM.
> +
> + - The only setuid-root component is fuservicemount3, which performs just the
> + mount and the file-descriptor hand-off.
> +
> +This is the same trust boundary as fusermount3, but with the filesystem
> +implementation itself kept out of the privileged and caller-facing paths.
> +
> +
> +Who establishes the connection
> +------------------------------
> +
> +Three parties touch the service socket, but only one dials it:
> +
> + - systemd owns and listens on the socket. Starting the .socket unit creates
> + the listening socket at <service-socket-dir>/<subtype>; no server is
> + running yet.
> +
> + - fuservicemount3 (the helper) is the socket client. When you mount, it
> + connects to that socket -- after dropping back to your real, unprivileged
> + user id, so the kernel checks the socket's permissions against you, not
> + against root. This is the access-control gate: only users the socket
> + grants connect (write) permission to can mount.
> +
> + - systemd accepts the connection and, because the .socket unit uses
> + Accept=yes, starts a fresh per-connection server instance and hands it the
> + already-connected socket. The server never connects or accepts; it
> + inherits the live connection.
> +
> +So to control who may mount a given filesystem, set SocketUser=, SocketGroup=
> +and SocketMode= for its .socket unit, for example with
> +"systemctl edit myfs.socket". A chmod or chown on the socket file itself is
> +lost when the socket unit restarts, because systemd creates the file anew.
> +
> +
> +Troubleshooting
> +---------------
> +
> + - "mounts the old way / service is ignored": confirm the socket exists with
> + "fuservicemount3 -t fuse.<subtype> --check", that <service-socket-dir>
> + matches how libfuse was built, and that the .socket unit is started.
> +
> + - "fuservicemount3: not found" or permission errors: verify the helper is
> + installed in sbindir and is setuid root.
> +
> + - "<path>: file must be in command line arguments or in /etc/fuse.conf":
> + the server asked for a path the mount command line does not name. Add a
> + service_open_path line for it (see "Security model").
> +
> + - server-side errors: because the server logs to its own journal, inspect it
> + with "journalctl -u myfs@*" (the example units log to the kernel ring
> + buffer via /dev/ttyprintk, viewable with dmesg).
> +
> +
> +See also
> +--------
> +
> + README.service-mount-dev writing a FUSE server that runs as a service
> + fuservicemount3(8)
> + mount.fuse3(8)
> + fusermount3(1)
> + mount(8)
> + systemd.socket(5)
> diff --git a/doc/README.service-mount-dev b/doc/README.service-mount-dev
> new file mode 100644
> index 000000000000..c39bcb5e4d51
> --- /dev/null
> +++ b/doc/README.service-mount-dev
> @@ -0,0 +1,456 @@
> +Writing a FUSE server that runs as a socket service
> +===================================================
> +
> +This document is for developers who want their FUSE server to be mountable as
> +a sandboxed systemd socket service, using the fuse_service_* API declared in
> +fuse_service.h. Administrators and users who only want to mount such a
> +filesystem should read README.service-mount instead.
> +
> +The complete working examples referenced throughout are:
> +
> + example/service_ll.c low-level API server
> + example/service_hl.c high-level API server
> + example/single_file.c backing-store helper shared by both
> + example/service_ll@.service, example/service_ll.socket.in systemd units
> +
> +
> +The execution model
> +--------------------
> +
> +A service-mount server does not mount anything itself and does not run in the
> +mounting user's context. Instead:
> +
> + 1. systemd listens on a per-subtype UNIX socket (Accept=yes) and starts one
> + confined instance of your server per incoming mount request.
> +
> + 2. The privileged fuservicemount3 helper connects to that socket, opens
> + /dev/fuse, and passes the device fd plus your command-line arguments to
> + the server.
> +
> + 3. Your server cannot open files itself (its sandbox has no access to the
> + caller's filesystem or to /dev), so it asks the helper to open any
> + backing files or block devices on its behalf and pass the descriptors
> + back.
> +
> + 4. Your server binds the FUSE session to the passed /dev/fuse fd and asks
> + the helper to perform the mount. Requests start flowing immediately.
> +
> +Everything the server needs from the outside world therefore arrives over the
> +socket; the server never needs mount permission and never touches the
> +caller's environment.
> +
> +
> +The mount protocol
> +------------------
> +
> +S and H denote the two parties:
> +
> + S = the FUSE server -- your binary, one <subtype>@.service instance
> + H = fuservicemount3 -- the setuid-root mount helper; the socket client
> +
> +Transport:
> +
> + - one AF_UNIX SOCK_SEQPACKET socket, created by systemd at
> + <service-socket-dir>/<subtype> (e.g. /run/fuse/myfs)
> + - H connect()s as the real user (that uid gates who may mount); systemd
> + accept()s (Accept=yes) and hands the connected fd to a fresh S, which
> + adopts it in fuse_service_accept()
> + - one message per datagram (sendmsg with MSG_EOR); a passed fd travels as
> + SCM_RIGHTS ancillary data, exactly one fd per message
> + - every multi-byte field is in network byte order; no message exceeds
> + FUSE_SERVICE_MAX_CMD_SIZE (65536 bytes)
> + - after "DOIT" FUSE traffic uses /dev/fuse, not this socket; H keeps
> + serving commands until S sends "BYEE" or closes the socket
> +
> +Every message begins with a 4-byte magic that spells the quoted tag in ASCII
> +(e.g. "OPEN" is 0x4f50454e), so a message is legible in a hex dump. Most tags
> +are operation mnemonics (OPEN, BDEV, TYPE, NAME, MNTP, DOIT, BYEE, ...); the
> +handshake pair is not: "SAFT" (the HELLO command) and "LAST" (its reply) are
> +named after the film "Safety Last!". Structures, verbatim from
> +fuse_service_priv.h:
> +
> + struct fuse_service_packet { uint32_t magic; };
> +
> + struct fuse_service_hello { /* "SAFT" */
> + struct fuse_service_packet p;
> + uint16_t min_version, max_version; /* both 1 */
> + uint32_t flags; /* ALLOW_OTHER 1<<0 | FUSEBLK 1<<1; what H allows */
> + };
> + struct fuse_service_hello_reply { /* "LAST" */
> + struct fuse_service_packet p;
> + uint16_t version, padding; /* version 1 */
> + };
> + struct fuse_service_simple_reply { /* "REPL" */
> + struct fuse_service_packet p;
> + uint32_t error; /* 0, else positive errno */
> + };
> + struct fuse_service_requested_file { /* "FILE", carries one fd */
> + struct fuse_service_packet p;
> + uint32_t error; /* 0, else positive errno and no fd */
> + char path[]; /* echoes the request path; NUL-terminated */
> + };
> + struct fuse_service_open_command { /* "OPEN" file / "BDEV" device */
> + struct fuse_service_packet p;
> + uint32_t open_flags; /* O_* */
> + uint32_t create_mode;
> + uint32_t request_flags; /* QUIET 1<<0 */
> + uint32_t block_size; /* "BDEV" only */
> + char path[];
> + };
> + struct fuse_service_fsopen_command { /* "TYPE" */
> + struct fuse_service_packet p;
> + uint32_t fsopen_flags; /* FUSEBLK 1<<0, set iff fstype is fuseblk */
> + };
> + struct fuse_service_string_command { /* "NAME" / "OPTS" / "MTAB" */
> + struct fuse_service_packet p;
> + char value[];
> + };
> + struct fuse_service_mountpoint_command { /* "MNTP" */
> + struct fuse_service_packet p;
> + uint16_t expected_fmt, padding; /* S_IFDIR / S_IFREG, or 0 */
> + char value[]; /* the mountpoint */
> + };
> + struct fuse_service_mount_command { /* "DOIT" */
> + struct fuse_service_packet p;
> + uint32_t ms_flags; /* MS_* */
> + };
> + struct fuse_service_bye_command { /* "BYEE" */
> + struct fuse_service_packet p;
> + uint32_t exitcode;
> + };
> +
> +The "argv" descriptor is a memfd; its bytes are one header, then argc entries,
> +then the packed argument strings:
> +
> + struct fuse_service_memfd_argv { uint32_t magic /* "ARGS" */, argc; };
> + struct fuse_service_memfd_arg { uint32_t pos, len; }; /* x argc */
> +
> +The whole memfd is at most FUSE_SERVICE_MAX_ARGV_SIZE (1 MiB) and argc is at
> +least 1; the server refuses a file that breaks either rule.
> +
> +Message sequence. "A -> B msg" = A sends msg to B. A bracketed [call] names
> +the fuse_service_* function that drives the step; "local:" steps send nothing.
> +
> + handshake [fuse_service_accept]
> + H -> S "SAFT" fuse_service_hello
> + S -> H "LAST" fuse_service_hello_reply
> +
> + fd handover, both pushed by H unsolicited [fuse_service_accept]
> + H -> S "FILE" fuse_service_requested_file +fd path "argv"
> + H -> S "FILE" fuse_service_requested_file +fd path "fusedev"
> + local: S reads argv out of the memfd [fuse_service_append_args]
> +
> + backing store, repeated per file, may be none
> + S -> H "OPEN" / "BDEV" fuse_service_open_command
> + [fuse_service_request_file / fuse_service_request_blockdev]
> + H -> S "FILE" fuse_service_requested_file +fd (or error and no fd)
> + [fuse_service_receive_file]
> + local: setsockopt(SO_PASSRIGHTS, 0) [fuse_service_finish_file_requests]
> +
> + mount [fuse_service_session_mount]. S sends each command below; H answers
> + every one with H -> S "REPL" fuse_service_simple_reply, whose
> + nonzero errno aborts the mount.
> + local: bind se to /dev/fd/<fusedev> (fuse_session_mount)
> + S -> H "TYPE" fuse_service_fsopen_command
> + S -> H "NAME" fuse_service_string_command (mtab source)
> + S -> H "MNTP" fuse_service_mountpoint_command
> + S -> H "OPTS" fuse_service_string_command (optional)
> + S -> H "MTAB" fuse_service_string_command (optional)
> + S -> H "DOIT" fuse_service_mount_command (H mounts here)
> +
> + shutdown [fuse_service_send_goodbye]
> + S -> H "BYEE" fuse_service_bye_command no reply; S closes socket
> +
> +README.service-mount-flow follows this sequence through the code of both
> +sides, with the checks each side makes.
> +
> +
> +The two entry points
> +--------------------
> +
> +There are two ways to write the server, mirroring the normal libfuse APIs:
> +
> + - High-level API: do the service setup, then call fuse_service_main(), the
> + service-aware counterpart of fuse_main(). See example/service_hl.c.
> +
> + - Low-level API: do the service setup, create the session yourself, call
> + fuse_service_session_mount(), and run your own event loop. See
> + example/service_ll.c.
> +
> +Both share the same startup, resource-request, and shutdown sequence.
> +
> +IMPORTANT: define FUSE_USE_VERSION to at least FUSE_MAKE_VERSION(3, 19) and
> +include <fuse_service.h>. Do NOT call fuse_daemonize(): a service must stay in
> +the foreground so systemd can track it (fuse_service_session_mount and
> +fuse_service_main arrange this for you). Service mounts do not support
> +synchronous FUSE_INIT yet: FUSE_INIT reaches the server only after
> +fuservicemount3 has performed the mount. Do not call
> +fuse_daemonize_early_start() either.
> +
> +
> +API reference
> +-------------
> +
> +The full per-call documentation lives in fuse_service.h (and fuse.h for the
> +high-level fuse_service_main). Unless noted, each int-returning call returns 0
> +on success or a negative errno. In call order:
> +
> + /* startup */
> + int fuse_service_accept(struct fuse_service **sfp);
> + bool fuse_service_accepted(const struct fuse_service *sf);
> + int fuse_service_append_args(struct fuse_service *sf,
> + struct fuse_args *args);
> + int fuse_service_parse_cmdline_opts(struct fuse_args *args,
> + struct fuse_cmdline_opts *opts); /* returns 0 / -1 */
> +
> + /* capability negotiation */
> + bool fuse_service_can_allow_other(const struct fuse_service *sf);
> + bool fuse_service_can_fuseblk(const struct fuse_service *sf);
> +
> + /* backing files: request, receive each fd, then stop fd passing */
> + int fuse_service_request_file(const struct fuse_service *sf,
> + const char *path, int open_flags, mode_t create_mode,
> + unsigned int request_flags);
> + int fuse_service_request_blockdev(const struct fuse_service *sf,
> + const char *path, int open_flags, mode_t create_mode,
> + unsigned int request_flags, unsigned int block_size);
> + int fuse_service_receive_file(const struct fuse_service *sf,
> + const char *path, int *fdp);
> + int fuse_service_finish_file_requests(const struct fuse_service *sf);
> +
> + /* mount */
> + void fuse_service_expect_mount_format(struct fuse_service *sf,
> + mode_t expected_fmt);
> + int fuse_service_session_mount(struct fuse_service *sf,
> + struct fuse_session *se, mode_t expected_fmt,
> + struct fuse_cmdline_opts *opts);
> + int fuse_service_main(struct fuse_service *sf, struct fuse_args *args,
> + const struct fuse_operations *op, void *user_data);
> +
> + /* shutdown */
> + int fuse_service_send_goodbye(struct fuse_service *sf, int exitcode);
> + void fuse_service_release(struct fuse_service *sf);
> + void fuse_service_destroy(struct fuse_service **sfp);
> + int fuse_service_exit(int ret);
> +
> + #define FUSE_SERVICE_REQUEST_FILE_QUIET (1U << 0)
> +
> +fuse_service_receive_file sets *fdp to a valid fd (>= 0) or a negated errno
> +from the helper's open attempt; the call itself returns nonzero only on a
> +socket-level failure. fuse_service_accept always initialises *sfp; test
> +fuse_service_accepted (true iff *sfp != NULL) to learn whether the program was
> +actually launched as a service.
> +
> +
> +Startup sequence
> +----------------
> +
> +The first thing main() does is accept the service context:
> +
> + struct fuse_service *service;
> +
> + if (fuse_service_accept(&service))
> + goto error; /* socket/handshake failure */
> +
> + if (!fuse_service_accepted(service))
> + goto error; /* not started as a service */
> +
> +fuse_service_accept() looks for the socket handed to the process by systemd,
> +performs the protocol handshake, and receives the argument vector and the
> +/dev/fuse fd. It always initialises *service; use fuse_service_accepted() to
> +find out whether the program is actually running as a service (it returns
> +false, with *service == NULL, when there is no service socket).
> +
> +The example servers require a service and exit otherwise (their error paths
> +run only once the context is valid). A server that also wants to support
> +traditional invocation can branch on fuse_service_accepted() and fall back to
> +fuse_main() / fuse_session_mount(); in that case do not call the other
> +fuse_service_* functions, which assume a valid service context.
> +
> +Next, fold the service-supplied arguments into the fuse_args built from the
> +argc and argv of main(), and parse them:
> +
> + struct fuse_args args = FUSE_ARGS_INIT(argc, argv);
> +
> + if (fuse_service_append_args(service, &args)) /* add helper's args */
> + goto error;
> +
> + if (fuse_opt_parse(&args, &priv, my_opts, my_opt_proc)) /* your opts */
> + goto error;
> +
> +For the low-level API also extract the common command-line options:
> +
> + struct fuse_cmdline_opts opts = { };
> +
> + if (fuse_service_parse_cmdline_opts(&args, &opts))
> + goto error;
> +
> +fuse_service_parse_cmdline_opts() is the service-mount analogue of
> +fuse_parse_cmdline(). It does NOT validate the mountpoint; that is the
> +helper's job. As usual, a missing -o subtype=/fsname= defaults the subtype to
> +the program's basename.
> +
> +
> +Requesting backing files and block devices
> +-------------------------------------------
> +
> +Because the sandbox cannot open files, the server asks the helper to open them
> +and send back the descriptor. This is a two-step request/receive pattern (see
> +single_file_service_open() in example/single_file.c):
> +
> + /* ask the helper to open it */
> + fuse_service_request_file(service, path, open_flags, create_mode, flags);
> + /* or, for a block device: */
> + fuse_service_request_blockdev(service, path, open_flags, create_mode,
> + flags, block_size);
> +
> + /* then collect the descriptor */
> + int fd;
> + fuse_service_receive_file(service, path, &fd);
> +
> +A block_size of 0 leaves the block size of the device unchanged.
> +
> +On success fd is a valid descriptor. A negative fd is a (negated) errno from
> +the helper's open attempt — single_file.c uses this to downgrade an O_RDWR
> +request to O_RDONLY when the backing store is read-only. Pass
> +FUSE_SERVICE_REQUEST_FILE_QUIET in the request flags to suppress the helper's
> +error message when a failure is expected.
> +
> +The helper opens only a path that the mount command line names or that a
> +service_open_path line in /etc/fuse.conf lists, and compares the strings
> +exactly; any other path gets fd == -EPERM. Request the path as the user wrote
> +it, not a canonicalized or rebuilt form. A relative path resolves against the
> +directory mount was run in.
> +
> +Once you have every descriptor you need, close the door on further fd passing:
> +
> + fuse_service_finish_file_requests(service);
> +
> +This tells the kernel to reject any additional descriptors on the socket
> +(via SO_PASSRIGHTS where available), so a compromised or malicious helper
> +cannot smuggle in more fds afterwards.
> +
> +
> +Capability negotiation
> +----------------------
> +
> +During the handshake the helper advertises what it is willing to do. Query it
> +before relying on those behaviours:
> +
> + fuse_service_can_allow_other(service) /* may honour -o allow_other */
> + fuse_service_can_fuseblk(service) /* may mount a fuseblk filesystem */
> +
> +
> +Mounting
> +--------
> +
> +fuservicemount3 mounts on a directory or on a regular file, and the kernel
> +gives the filesystem root the type of the mountpoint. Every access to the
> +root fails with EIO when the root your server reports has a different type.
> +To make fuservicemount3 refuse such a mountpoint before it mounts, pass the
> +type of your root (S_IFDIR or S_IFREG):
> +
> + fuse_service_expect_mount_format(service, S_IFDIR);
> +
> +This call is optional. Without it, fuservicemount3 does not check the root
> +type. A low-level server can instead pass the type as the third argument of
> +fuse_service_session_mount().
> +
> +High-level API — hand off to fuse_service_main(), which builds the operations,
> +performs the mount, and runs the loop:
> +
> + ret = fuse_service_main(service, &args, &my_oper, NULL);
> +
> +Low-level API — create the session, install signal handlers, then mount:
> +
> + se = fuse_session_new(&args, &my_ll_oper, sizeof(my_ll_oper), NULL);
> + ...
> + fuse_set_signal_handlers(se);
> +
> + if (fuse_service_session_mount(service, se, S_IFDIR, &opts))
> + goto error;
> +
> + fuse_session_loop(se); /* or fuse_session_loop_mt(se, config) */
> +
> +fuse_service_session_mount() binds the session to the passed /dev/fuse fd and
> +asks the helper to mount the filesystem. It forces foreground operation and
> +chdir("/") so you do not need (and must not) call fuse_daemonize(). After it
> +returns successfully the kernel is already routing requests to your server, so
> +enter your event loop promptly.
> +
> +
> +Shutdown
> +--------
> +
> +Tell the helper you are leaving, releasing and destroying the service context:
> +
> + fuse_service_send_goodbye(service, exitcode); /* report exit status */
> + fuse_service_release(service); /* free socket-side state */
> + ...
> + fuse_service_destroy(&service); /* free the context */
> +
> + return fuse_service_exit(ret); /* map ret to an exit code */
> +
> +In the examples, send_goodbye is sent once mounting has succeeded and the loop
> +is about to start, and again on the error paths; fuse_service_exit() at the end
> +of main() converts the server's return value into the exit status systemd
> +expects. fuse_service_destroy() takes a pointer to the pointer and clears it.
> +
> +
> +The systemd units
> +-----------------
> +
> +Ship two units per filesystem, named after the subtype (the part after
> +"fuse." in the mount type). For subtype "myfs":
> +
> + myfs.socket — the listening socket. It must use SOCK_SEQPACKET, accept
> + each connection, and listen at the configured service
> + socket directory under the subtype name. The example
> + socket file is processed by meson, which substitutes the
> + build-time values:
> +
> + [Socket]
> + ListenSequentialPacket=@FUSE_SERVICE_SOCKET_DIR_RAW@/myfs
> + Accept=yes
> + SocketMode=@FUSE_SERVICE_SOCKET_PERMS@
> + RemoveOnStop=yes
> +
> + [Install]
> + WantedBy=sockets.target
> +
> + myfs@.service — the server template, one instance per connection. Set
> + ExecStart to your binary and lock the unit down as tightly
> + as the filesystem allows. example/service_ll@.service is a
> + good starting point: DynamicUser, no capabilities, private
> + mount/network/pid namespaces, a @system-service syscall
> + filter, and OOMPolicy=continue so the filesystem is not
> + torn down under memory pressure.
> +
> +The socket name must match the subtype your server reports (via the program
> +basename or -o subtype=), because that is the name fuservicemount3 looks for
> +under the service socket directory.
> +
> +
> +Building and installing
> +-----------------------
> +
> +Build a server the usual way, linking against fuse3 and including the new
> +header:
> +
> + gcc -Wall single_file.c service_ll.c \
> + `pkg-config fuse3 --cflags --libs` -o service_ll
> +
> +Install the binary, point ExecStart at it, install the .service and .socket
> +units into the systemd unit directory, then "systemctl daemon-reload" and
> +"systemctl start myfs.socket". From there the filesystem is mounted exactly as
> +described in README.service-mount.
> +
> +
> +See also
> +--------
> +
> + fuse_service.h the full fuse_service_* API reference (Doxygen)
> + README.service-mount installing and mounting a service filesystem
> + README.service-mount-flow the mount sequence through the code
> + example/service_ll.c, example/service_hl.c, example/single_file.c
> + systemd.service(5), systemd.socket(5), systemd.exec(5)
> diff --git a/doc/README.service-mount-flow b/doc/README.service-mount-flow
> new file mode 100644
> index 000000000000..bd5c4dee8b10
> --- /dev/null
> +++ b/doc/README.service-mount-flow
> @@ -0,0 +1,201 @@
> +How a service mount is set up
> +=============================
> +
> +How a fuse server, the libfuse service code, and the fuservicemount3 mount
> +helper set up a mount together: who talks to whom, over which transport, and
> +where each side checks what the other sent. README.service-mount-dev
> +describes the protocol and the API; this file follows the code.
> +
> +
> +Participants
> +------------
> +
> + - fuservicemount3 util/mount_service.c separate process, setuid
> + root, spawned by
> + mount.fuse3 or run by hand
> + - libfuse service code lib/fuse_service.c linked into the fuse server
> + - fuse server example/service_ll.c same process as the library
> +
> +The flow graph below puts the process boundary between its first two
> +columns. lib/fuse_service.c is compiled into the fuse server, so placing
> +fuservicemount3 between the other two would draw two boundaries where there
> +is one.
> +
> +Transports:
> +
> + - AF_UNIX SOCK_SEQPACKET between fuservicemount3 and lib/fuse_service.c.
> + fuservicemount3 calls connect(); the fuse server receives the already
> + connected socket as SD_LISTEN_FDS_START via systemd socket activation
> + (Accept=yes), so fuse_service_accept() never calls accept(2).
> + - SCM_RIGHTS on that socket for the argv memfd, /dev/fuse, and each file
> + the server asks for with "OPEN" or "BDEV".
> +
> +
> +Overview
> +--------
> +
> +fuservicemount3 runs as root and does everything that needs privilege. The
> +fuse server runs sandboxed and only asks. "Entry points" and "Flow" below show
> +the steps and the main checks.
> +
> +legend: ---> request, data or file descriptor; <--- reply or request back
> +
> + user: mount -t fuse.<subtype> <image> <mountpoint>
> + |
> + v
> + mount.fuse3 -> fuservicemount3 fuse server
> + (setuid root, trusted) (systemd service, sandboxed)
> + ----------------------------------------------- -----------------------------
> + connect /run/fuse/<subtype> -----------> systemd starts the server
> + hello, argv memfd, /dev/fuse -----------> fuse_service_accept()
> + parse the arguments
> + path named on the command line <----------- OPEN <image>
> + or listed in fuse.conf?
> + yes: open as the user, send fd -----------> keep the fd as backing store
> + no: EPERM fuse_service_finish_file_requests()
> + mount point on the command line? <----------- MNTP <mountpoint>
> + open it as the user and pin it
> + mount the fuse filesystem <----------- DOIT
> + exit <----------- BYEE
> + | serve FUSE requests from the
> + v kernel until umount
> + mount(8) returns
> +
> +
> +Entry points
> +------------
> +
> +Two binaries link the same mount_service.c (util/meson.build) and both funnel
> +into mount_service_main(). mount(8) only ever execs mount.fuse3;
> +fuservicemount3 is reached by direct invocation or because mount.fuse3
> +spawned it, which is also the only place a second process appears. The last
> +step below, mount_service_connect(), is where the socket to the fuse server
> +is created and connected.
> +
> + mount(8) -t fuse.<subtype> <source> <mountpoint> [-o opts]
> + | execs /sbin/mount.fuse3 user, directly:
> + | fuservicemount3 <source> <mountpoint> -t fuse.<subtype>
> + v v
> + /sbin/mount.fuse3 /sbin/fuservicemount3
> + util/mount.fuse.c main() util/fuservicemount.c main()
> + | strips "fuse." / "fuseblk." | also spawned by mount.fuse3
> + | no setuid=, no drop_privileges | when not root, see below
> + | -> try_service_main() |
> + v | exactly "-t FSTYPE --check"?
> + try_service_main() | exit 0/1, mounts nothing
> + no socket, no write access, or |
> + name too long -> FALLBACK_NEEDED |
> + +- getuid() != 0 |
> + | spawn fuservicemount3 ---+ a SECOND process starts here;
> + | fails -> FALLBACK_NEEDED | the parent waitpid()s
> + | | and returns its exit status
> + +- getuid() == 0 |
> + mount_service_main() +-> mount_service_main()
> + | |
> + +----------------+----------------+
> + v
> + mount_service_main() util/mount_service.c
> + read fuse.conf as the user
> + mount_service_init()
> + subtype from the fstype, reject a '/' in it
> + open the working directory as the user
> + mount_service_connect()
> + connect to /run/fuse/<subtype> as the user
> + path longer than sun_path -> exit failure
> + send buffer too small, no socket, or no
> + listener -> MOUNT_SERVICE_FALLBACK_NEEDED
> + |
> + v
> + a systemd .socket unit with Accept=yes is listening on
> + /run/fuse/<subtype>; it accepts the connection and spawns the
> + fuse server, handing it the connected fd as SD_LISTEN_FDS_START.
> + example/ carries units for the examples (null, service_ll, service_hl:
> + *.socket.in configured into *.socket, plus *@.service), none of which
> + meson installs. The socket directory comes from the meson option
> + service-socket-dir, built into FUSE_SERVICE_SOCKET_DIR (fuse_config.h).
> + |
> + v
> + continues in the three-column graph below
> +
> +MOUNT_SERVICE_FALLBACK_NEEDED on the mount.fuse3 path makes main() in
> +mount.fuse3 run the filesystem server program itself, as it did before
> +service mounts: /bin/sh -c "<subtype> [<source>] <mountpoint> [-o <options>]".
> +
> +
> +Flow
> +----
> +
> +The horizontal rule across the middle is the phase boundary. Above it the
> +server is blocked inside its single fuse_service_accept() call and
> +fuservicemount3 does the pushing (hello, argv memfd, /dev/fuse). Below it
> +that call has returned, the server drives every step, and each socket message
> +travels the other way: the server sends a command, fuservicemount3 replies.
> +
> +The server sends its "OPEN" and "BDEV" requests before
> +fuse_service_finish_file_requests(), and the mount commands from
> +fuse_service_session_mount() after it.
> +
> + fuservicemount3 (separate process) # lib/fuse_service.c | fuse server
> + util/mount_service.c # (linked into the server) | example/service_ll.c
> + setuid root, or mount.fuse3 as root # |
> +==========================================+============================================+========================
> + # one process |
> + mount_service_connect() done # systemd started the fuse server |
> + (see "Entry points" above) # with the connected fd |
> + # |
> + # | main()
> + # fuse_service_accept() <------------------+-- fuse_service_accept()
> + # check the socket fd from systemd |
> + # |
> + mount_service_send_hello() --------------+-> negotiate_hello() |
> + "SAFT": versions and flags # bad magic, version or flags -> error |
> + "LAST" hello reply <------------------+-- reply with the chosen version |
> + # |
> + mount_service_capture_args() # |
> + copy argv into a memfd # |
> + memfd larger than # |
> + FUSE_SERVICE_MAX_ARGV_SIZE # |
> + -> exit failure # |
> + mount_service_send_required_files() # |
> + "FILE" + argv memfd -------------------+-> fuse_service_receive_file(ARGV) |
> + "FILE" + /dev/fuse --------------------+-> fuse_service_receive_file(FUSEDEV) |
> +-- fuse_service_accept() returns ---------+-- traffic direction reverses --------------+-- server drives below --
> + main loop: while (running) # |
> + mount_service_receive_command() # fuse_service_append_args() <-------------+-- fuse_service_append_args()
> + command larger than # read the arguments from the memfd |
> + FUSE_SERVICE_MAX_CMD_SIZE # memfd larger than |
> + -> exit failure # FUSE_SERVICE_MAX_ARGV_SIZE, |
> + # argc 0 or more than the memfd holds, |
> + # argument longer than the memfd |
> + # -> -EBADMSG | fuse_opt_parse()
> + # fuse_service_parse_cmdline_opts() <------+-- fuse_service_parse_cmdline_opts()
> + # |
> + "OPEN" / "BDEV" <---------------------+-- fuse_service_request_file() <-----------+-- single_file_service_open()
> + not on the command line and not # or fuse_service_request_blockdev() |
> + listed in fuse.conf -> EPERM # |
> + open it as the user # |
> + "FILE" + fd, or -errno --------------+-> fuse_service_receive_file(path) |
> + # |
> + # fuse_service_finish_file_requests() <----+-- fuse_service_finish_file_requests()
> + # no more fds accepted | fuse_session_new()
> + # |
> + # fuse_service_session_mount() <-----------+-- fuse_service_session_mount()
> + "TYPE" <------------------------------+-- one command per mount parameter |
> + fuseblk, not root -> EPERM # |
> + "NAME" <------------------------------+-- |
> + "MNTP" <------------------------------+-- |
> + not on the command line -> EINVAL # |
> + open it as the user and pin it # |
> + "OPTS" <------------------------------+-- |
> + allow_other/allow_root, not root, # |
> + no user_allow_other -> EPERM # |
> + "MTAB" <------------------------------+-- |
> + "DOIT" <------------------------------+-- |
> + not root: limit mounts, reject # |
> + unsafe flags, check mount point # |
> + mount the fuse filesystem # |
> + "REPL" after each command -------------+-> error -> -errno to the caller |
> + # |
> + "BYEE" <------------------------------+-- fuse_service_send_goodbye() <-----------+-- fuse_service_send_goodbye(0)
> + exit # | fuse_session_loop[_mt]()
> + # | serves until umount
> diff --git a/doc/fuservicemount3.8 b/doc/fuservicemount3.8
> index 18e285c1ab29..fa2358f3cfed 100644
> --- a/doc/fuservicemount3.8
> +++ b/doc/fuservicemount3.8
> @@ -16,9 +16,17 @@ fuservicemount3 \- mount a FUSE filesystem that runs as a system socket service
>
> .SH DESCRIPTION
> Mount a filesystem using a FUSE server that runs as a socket service.
> -These servers can be contained using the platform's service management
> -framework.
> -
> +Unlike a traditional FUSE filesystem, which runs in the mount caller's
> +context, such a server runs as an independent, sandboxed systemd service.
> +\fBfuservicemount3\fP connects to the per-type service socket, hands the
> +running server the \fI/dev/fuse\fP device and any backing files it needs,
> +and performs the mount on its behalf. These servers can therefore be
> +contained using the platform's service management framework.
> +.PP
> +\fBfuservicemount3\fP is installed setuid root so that unprivileged users
> +can mount filesystems handled by a service. It is normally invoked
> +indirectly by \fBmount.fuse3\fP(8), not run directly.
> +.PP
> The FUSE server may ask fuservicemount3 to open files on its behalf.
> fuservicemount3 opens a path only in these cases:
> .IP \- 2
> @@ -34,15 +42,145 @@ A service_open_path line in /etc/fuse.conf lists the path for the filesystem
> type.
> .PP
> It refuses any other request with EPERM.
> -
> -The second form checks if there is a FUSE service available for the given
> -filesystem type.
> +.PP
> +The second form checks whether a FUSE service is available for the given
> +filesystem type, without mounting anything.
> +.SH FILESYSTEM REQUIREMENTS
> +This is not a transparent wrapper for arbitrary FUSE programs. Only a
> +filesystem whose server is written to the libfuse service API can be mounted
> +this way. A conventional server calls \fBfuse_main\fP(3), which opens
> +\fI/dev/fuse\fP and performs the mount itself. The service sandbox does not
> +permit this, so such a server cannot be used unmodified.
> +.PP
> +A service-capable server instead:
> +.IP \- 2
> +accepts the listening socket that systemd hands it, and receives its
> +arguments and the \fI/dev/fuse\fP descriptor over that socket, rather than
> +opening the device itself;
> +.IP \- 2
> +asks the helper to open any backing files or block devices on its behalf,
> +because its sandbox has no direct filesystem access; and
> +.IP \- 2
> +lets the helper perform the mount, staying in the foreground under systemd.
> +.PP
> +The server binary, its \fB@.service\fP unit, and its \fB.socket\fP unit must
> +all be installed before the type can be mounted. See \fBEXAMPLES\fP below, the
> +\fIservice_ll.c\fP and \fIservice_hl.c\fP example servers, the
> +\fI<fuse_service.h>\fP header, and the \fIREADME.service-mount-dev\fP document
> +for how to build one.
> +.SH OPTIONS
> +.TP
> +.B source
> +The filesystem source, passed on to the running server (for example a
> +backing device or file). May be empty.
> +.TP
> +.B mountpoint
> +Where to mount the filesystem.
> +.TP
> +.BI -t " fstype"
> +The filesystem type, of the form \fBfuse.\fIsubtype\fR or
> +\fBfuseblk.\fIsubtype\fR. The \fIsubtype\fR selects the service socket.
> +.TP
> +.BI -o " options"
> +Mount options to forward to the server.
> +.TP
> +.B --check
> +Only test whether a service socket exists for the type given with \fB-t\fP
> +and whether the calling user may connect to it; do not mount. Exit status is
> +zero if both hold, non-zero otherwise.
> +.SH FILES
> +.TP
> +.I /run/fuse/<subtype>
> +The default location of the per-filesystem service socket. The directory is
> +configurable at build time (meson option \fBservice-socket-dir\fP).
> +.SH EXAMPLES
> +A complete walk-through using the \fIservice_ll\fP example filesystem that
> +ships with libfuse.
> +.SS "What it is for"
> +\fIservice_ll\fP exports a single file or block device as a one-file
> +filesystem. Running it as a service keeps the server inside a systemd sandbox
> +\(em its own unprivileged user, private namespaces, and no capabilities \(em
> +while still letting a permitted user mount it with an ordinary \fBmount\fP
> +command. The privileged work (opening the backing device and performing the
> +mount) is done only by the setuid \fBfuservicemount3\fP helper. The
> +same recipe applies to any server written with the libfuse service API.
> +.SS "Setting up the service (administrator, once)"
> +The source for this example ships with the libfuse distribution in its
> +\fIexample\fP directory: \fIservice_ll.c\fP together with its helper
> +\fIsingle_file.c\fP make up the server, and \fIservice_ll@.service\fP and
> +\fIservice_ll.socket\fP are its systemd units. From that directory, build the
> +server and install the binary on the root filesystem:
> +.PP
> +.RS
> +.nf
> +gcc -Wall single_file.c service_ll.c $(pkg-config fuse3 --cflags --libs) -o service_ll
> +sudo install -m 0755 service_ll /usr/local/sbin/service_ll
> +.fi
> +.RE
> +.PP
> +libfuse provides two systemd units for this example: \fIservice_ll@.service\fP
> +(the sandboxed server) and \fIservice_ll.socket\fP (the activation socket,
> +already configured to listen at \fI/run/fuse/service_ll\fP). Edit the
> +service unit's \fBExecStart\fP to point at the installed binary:
> +.PP
> +.RS
> +.nf
> +ExecStart=/usr/local/sbin/service_ll
> +.fi
> +.RE
> +.PP
> +Install both units, reload systemd, and start the socket:
> +.PP
> +.RS
> +.nf
> +sudo cp service_ll@.service service_ll.socket /run/systemd/system/
> +sudo systemctl daemon-reload
> +sudo systemctl start service_ll.socket
> +.fi
> +.RE
> +.PP
> +Only the socket is running now; systemd starts a fresh, isolated server
> +instance on demand for each mount.
> +.SS "Mounting and using it"
> +Confirm a service is available for the type (this prints nothing; the exit
> +status is the answer):
> +.PP
> +.RS
> +.nf
> +fuservicemount3 -t fuse.service_ll --check && echo available
> +.fi
> +.RE
> +.PP
> +Mount it, passing the backing device or file as the source. Run this as root
> +or from an \fI/etc/fstab\fP entry that permits the mount:
> +.PP
> +.RS
> +.nf
> +mount -t fuse.service_ll /dev/sda /mnt
> +.fi
> +.RE
> +.PP
> +\fBmount.fuse3\fP(8) notices the service, \fBfuservicemount3\fP opens
> +\fI/dev/sda\fP and performs the mount, and the data appears under \fI/mnt\fP.
> +Unmount it like any other FUSE filesystem:
> +.PP
> +.RS
> +.nf
> +fusermount3 -u /mnt
> +.fi
> +.RE
> +.PP
> +For the full hardened unit files and further detail, see the
> +\fIservice_ll@.service\fP and \fIservice_ll.socket\fP files shipped with
> +libfuse and the \fIREADME.service-mount\fP document.
> .SH "AUTHORS"
> .LP
> The author of the fuse socket service code is Darrick J. Wong <djwong@kernel.org>.
> Debian GNU/Linux distribution.
> .SH SEE ALSO
> +.BR mount.fuse3 (8)
> .BR fusermount3 (1)
> .BR fusermount (1)
> .BR mount (8)
> .BR fuse (4)
> +.BR systemd.socket (5)
> diff --git a/doc/mainpage.dox b/doc/mainpage.dox
> index 36ba3bcba268..9de96e1410ab 100644
> --- a/doc/mainpage.dox
> +++ b/doc/mainpage.dox
> @@ -28,6 +28,19 @@ separate set of API functions.
> The high-level API that is primarily specified in fuse.h. The
> low-level API that is primarily documented in fuse_lowlevel.h.
>
> +## Running a filesystem as a systemd service ##
> +
> +A FUSE server can also be run as a sandboxed, socket-activated systemd
> +service rather than as a child of the mounting process. In this model the
> +server runs under its own unprivileged identity and in private namespaces,
> +while a small setuid helper (fuservicemount3) performs the mount on its
> +behalf. Servers use the service API in fuse_service.h; the service_hl.c and
> +service_ll.c examples show the high- and low-level variants.
> +
> +The README.service-mount and README.service-mount-dev files in the source
> +*doc* directory document this feature for administrators and filesystem
> +authors respectively.
> +
> ## Examples ##
>
> FUSE comes with several examples in the <a
> diff --git a/doc/mount.fuse3.8 b/doc/mount.fuse3.8
> index d55c96139d9f..b3c959aad63c 100644
> --- a/doc/mount.fuse3.8
> +++ b/doc/mount.fuse3.8
> @@ -231,6 +231,23 @@ Switch to \fBUSER\fP and its primary group before launching the FUSE file system
> \fBdrop_privileges\fP
> Perform setup of the FUSE file descriptor and mounting the file system before launching the FUSE file system process. \fBmount.fuse3\fP requires privilege to do so, i.e. must be run as root or at least with \fBCAP_SYS_ADMIN\fP and \fBCAP_SETPCAP\fP. It will launch the file system process fully unprivileged, i.e. without \fBcapabilities\fP(7) and \fBprctl\fP(2) flags set up such that privileges can't be reacquired (e.g. via setuid or fscaps binaries). This reduces risk in the event of the FUSE file system process getting compromised by malicious file system data. Because the file system program is launched after privileges have been dropped, it and the libraries it links against must reside at a path the unprivileged process can resolve: every directory component must be searchable without elevated privileges.
>
> +.SH SERVICE MOUNTS
> +If libfuse was built with service mount support, \fBmount.fuse3\fP can mount
> +filesystems whose server runs as a sandboxed systemd socket service instead of
> +as a child of the mounting process. When you mount a type \fBfuse.\fIsubtype\fR
> +(or \fBfuseblk.\fIsubtype\fR), \fBmount.fuse3\fP first checks for a service
> +socket for that subtype. If one exists, the mount is handed to
> +\fBfuservicemount3\fP(8) and performed by the already-running, isolated server;
> +all arguments except \fB-t\fP \fItype\fR are forwarded to it.
> +.PP
> +If no service socket exists, \fBmount.fuse3\fP transparently falls back to the
> +traditional behaviour and runs the filesystem server program directly, so
> +filesystems that are not set up as services are unaffected. Some options that
> +are incompatible with an already-running server (such as passing a pre-opened
> +FUSE file descriptor, or \fBsetuid=USER\fP) also force the traditional path.
> +.PP
> +See the libfuse \fIREADME.service-mount\fP document for details on installing
> +and using service-mounted filesystems.
> .SH FUSE MODULES (STACKING)
> Modules are filesystem stacking support to high level API. Filesystem modules can be built into libfuse or loaded from shared object
> .SS "iconv"
> @@ -276,5 +293,6 @@ Debian GNU/Linux distribution.
> .SH SEE ALSO
> .BR fusermount3 (1)
> .BR fusermount (1)
> +.BR fuservicemount3 (8)
> .BR mount (8)
> .BR fuse (4)
>
> --
> 2.53.0
>
>
>
^ permalink raw reply [flat|nested] 26+ messages in thread
* Re: [PATCH v2 08/14] test: check which files fuservicemount3 opens for the server
2026-09-28 11:02 ` [PATCH v2 08/14] test: check which files fuservicemount3 opens for the server Bernd Schubert via B4 Relay
@ 2026-09-29 3:52 ` Darrick J. Wong
0 siblings, 0 replies; 26+ messages in thread
From: Darrick J. Wong @ 2026-09-29 3:52 UTC (permalink / raw)
To: bernd; +Cc: fuse-devel, neal
On Mon, Sep 28, 2026 at 01:02:10PM +0200, Bernd Schubert via B4 Relay wrote:
> From: Bernd Schubert <bernd@bsbernd.com>
>
> No test covered the OPEN and OPEN_BDEV commands of fuservicemount3.
> test_service is a fuse server that sends one request and prints the
> errno it got back. socket_activate.py starts it the way a systemd
> socket unit does, so the tests do not depend on systemd.
>
> Assisted-by: LLM
> Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
Good basic test of the file opening capabilities :)
Reviewed-by: "Darrick J. Wong" <djwong@kernel.org>
--D
> ---
> .gitignore | 1 +
> test/cases/lib/service.sh | 83 ++++++++++++++++
> test/cases/lib/socket_activate.py | 42 ++++++++
> test/cases/mount/service-open-bound.sh | 61 ++++++++++++
> test/meson.build | 6 ++
> test/test_service.c | 176 +++++++++++++++++++++++++++++++++
> 6 files changed, 369 insertions(+)
>
> diff --git a/.gitignore b/.gitignore
> index 877c2fe2ba87..ebf9b7cdae47 100644
> --- a/.gitignore
> +++ b/.gitignore
> @@ -42,6 +42,7 @@ TAGS
> /test/test_setattr
> /test/test_api_30
> /test/test_fuser_conf
> +/test/test_service
> /build/
> # run-tests.py output, when pointed at the source tree with --run-dir
> /fuse-tests/
> diff --git a/test/cases/lib/service.sh b/test/cases/lib/service.sh
> new file mode 100644
> index 000000000000..d0a0fa0183d0
> --- /dev/null
> +++ b/test/cases/lib/service.sh
> @@ -0,0 +1,83 @@
> +# lib/service.sh - start a fuse service server the way a systemd socket unit
> +# does, for the fuservicemount3 cases.
> +#
> +# Sourced after common.sh. The socket has to be in the build-time socket
> +# directory, so every caller runs as root.
> +
> +# service_setup <subtype>
> +# Set service_sock. At exit remove the socket and any mount on $TEST_MNT.
> +service_setup()
> +{
> + service_subtype=$1
> + service_runs=0
> + # A case may set it, to add arguments to the helper command line
> + service_helper_args=()
> + service_sock=$("$FUSE_TEST_BIN_DIR/test_service" socket-path "$1")
> + # Every service script binds its own socket here; removing the
> + # directory would break another script's bind()
> + mkdir -p "$(dirname "$service_sock")"
> + _at_exit "rm -f '$service_sock'"
> + _at_exit "umount -l '$TEST_MNT' 2>/dev/null"
> +}
> +
> +# service_start <log> <program> [args...]
> +# Listen on service_sock and run <program> for the first connection, with its
> +# output in <log>. Sets service_pid.
> +service_start()
> +{
> + local log=$1; shift
> +
> + rm -f "$service_sock"
> + python3 "$TEST_LIB/socket_activate.py" "$service_sock" "$@" \
> + >"$log" 2>&1 &
> + service_pid=$!
> + _wait_for 10 "grep -q '^listening' '$log'" ||
> + _fail "$service_sock never listened"
> +}
> +
> +# service_wait_exit
> +# Reap the server and set service_rc to its exit status. A helper that never
> +# connected leaves the activator in accept(), which is a failure.
> +service_wait_exit()
> +{
> + _wait_for 10 "! kill -0 $service_pid 2>/dev/null" || {
> + kill "$service_pid" 2>/dev/null || true
> + _fail "server (pid $service_pid) did not exit"
> + }
> + service_rc=0
> + wait "$service_pid" || service_rc=$?
> +}
> +
> +# service_mount <source> <mountpoint> <case> [args...]
> +# Run fuservicemount3 once against test_service <case> [args...] and reap the
> +# server. Sets service_log.
> +service_mount()
> +{
> + local source=$1 mnt=$2; shift 2
> +
> + service_log=$TEST_LOGDIR/fs-$service_runs-$1.out
> + service_runs=$((service_runs + 1))
> + service_start "$service_log" "$FUSE_TEST_BIN_DIR/test_service" "$@"
> +
> + # Its exit status depends on the case; the server's line is the verdict.
> + "$FUSE_UTIL_DIR/fuservicemount3" "$source" "$mnt" \
> + -t "fuse.$service_subtype" "${service_helper_args[@]}" || true
> +
> + service_wait_exit
> +}
> +
> +# service_result <what>
> +# The last server prints one "<what> result: <value>" line, for example
> +# "request result: EPERM". Echo <value>; fail on no such line or several.
> +service_result()
> +{
> + local count
> +
> + count=$(grep -c "^$1 result: " "$service_log") || true
> + if [ "$count" != 1 ]; then
> + cat "$service_log" >&2
> + _fail "$service_log: $count \"$1 result:\" lines, want 1"
> + fi
> + # -n and p: print only the line the substitution matched
> + sed -n "s/^$1 result: //p" "$service_log"
> +}
> diff --git a/test/cases/lib/socket_activate.py b/test/cases/lib/socket_activate.py
> new file mode 100755
> index 000000000000..c33ca3a61ff2
> --- /dev/null
> +++ b/test/cases/lib/socket_activate.py
> @@ -0,0 +1,42 @@
> +#!/usr/bin/env python3
> +"""socket_activate.py <socket-path> <program> [args...]
> +
> +Listen on a SOCK_SEQPACKET socket, accept one connection and exec <program>
> +with it, the way systemd starts a socket unit with Accept=yes: the connection
> +is fd 3, LISTEN_FDS=1 and LISTEN_PID is the pid that runs <program>.
> +
> +Prints "listening" once a connect() can succeed.
> +"""
> +
> +import os
> +import socket
> +import sys
> +
> +SD_LISTEN_FDS_START = 3
> +
> +
> +def main():
> + if len(sys.argv) < 3:
> + sys.exit(__doc__)
> + path = sys.argv[1]
> + program = sys.argv[2:]
> +
> + listener = socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET)
> + listener.bind(path)
> + listener.listen(1)
> + print('listening', flush=True)
> +
> + conn, _ = listener.accept()
> + listener.close()
> +
> + # conn itself is close-on-exec and goes away with the exec
> + os.dup2(conn.fileno(), SD_LISTEN_FDS_START)
> + os.set_inheritable(SD_LISTEN_FDS_START, True)
> + os.environ['LISTEN_FDS'] = '1'
> + # exec keeps the pid
> + os.environ['LISTEN_PID'] = str(os.getpid())
> + os.execv(program[0], program)
> +
> +
> +if __name__ == '__main__':
> + main()
> diff --git a/test/cases/mount/service-open-bound.sh b/test/cases/mount/service-open-bound.sh
> new file mode 100755
> index 000000000000..3ef3690a42e9
> --- /dev/null
> +++ b/test/cases/mount/service-open-bound.sh
> @@ -0,0 +1,61 @@
> +#!/usr/bin/env bash
> +# GROUP: mount
> +#
> +# fuservicemount3 opens a file for the fuse server only if the path is on its
> +# command line.
> +
> +_fuse_no_mount_needed=1
> +. "$TEST_LIB/common.sh"
> +
> +_require_linux "fuservicemount3"
> +_require_root
> +_require_fuse_device
> +_require_binary util/fuservicemount3
> +_require_binary test/test_service
> +
> +. "$TEST_LIB/service.sh"
> +
> +# One socket per run, so a parallel run does not connect to this one
> +subtype=test-open-$$
> +img=$TEST_SRC/img
> +
> +touch "$img"
> +service_setup "$subtype"
> +
> +# service_open_request <case> <path> <expected errno name, or 0>
> +# Mount $img through fuservicemount3 with the server started for <case>.
> +service_open_request()
> +{
> + local case=$1 path=$2 expected=$3
> +
> + service_mount "$img" "$TEST_MNT" "$case" "$path"
> + _assert_eq "$(service_result request)" "$expected" "$case $path"
> +}
> +
> +# $img is on the command line
> +service_open_request open "$img" 0
> +# Root can read /etc/passwd, so EPERM comes from the command line check
> +service_open_request open /etc/passwd EPERM
> +# Passes the command line check, fails the block device check
> +service_open_request open-bdev "$img" ENOTBLK
> +# Not ENOTBLK: OPEN_BDEV gets the command line check first
> +service_open_request open-bdev /etc/passwd EPERM
> +
> +# A path in an option value, as the server's option parser splits it
> +journal=$TEST_SRC/journal,img
> +touch "$journal"
> +service_helper_args=(-o "ro,journal_dev=${journal//,/\\,}")
> +service_open_request open "$journal" 0
> +# The directory of an option value is not a path the user named
> +service_open_request open "$TEST_SRC" EPERM
> +service_helper_args=("-J$journal")
> +service_open_request open "$journal" 0
> +service_helper_args=()
> +
> +# After MNTPT the helper runs inside $TEST_MNT. A relative path must still
> +# resolve in the directory the helper started in.
> +cd "$TEST_SRC"
> +service_mount img "$TEST_MNT" open-after-mount img
> +cd "$OLDPWD"
> +_assert_eq "$(service_result request)" 0 "open-after-mount img"
> +umount "$TEST_MNT"
> diff --git a/test/meson.build b/test/meson.build
> index 68e083f7885c..ec48dab44a61 100644
> --- a/test/meson.build
> +++ b/test/meson.build
> @@ -54,6 +54,12 @@ if build_utils
> c_args: '-DFUSE_CONF="fuse.conf"',
> install: false)
> endif
> +if private_cfg.get('HAVE_SERVICEMOUNT', false)
> + td += executable('test_service', 'test_service.c',
> + include_directories: include_dirs,
> + link_with: [ libfuse ],
> + install: false)
> +endif
>
> if meson.is_subproject()
> # Skipped rather than run: the tests mount filesystems, which is not
> diff --git a/test/test_service.c b/test/test_service.c
> new file mode 100644
> index 000000000000..0d54df3427a9
> --- /dev/null
> +++ b/test/test_service.c
> @@ -0,0 +1,176 @@
> +/*
> + * FUSE: Filesystem in Userspace
> + *
> + * This program can be distributed under the terms of the GNU GPLv2.
> + * See the file GPL2.txt.
> + *
> + * A fuse service server for the service mount tests. Each mode takes one
> + * step against fuservicemount3 and prints the name of the errno that came
> + * back, 0 for success.
> + *
> + * test_service socket-path <subtype>
> + * test_service open <path>
> + * test_service open-bdev <path>
> + * test_service open-after-mount <path>
> + */
> +
> +#define FUSE_USE_VERSION FUSE_MAKE_VERSION(3, 19)
> +
> +/* strerrorname_np() */
> +#ifndef _GNU_SOURCE
> +#define _GNU_SOURCE
> +#endif
> +
> +#include "fuse_config.h"
> +#include <fuse_lowlevel.h>
> +#include <fuse_service.h>
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <string.h>
> +#include <fcntl.h>
> +#include <unistd.h>
> +#include <sys/stat.h>
> +
> +static const struct fuse_lowlevel_ops test_service_oper = { };
> +
> +/* @return "EPERM" and so on, "0" for no error */
> +static const char *errno_name(int error)
> +{
> + const char *name;
> +
> + if (!error)
> + return "0";
> +
> + name = strerrorname_np(error);
> + return name ? name : "unknown errno";
> +}
> +
> +/* The first non-option argument is the mount source, not the mountpoint */
> +static int skip_source(void *data, const char *arg, int key,
> + struct fuse_args *outargs)
> +{
> + bool *source_seen = data;
> +
> + (void)arg;
> + (void)outargs;
> +
> + if (key == FUSE_OPT_KEY_NONOPT && !*source_seen) {
> + *source_seen = true;
> + return 0;
> + }
> + return 1;
> +}
> +
> +/*
> + * Mount through the helper so that it has a mount point when the file is
> + * requested.
> + *
> + * @return the mounted session, or NULL on failure
> + */
> +static struct fuse_session *session_mounted(struct fuse_service *service,
> + const char *argv0)
> +{
> + struct fuse_args args = FUSE_ARGS_INIT(0, NULL);
> + struct fuse_cmdline_opts opts = { };
> + struct fuse_session *se = NULL;
> + bool source_seen = false;
> +
> + if (fuse_opt_add_arg(&args, argv0) ||
> + fuse_service_append_args(service, &args) ||
> + fuse_opt_parse(&args, &source_seen, NULL, skip_source) ||
> + fuse_service_parse_cmdline_opts(&args, &opts))
> + goto out;
> +
> + se = fuse_session_new(&args, &test_service_oper,
> + sizeof(test_service_oper), NULL);
> + if (!se)
> + goto out;
> +
> + if (fuse_service_session_mount(service, se, S_IFDIR, &opts)) {
> + fuse_session_destroy(se);
> + se = NULL;
> + }
> +
> +out:
> + free(opts.mountpoint);
> + fuse_opt_free_args(&args);
> + return se;
> +}
> +
> +/* @return 0 when the result was printed, negative errno otherwise */
> +static int request_printed(const struct fuse_service *service,
> + const char *path, bool blockdev)
> +{
> + int fd;
> + int ret;
> +
> + if (blockdev)
> + ret = fuse_service_request_blockdev(service, path, O_RDONLY,
> + 0, 0, 0);
> + else
> + ret = fuse_service_request_file(service, path, O_RDONLY, 0, 0);
> + if (ret)
> + return ret;
> +
> + /* A refusal by the helper is a success return, with -errno in fd */
> + ret = fuse_service_receive_file(service, path, &fd);
> + if (ret)
> + return ret;
> +
> + if (fd >= 0) {
> + close(fd);
> + printf("request result: 0\n");
> + } else {
> + printf("request result: %s\n", errno_name(-fd));
> + }
> + fflush(stdout);
> + return 0;
> +}
> +
> +int main(int argc, char *argv[])
> +{
> + struct fuse_service *service = NULL;
> + struct fuse_session *se = NULL;
> + bool blockdev = false;
> + int ret = 1;
> +
> + if (argc != 3) {
> + fprintf(stderr, "usage: %s socket-path <subtype>\n", argv[0]);
> + fprintf(stderr, " %s open|open-bdev|open-after-mount <path>\n",
> + argv[0]);
> + return 1;
> + }
> +
> + if (!strcmp(argv[1], "socket-path")) {
> + printf("%s/%s\n", FUSE_SERVICE_SOCKET_DIR, argv[2]);
> + return 0;
> + }
> +
> + if (fuse_service_accept(&service) || !fuse_service_accepted(service)) {
> + fprintf(stderr, "%s: not started as a fuse service\n", argv[0]);
> + return 1;
> + }
> +
> + if (!strcmp(argv[1], "open-after-mount")) {
> + se = session_mounted(service, argv[0]);
> + if (!se)
> + goto out;
> + } else if (!strcmp(argv[1], "open-bdev")) {
> + blockdev = true;
> + } else if (strcmp(argv[1], "open")) {
> + fprintf(stderr, "%s: unknown case %s\n", argv[0], argv[1]);
> + goto out;
> + }
> +
> + if (request_printed(service, argv[2], blockdev))
> + goto out;
> +
> + ret = 0;
> +out:
> + fuse_service_send_goodbye(service, ret);
> + fuse_service_destroy(&service);
> + /* Closes /dev/fuse; the test script unmounts */
> + if (se)
> + fuse_session_destroy(se);
> + return ret;
> +}
>
> --
> 2.53.0
>
>
>
^ permalink raw reply [flat|nested] 26+ messages in thread
* Re: [PATCH v2 09/14] test: check what fuservicemount3 refuses
2026-09-28 11:02 ` [PATCH v2 09/14] test: check what fuservicemount3 refuses Bernd Schubert via B4 Relay
@ 2026-09-29 3:55 ` Darrick J. Wong
0 siblings, 0 replies; 26+ messages in thread
From: Darrick J. Wong @ 2026-09-29 3:55 UTC (permalink / raw)
To: bernd; +Cc: fuse-devel, neal
On Mon, Sep 28, 2026 at 01:02:11PM +0200, Bernd Schubert via B4 Relay wrote:
> From: Bernd Schubert <bernd@bsbernd.com>
>
> fuservicemount3 runs setuid root and acts on requests from a fuse
> server it does not trust. No test covered its checks on the subtype,
> the mount point and its file type, fuseblk for a user who is not root,
> or a server that exits before its goodbye.
>
> Assisted-by: LLM
> Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
Seems reasonable to me,
Reviewed-by: "Darrick J. Wong" <djwong@kernel.org>
--D
> ---
> test/cases/lib/service.sh | 20 ++++--
> test/cases/mount/service-caps.sh | 20 ++++++
> test/cases/mount/service-check.sh | 38 ++++++++++++
> test/cases/mount/service-mountpoint.sh | 36 +++++++++++
> test/cases/mount/service-nonroot.sh | 54 +++++++++++++++++
> test/cases/mount/service-server-exit.sh | 22 +++++++
> test/test_service.c | 104 ++++++++++++++++++++++++--------
> 7 files changed, 266 insertions(+), 28 deletions(-)
>
> diff --git a/test/cases/lib/service.sh b/test/cases/lib/service.sh
> index d0a0fa0183d0..60c6c72820ef 100644
> --- a/test/cases/lib/service.sh
> +++ b/test/cases/lib/service.sh
> @@ -10,6 +10,8 @@ service_setup()
> {
> service_subtype=$1
> service_runs=0
> + # A case may prefix it, to run the helper as another user
> + service_helper=("$FUSE_UTIL_DIR/fuservicemount3")
> # A case may set it, to add arguments to the helper command line
> service_helper_args=()
> service_sock=$("$FUSE_TEST_BIN_DIR/test_service" socket-path "$1")
> @@ -33,6 +35,16 @@ service_start()
> service_pid=$!
> _wait_for 10 "grep -q '^listening' '$log'" ||
> _fail "$service_sock never listened"
> + # connect() needs write permission, and a case may run as another user
> + chmod 0666 "$service_sock"
> +}
> +
> +# service_stop
> +# Kill an activator that no helper connected to.
> +service_stop()
> +{
> + kill "$service_pid" 2>/dev/null || true
> + wait "$service_pid" 2>/dev/null || true
> }
>
> # service_wait_exit
> @@ -50,7 +62,7 @@ service_wait_exit()
>
> # service_mount <source> <mountpoint> <case> [args...]
> # Run fuservicemount3 once against test_service <case> [args...] and reap the
> -# server. Sets service_log.
> +# server. Sets service_log and service_helper_rc.
> service_mount()
> {
> local source=$1 mnt=$2; shift 2
> @@ -59,9 +71,9 @@ service_mount()
> service_runs=$((service_runs + 1))
> service_start "$service_log" "$FUSE_TEST_BIN_DIR/test_service" "$@"
>
> - # Its exit status depends on the case; the server's line is the verdict.
> - "$FUSE_UTIL_DIR/fuservicemount3" "$source" "$mnt" \
> - -t "fuse.$service_subtype" "${service_helper_args[@]}" || true
> + service_helper_rc=0
> + "${service_helper[@]}" "$source" "$mnt" -t "fuse.$service_subtype" \
> + "${service_helper_args[@]}" || service_helper_rc=$?
>
> service_wait_exit
> }
> diff --git a/test/cases/mount/service-caps.sh b/test/cases/mount/service-caps.sh
> new file mode 100755
> index 000000000000..69bc2bdbef8d
> --- /dev/null
> +++ b/test/cases/mount/service-caps.sh
> @@ -0,0 +1,20 @@
> +#!/usr/bin/env bash
> +# GROUP: mount
> +#
> +# fuservicemount3 run by root offers the fuse server allow_other and fuseblk.
> +
> +_fuse_no_mount_needed=1
> +. "$TEST_LIB/common.sh"
> +
> +_require_linux "fuservicemount3"
> +_require_root
> +_require_fuse_device
> +_require_binary util/fuservicemount3
> +_require_binary test/test_service
> +
> +. "$TEST_LIB/service.sh"
> +
> +service_setup "test-caps-$$"
> +
> +service_mount "$service_subtype" "$TEST_MNT" caps
> +_assert_eq "$(service_result caps)" "allow_other=1 fuseblk=1" "caps as root"
> diff --git a/test/cases/mount/service-check.sh b/test/cases/mount/service-check.sh
> new file mode 100755
> index 000000000000..ec30031d039e
> --- /dev/null
> +++ b/test/cases/mount/service-check.sh
> @@ -0,0 +1,38 @@
> +#!/usr/bin/env bash
> +# GROUP: mount
> +#
> +# fuservicemount3 --check succeeds only for a socket named after the subtype,
> +# and never for a subtype that is a path.
> +
> +_fuse_no_mount_needed=1
> +. "$TEST_LIB/common.sh"
> +
> +_require_linux "fuservicemount3"
> +_require_root
> +_require_binary util/fuservicemount3
> +_require_binary test/test_service
> +
> +. "$TEST_LIB/service.sh"
> +
> +subtype=test-check-$$
> +service_setup "$subtype"
> +
> +# check_rc <fstype>
> +check_rc()
> +{
> + local rc=0
> +
> + "$FUSE_UTIL_DIR/fuservicemount3" -t "$1" --check || rc=$?
> + echo "$rc"
> +}
> +
> +_assert_eq "$(check_rc "fuse.$subtype")" 1 "no socket"
> +
> +touch "$service_sock"
> +_assert_eq "$(check_rc "fuse.$subtype")" 1 "regular file"
> +
> +service_start "$TEST_LOGDIR/fs-check.out" "$FUSE_TEST_BIN_DIR/test_service" caps
> +_assert_eq "$(check_rc "fuse.$subtype")" 0 "listening socket"
> +# The same socket, named through a path
> +_assert_eq "$(check_rc "fuse../$subtype")" 1 "subtype ./$subtype"
> +service_stop
> diff --git a/test/cases/mount/service-mountpoint.sh b/test/cases/mount/service-mountpoint.sh
> new file mode 100755
> index 000000000000..0df4733389e0
> --- /dev/null
> +++ b/test/cases/mount/service-mountpoint.sh
> @@ -0,0 +1,36 @@
> +#!/usr/bin/env bash
> +# GROUP: mount
> +#
> +# The fuse server names the mount point, so fuservicemount3 has to refuse one
> +# that is not on its command line, and one of the wrong file type.
> +
> +_fuse_no_mount_needed=1
> +. "$TEST_LIB/common.sh"
> +
> +_require_linux "fuservicemount3"
> +_require_root
> +_require_fuse_device
> +_require_binary util/fuservicemount3
> +_require_binary test/test_service
> +
> +. "$TEST_LIB/service.sh"
> +
> +file=$TEST_SRC/file
> +
> +touch "$file"
> +service_setup "test-mntpt-$$"
> +
> +service_mount "$service_subtype" "$TEST_MNT" mount dir
> +_assert_eq "$(service_result mount)" 0 "mount dir on a directory"
> +_assert_fstype "$TEST_MNT" "fuse.$service_subtype" fuse
> +umount "$TEST_MNT"
> +
> +service_mount "$service_subtype" "$TEST_MNT" mount-elsewhere "$TEST_SRC"
> +_assert_eq "$(service_result mount)" EINVAL \
> + "mount point not on the command line"
> +
> +service_mount "$service_subtype" "$TEST_MNT" mount file
> +_assert_eq "$(service_result mount)" EISDIR "mount file on a directory"
> +
> +service_mount "$service_subtype" "$file" mount dir
> +_assert_eq "$(service_result mount)" ENOTDIR "mount dir on a regular file"
> diff --git a/test/cases/mount/service-nonroot.sh b/test/cases/mount/service-nonroot.sh
> new file mode 100755
> index 000000000000..0c211d0a9965
> --- /dev/null
> +++ b/test/cases/mount/service-nonroot.sh
> @@ -0,0 +1,54 @@
> +#!/usr/bin/env bash
> +# GROUP: mount
> +#
> +# fuservicemount3 installed setuid and run by an unprivileged user mounts only
> +# on a directory that user can write, and never offers fuseblk.
> +
> +_fuse_no_mount_needed=1
> +. "$TEST_LIB/common.sh"
> +
> +_require_linux "fuservicemount3"
> +# Root installs the setuid copy and the socket
> +_require_root
> +_require_fuse_device
> +_require_binary util/fuservicemount3
> +_require_binary test/test_service
> +_require_prog setpriv
> +_require_prog findmnt
> +
> +user=nobody
> +uid=$(id -u "$user") || _notrun "no user $user"
> +gid=$(id -g "$user")
> +run_as=(setpriv --reuid="$uid" --regid="$gid" --clear-groups)
> +
> +helper=$TEST_WORKDIR/fuservicemount3
> +case ",$(findmnt -n -o OPTIONS -T "$TEST_WORKDIR")," in
> +*,nosuid,*) _notrun "$TEST_WORKDIR is on a nosuid mount" ;;
> +esac
> +cp "$FUSE_UTIL_DIR/fuservicemount3" "$helper"
> +_at_exit "rm -f '$helper'"
> +chmod 4755 "$helper"
> +"${run_as[@]}" test -x "$helper" || _notrun "$user cannot reach $helper"
> +
> +. "$TEST_LIB/service.sh"
> +
> +service_setup "test-nonroot-$$"
> +service_helper=("${run_as[@]}" "$helper")
> +root_dir=$TEST_WORKDIR/root-mnt
> +mkdir -m 0755 "$root_dir"
> +
> +chown "$uid" "$TEST_MNT"
> +service_mount "$service_subtype" "$TEST_MNT" mount dir
> +_assert_eq "$(service_result mount)" 0 "mount on a directory $user owns"
> +umount "$TEST_MNT"
> +
> +service_mount "$service_subtype" "$root_dir" mount dir
> +_assert_eq "$(service_result mount)" EPERM \
> + "mount on a directory owned by root"
> +
> +# allow_other depends on user_allow_other in the system fuse.conf
> +service_mount "$service_subtype" "$TEST_MNT" caps
> +case $(service_result caps) in
> +*" fuseblk=0") ;;
> +*) _fail "caps as $user: $(service_result caps)" ;;
> +esac
> diff --git a/test/cases/mount/service-server-exit.sh b/test/cases/mount/service-server-exit.sh
> new file mode 100755
> index 000000000000..6304f20ff2cb
> --- /dev/null
> +++ b/test/cases/mount/service-server-exit.sh
> @@ -0,0 +1,22 @@
> +#!/usr/bin/env bash
> +# GROUP: mount
> +#
> +# A fuse server that exits without a goodbye makes fuservicemount3 fail, and
> +# leaves nothing mounted.
> +
> +_fuse_no_mount_needed=1
> +. "$TEST_LIB/common.sh"
> +
> +_require_linux "fuservicemount3"
> +_require_root
> +_require_fuse_device
> +_require_binary util/fuservicemount3
> +_require_binary test/test_service
> +
> +. "$TEST_LIB/service.sh"
> +
> +service_setup "test-exit-$$"
> +
> +service_mount "$service_subtype" "$TEST_MNT" exit-early
> +_assert_ne "$service_helper_rc" 0 "fuservicemount3 exit status"
> +_assert_eq "$(mountinfo_field "$TEST_MNT" fstype)" "" "$TEST_MNT mounted"
> diff --git a/test/test_service.c b/test/test_service.c
> index 0d54df3427a9..7bbd14e2650d 100644
> --- a/test/test_service.c
> +++ b/test/test_service.c
> @@ -12,6 +12,10 @@
> * test_service open <path>
> * test_service open-bdev <path>
> * test_service open-after-mount <path>
> + * test_service mount dir|file
> + * test_service mount-elsewhere <mountpoint>
> + * test_service caps
> + * test_service exit-early
> */
>
> #define FUSE_USE_VERSION FUSE_MAKE_VERSION(3, 19)
> @@ -62,18 +66,25 @@ static int skip_source(void *data, const char *arg, int key,
> }
>
> /*
> - * Mount through the helper so that it has a mount point when the file is
> - * requested.
> + * Mount through the helper. On success *sep is the mounted session, which
> + * keeps /dev/fuse open until it is destroyed.
> *
> - * @return the mounted session, or NULL on failure
> + * @param fmt mount point type the helper has to find
> + * @param mountpoint sent in place of the one on the command line, or NULL
> + * @return 0 when the result was printed, -1 otherwise
> */
> -static struct fuse_session *session_mounted(struct fuse_service *service,
> - const char *argv0)
> +static int mount_printed(struct fuse_service *service, const char *argv0,
> + mode_t fmt, const char *mountpoint,
> + struct fuse_session **sep)
> {
> struct fuse_args args = FUSE_ARGS_INIT(0, NULL);
> struct fuse_cmdline_opts opts = { };
> - struct fuse_session *se = NULL;
> + struct fuse_session *se;
> bool source_seen = false;
> + int printed = -1;
> + int ret;
> +
> + *sep = NULL;
>
> if (fuse_opt_add_arg(&args, argv0) ||
> fuse_service_append_args(service, &args) ||
> @@ -81,20 +92,30 @@ static struct fuse_session *session_mounted(struct fuse_service *service,
> fuse_service_parse_cmdline_opts(&args, &opts))
> goto out;
>
> + if (mountpoint) {
> + free(opts.mountpoint);
> + opts.mountpoint = strdup(mountpoint);
> + if (!opts.mountpoint)
> + goto out;
> + }
> +
> se = fuse_session_new(&args, &test_service_oper,
> sizeof(test_service_oper), NULL);
> if (!se)
> goto out;
>
> - if (fuse_service_session_mount(service, se, S_IFDIR, &opts)) {
> + ret = fuse_service_session_mount(service, se, fmt, &opts);
> + if (ret)
> fuse_session_destroy(se);
> - se = NULL;
> - }
> + else
> + *sep = se;
>
> + printf("mount result: %s\n", errno_name(-ret));
> + printed = 0;
> out:
> free(opts.mountpoint);
> fuse_opt_free_args(&args);
> - return se;
> + return printed;
> }
>
> /* @return 0 when the result was printed, negative errno otherwise */
> @@ -127,22 +148,40 @@ static int request_printed(const struct fuse_service *service,
> return 0;
> }
>
> +/* @return S_IFDIR or S_IFREG, 0 for an unknown name */
> +static mode_t mount_format(const char *name)
> +{
> + if (!strcmp(name, "dir"))
> + return S_IFDIR;
> + if (!strcmp(name, "file"))
> + return S_IFREG;
> + return 0;
> +}
> +
> int main(int argc, char *argv[])
> {
> struct fuse_service *service = NULL;
> struct fuse_session *se = NULL;
> - bool blockdev = false;
> + const char *mode;
> + const char *arg;
> int ret = 1;
>
> - if (argc != 3) {
> + if (argc != 2 && argc != 3) {
> fprintf(stderr, "usage: %s socket-path <subtype>\n", argv[0]);
> fprintf(stderr, " %s open|open-bdev|open-after-mount <path>\n",
> argv[0]);
> + fprintf(stderr, " %s mount dir|file\n", argv[0]);
> + fprintf(stderr, " %s mount-elsewhere <mountpoint>\n",
> + argv[0]);
> + fprintf(stderr, " %s caps|exit-early\n", argv[0]);
> return 1;
> }
> + mode = argv[1];
> + /* argv[argc] is NULL */
> + arg = argv[2];
>
> - if (!strcmp(argv[1], "socket-path")) {
> - printf("%s/%s\n", FUSE_SERVICE_SOCKET_DIR, argv[2]);
> + if (!strcmp(mode, "socket-path") && arg) {
> + printf("%s/%s\n", FUSE_SERVICE_SOCKET_DIR, arg);
> return 0;
> }
>
> @@ -151,19 +190,36 @@ int main(int argc, char *argv[])
> return 1;
> }
>
> - if (!strcmp(argv[1], "open-after-mount")) {
> - se = session_mounted(service, argv[0]);
> - if (!se)
> - goto out;
> - } else if (!strcmp(argv[1], "open-bdev")) {
> - blockdev = true;
> - } else if (strcmp(argv[1], "open")) {
> - fprintf(stderr, "%s: unknown case %s\n", argv[0], argv[1]);
> - goto out;
> + if (!strcmp(mode, "exit-early")) {
> + /* No goodbye, the helper only sees the connection close */
> + fuse_service_destroy(&service);
> + return 0;
> }
>
> - if (request_printed(service, argv[2], blockdev))
> + if (!strcmp(mode, "caps")) {
> + printf("caps result: allow_other=%d fuseblk=%d\n",
> + fuse_service_can_allow_other(service),
> + fuse_service_can_fuseblk(service));
> + } else if (!strcmp(mode, "mount") && arg && mount_format(arg)) {
> + if (mount_printed(service, argv[0], mount_format(arg), NULL,
> + &se))
> + goto out;
> + } else if (!strcmp(mode, "mount-elsewhere") && arg) {
> + if (mount_printed(service, argv[0], S_IFDIR, arg, &se))
> + goto out;
> + } else if (!strcmp(mode, "open-after-mount") && arg) {
> + if (mount_printed(service, argv[0], S_IFDIR, NULL, &se) || !se)
> + goto out;
> + if (request_printed(service, arg, false))
> + goto out;
> + } else if ((!strcmp(mode, "open") || !strcmp(mode, "open-bdev")) &&
> + arg) {
> + if (request_printed(service, arg, !strcmp(mode, "open-bdev")))
> + goto out;
> + } else {
> + fprintf(stderr, "%s: unknown case %s\n", argv[0], mode);
> goto out;
> + }
>
> ret = 0;
> out:
>
> --
> 2.53.0
>
>
>
^ permalink raw reply [flat|nested] 26+ messages in thread
* Re: [PATCH v2 02/14] mount_service: warn about paths not named on the command line
2026-09-29 2:10 ` Darrick J. Wong
@ 2026-09-30 11:06 ` Bernd Schubert
0 siblings, 0 replies; 26+ messages in thread
From: Bernd Schubert @ 2026-09-30 11:06 UTC (permalink / raw)
To: Darrick J. Wong; +Cc: fuse-devel, neal
On 9/29/26 04:10, Darrick J. Wong wrote:
> On Mon, Sep 28, 2026 at 01:02:04PM +0200, Bernd Schubert via B4 Relay wrote:
>> From: Bernd Schubert <bernd@bsbernd.com>
>>
>> In a service mount, the fuse server runs as a systemd service in a
>> sandbox that has no access to the user's files. The user runs mount,
>> which starts fuservicemount3, a setuid-root helper. The fuse server
>> sends requests to the helper over a socket. With an OPEN request, the
>> server asks the helper to open its backing file, for example the disk
>> image named on the mount command line. The helper opens the file with
>> the user's credentials and passes the file descriptor to the server.
>> fusermount3 opens nothing for the fuse server except /dev/fuse; the
>> server runs as the user and opens its own files.
>>
>> The helper opens any path the server sends. An attacker who controlled
>> the server could use this to read every file the user can read, for
>> example ~/.ssh/id_ed25519, and the sandbox does not prevent it. The
>> helper now prints a warning if the user did not name the path when
>> mounting: as a whole argument, as the value of a name=value option, or
>> glued to a short option as in "-J/dev/sdb1". The helper splits the
>> options with fuse_opt_parse(), as the fuse server does, so both see the
>> same option values. The helper still opens the path, because a server
>> can take a path in a form that none of these checks recognizes.
>>
>> Enforced permissions follow up in the next commit.
>>
>> Assisted-by: LLM
>> Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
>
> I wonder if there are any fuse servers out there that take parameters
> like:
>
> -o bdevs=/dev/sda:/dev/sdb,otheroption=whatever
>
> but ... let's let them come out of the woodwork?
Yeah, I think we can handle that later on based on needs.
>
> I think this is a good addition :)
> Reviewed-by: "Darrick J. Wong" <djwong@kernel.org>
Thanks a lot for your reviews!
Thanks,
Bernd
^ permalink raw reply [flat|nested] 26+ messages in thread
* Re: [PATCH v2 03/14] mount_service: refuse paths the user did not name
2026-09-29 2:26 ` Darrick J. Wong
@ 2026-09-30 11:46 ` Bernd Schubert
0 siblings, 0 replies; 26+ messages in thread
From: Bernd Schubert @ 2026-09-30 11:46 UTC (permalink / raw)
To: Darrick J. Wong; +Cc: fuse-devel, neal
On 9/29/26 04:26, Darrick J. Wong wrote:
> On Mon, Sep 28, 2026 at 01:02:05PM +0200, Bernd Schubert via B4 Relay wrote:
>> From: Bernd Schubert <bernd@bsbernd.com>
>>
>> fuservicemount3 warns about a path that the user did not name on the
>> command line, but still opens it. A server can take a path in a form
>> that the helper cannot split, for example "-journal/dev/sdb1" or its
>> own option syntax. The administrator can now list such paths in
>> /etc/fuse.conf, per filesystem type:
>>
>> service_open_path = ext4 /dev/sd*
>>
>> The pattern is matched with fnmatch() and FNM_PATHNAME, so "*" does not
>> match "/". A requested path with a "." or ".." component never matches,
>> because "*" matches "..", and "/dev/*" would then open "/". The helper
>> now refuses any other path with EPERM.
>>
>> Assisted-by: LLM
>> Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
>> ---
>> doc/fuservicemount3.8 | 16 ++++++++
>> doc/mount.fuse3.8 | 7 ++++
>> include/fuse_service.h | 4 +-
>> test/test_fuser_conf.c | 55 ++++++++++++++++++++++++++++
>> util/fuse.conf | 11 ++++++
>> util/fuser_conf.c | 99 ++++++++++++++++++++++++++++++++++++++++++++++++++
>> util/fuser_conf.h | 3 ++
>> util/mount_service.c | 13 ++++---
>> 8 files changed, 202 insertions(+), 6 deletions(-)
>>
>> diff --git a/doc/fuservicemount3.8 b/doc/fuservicemount3.8
>> index aa2167cb4872..18e285c1ab29 100644
>> --- a/doc/fuservicemount3.8
>> +++ b/doc/fuservicemount3.8
>> @@ -19,6 +19,22 @@ Mount a filesystem using a FUSE server that runs as a socket service.
>> These servers can be contained using the platform's service management
>> framework.
>>
>> +The FUSE server may ask fuservicemount3 to open files on its behalf.
>> +fuservicemount3 opens a path only in these cases:
>> +.IP \- 2
>> +The path is a command line argument, for example /srv/disk.img.
>> +.IP \- 2
>> +The path is the value in a key=value option, for example /dev/sdb1 in
>> +"-o journal_dev=/dev/sdb1".
>> +.IP \- 2
>> +The path directly follows a short option, for example /dev/sdb1 in
>> +"-J/dev/sdb1".
>> +.IP \- 2
>> +A service_open_path line in /etc/fuse.conf lists the path for the filesystem
>> +type.
>> +.PP
>> +It refuses any other request with EPERM.
>> +
>> The second form checks if there is a FUSE service available for the given
>> filesystem type.
>> .SH "AUTHORS"
>> diff --git a/doc/mount.fuse3.8 b/doc/mount.fuse3.8
>> index 2e587458a06e..d55c96139d9f 100644
>> --- a/doc/mount.fuse3.8
>> +++ b/doc/mount.fuse3.8
>> @@ -38,6 +38,13 @@ Allow non-root users to specify the \fBallow_other\fP or
>> \fBallow_root\fP mount options (see below).
>> .TP
>> These limits are enforced by the \fBfusermount3\fP helper, so they can be avoided by filesystems that run as root.
>> +.TP
>> +\fBservice_open_path = SUBTYPE PATTERN\fP
>> +Allow \fBfuservicemount3\fP(8) to open paths that match \fIPATTERN\fP for the
>> +server of a service mount of type \fBfuse.\fISUBTYPE\fR, in addition to the
>> +paths on the mount command line. \fIPATTERN\fP is an absolute path in which "*"
>> +does not match "/". A pattern that matches a directory gives the server every
>> +file below it. The line can be repeated.
>> .SH OPTIONS
>> Most of the generic mount options described in \fBmount\fP are
>> supported (\fBro\fP, \fBrw\fP, \fBsuid\fP, \fBnosuid\fP, \fBdev\fP,
>> diff --git a/include/fuse_service.h b/include/fuse_service.h
>> index d6aedea8f0f8..2114e7772bf5 100644
>> --- a/include/fuse_service.h
>> +++ b/include/fuse_service.h
>> @@ -139,6 +139,8 @@ int fuse_service_parse_cmdline_opts(struct fuse_args *args,
>>
>> /**
>> * Ask the mount.service helper to open a file on behalf of the fuse server.
>> + * The helper refuses a path that the mount command line does not name and
>> + * fuse.conf does not list; fuse_service_receive_file() then reports -EPERM.
>> *
>> * @param sf service context
>> * @param path the path to file
>> @@ -153,7 +155,7 @@ int fuse_service_request_file(const struct fuse_service *sf, const char *path,
>>
>> /**
>> * Ask the mount.service helper to open a block device on behalf of the fuse
>> - * server.
>> + * server. The helper refuses the same paths as for a file request.
>> *
>> * @param sf service context
>> * @param path the path to file
>> diff --git a/test/test_fuser_conf.c b/test/test_fuser_conf.c
>> index 4d974931cf58..6d95fe932039 100644
>> --- a/test/test_fuser_conf.c
>> +++ b/test/test_fuser_conf.c
>> @@ -105,6 +105,59 @@ static int test_trimmed_options(void)
>> return 0;
>> }
>>
>> +static int test_service_open_path(void)
>> +{
>> + const char *test = "service_open_path";
>> +
>> + if (write_conf("service_open_path = ext4 /dev/sd*\n"
>> + "service_open_path = ext4 /dev/nvme*\n"
>> + "service_open_path = ext4 relative/path\n"
>> + "service_open_path = xfs\t/srv/xfs.img\n"
>> + "service_open_path = xfs /srv/img/*\n"
>> + " \tservice_open_path = ext4 /srv/indented.img\n"
>> + "service_open_path =\x20\n"
>> + "service_open_path = ext4\n") == -1)
>> + return fail(test, "could not write the config file");
>> +
>> + read_conf(progname);
>> +
>> + if (!service_open_path_listed("ext4", "/dev/sda"))
>> + return fail(test, "/dev/sd* did not match /dev/sda");
>> + if (!service_open_path_listed("ext4", "/dev/nvme0n1"))
>> + return fail(test, "a second ext4 line was not recognised");
>> + if (!service_open_path_listed("ext4", "/srv/indented.img"))
>> + return fail(test, "an indented line was not recognised");
>> + if (service_open_path_listed("ext4", "/dev/sda/x"))
>> + return fail(test, "* matched a /");
>> + if (service_open_path_listed("xfs", "/dev/sda"))
>> + return fail(test, "an ext4 line matched for xfs");
>> + if (service_open_path_listed("ext4", "relative/path"))
>> + return fail(test, "a relative pattern was accepted");
>> + if (!service_open_path_listed("xfs", "/srv/xfs.img"))
>> + return fail(test, "a tab-separated line was not recognised");
>> + if (!service_open_path_listed("xfs", "/srv/img/a.img"))
>> + return fail(test, "/srv/img/* did not match /srv/img/a.img");
>> + if (service_open_path_listed("xfs", "/srv/img/..") ||
>> + service_open_path_listed("xfs", "/srv/img/."))
>> + return fail(test, "a . or .. component was accepted");
>> + /* Either line, if stored, would match the empty path */
>> + if (service_open_path_listed("", ""))
>> + return fail(test, "a line without a subtype was accepted");
>> + if (service_open_path_listed("ext4", ""))
>> + return fail(test, "a line without a pattern was accepted");
>> +
>> + if (write_conf("\n") == -1)
>> + return fail(test, "could not write the config file");
>> +
>> + read_conf(progname);
>> +
>> + if (service_open_path_listed("ext4", "/dev/sda"))
>> + return fail(test, "a line survived re-reading the config");
>> +
>> + printf("PASS: %s\n", test);
>> + return 0;
>> +}
>> +
>> int main(void)
>> {
>> char tempdir[] = "/tmp/test_fuser_conf.XXXXXX";
>> @@ -123,6 +176,8 @@ int main(void)
>> goto out_unlink;
>> if (test_trimmed_options())
>> goto out_unlink;
>> + if (test_service_open_path())
>> + goto out_unlink;
>>
>> printf("All fuse.conf parser tests passed\n");
>> result = 0;
>> diff --git a/util/fuse.conf b/util/fuse.conf
>> index ab048e0347b2..2c182ffa9d6a 100644
>> --- a/util/fuse.conf
>> +++ b/util/fuse.conf
>> @@ -15,3 +15,14 @@
>> # equals sign).
>>
>> #mount_max = 1000
>> +
>> +
>> +# service_open_path = <subtype> <pattern> - a FUSE server that runs as a socket
>> +# service may ask fuservicemount3 to open paths that match <pattern>, in
>> +# addition to the paths on the mount command line. <subtype> is the filesystem
>> +# type after "fuse.", <pattern> an absolute path in which "*" does not match
>> +# "/". The line can be repeated to allow different patterns and subtypes.
>> +# A pattern that matches a directory gives the server every file below it.
>> +
>> +#service_open_path = ext4 /dev/sd*
>> +#service_open_path = ext4 /dev/nvme*
>> diff --git a/util/fuser_conf.c b/util/fuser_conf.c
>> index 12688f6c42b7..5ec9d263ac2f 100644
>> --- a/util/fuser_conf.c
>> +++ b/util/fuser_conf.c
>> @@ -18,6 +18,7 @@
>> #include <stdio.h>
>> #include <stdlib.h>
>> #include <errno.h>
>> +#include <fnmatch.h>
>> #include <mntent.h>
>> #include <unistd.h>
>> #include <sys/fsuid.h>
>> @@ -35,6 +36,14 @@ int mount_max = 1000;
>> static uid_t oldfsuid;
>> static gid_t oldfsgid;
>>
>> +struct service_open_path {
>> + struct service_open_path *next;
>> + char *subtype;
>> + char *pattern;
>> +};
>> +
>> +static struct service_open_path *service_open_paths;
>> +
>> // Older versions of musl libc don't unescape entries in /etc/mtab
>>
>> // unescapes octal sequences like \040 in-place
>> @@ -192,12 +201,100 @@ static void strip_line(char *line)
>> memmove(line, s, strlen(s)+1);
>> }
>>
>> +/*
>> + * Store one service_open_path line. For the line
>> + * "service_open_path = ext4 /dev/sd*", str is "ext4 /dev/sd*".
>> + */
>> +static void parse_service_open_path(const char *str, int linenum,
>> + const char *progname)
>> +{
>> + /* <subtype> ends at the first blank */
>> + const size_t subtype_len = strcspn(str, " \t");
>> + const char *pattern = str + subtype_len;
>> + struct service_open_path *entry;
>> +
>> + /* The rest of the line is <pattern>, blanks inside it included */
>> + pattern += strspn(pattern, " \t");
>> + /* A relative pattern would depend on each user's working directory */
>> + if (!subtype_len || pattern[0] != '/') {
>> + fprintf(stderr,
>> + "%s: invalid service_open_path in %s at line %i\n",
>> + progname, FUSE_CONF, linenum);
>> + return;
>> + }
>> +
>> + entry = calloc(1, sizeof(*entry));
>> + if (entry) {
>> + entry->subtype = strndup(str, subtype_len);
>> + entry->pattern = strdup(pattern);
>> + }
>> + /* Going on without the line would refuse paths the admin allowed */
>> + if (!entry || !entry->subtype || !entry->pattern) {
>> + fprintf(stderr, "%s: failed to allocate memory\n", progname);
>> + exit(1);
>> + }
>> +
>> + /* Order does not matter, the lookup checks every entry */
>> + entry->next = service_open_paths;
>> + service_open_paths = entry;
>> +}
>> +
>> +/* The config can be read more than once; drop the lines of the last read */
>> +static void free_service_open_paths(void)
>> +{
>> + while (service_open_paths) {
>> + struct service_open_path *entry = service_open_paths;
>> +
>> + service_open_paths = entry->next;
>> + free(entry->subtype);
>> + free(entry->pattern);
>> + free(entry);
>> + }
>
> Might want to null out service_open_paths here to avoid a UAF in case
> this function ever gets used anywhere other than exit.
>
>> +}
>> +
>> +/* @return true if a path component is "." or "..", as in "/srv/img/.." */
>> +static bool has_dot_component(const char *path)
>> +{
>> + const char *comp = path;
>> +
>> + for (;;) {
>> + const size_t len = strcspn(comp, "/");
>> +
>> + if ((len == 1 && comp[0] == '.') ||
>> + (len == 2 && comp[0] == '.' && comp[1] == '.'))
>> + return true;
>> + if (!comp[len])
>> + return false;
>> + comp += len + 1;
>> + }
>> +}
>> +
>> +bool service_open_path_listed(const char *subtype, const char *path)
>> +{
>> + const struct service_open_path *entry;
>> +
>> + /* "*" also matches "..", which reaches the parent directory */
>> + if (has_dot_component(path))
>> + return false;
>> +
>> + for (entry = service_open_paths; entry; entry = entry->next)
>> + if (!strcmp(entry->subtype, subtype) &&
>
> Would you consider allowing "*" for the subtype in the config file?
>
> e.g.
>
> service_open_path = * /proc/cpuinfo
>
> So that we could (say) allowlist things like /proc/pressure that would
> allow a fuse server to monitor memory stalls in the calling process'
> namespaces and perhaps drop its caches?
>
> (I don't know if PSI info is really useful for anyone, it's just a
> thought I had while reading this patch.)
Oh PSI can be really handy if you have your own cache and want to act on
system pressure. Although PSI then slows down your fuse-server, better
to have polling on cgroup memory memory usage and to start to reduce
your own cache before PSI kicks in and PSI high limit then as last
enforcement.
>
> The code changes look good to me, modulo that question above. :)
Allowing subtype * sounds good to me, added in.
Thanks,
Bernd
^ permalink raw reply [flat|nested] 26+ messages in thread
* Re: [PATCH v2 14/14] Improve documentation for fuse service mount
2026-09-29 2:43 ` Darrick J. Wong
@ 2026-09-30 13:09 ` Bernd Schubert
0 siblings, 0 replies; 26+ messages in thread
From: Bernd Schubert @ 2026-09-30 13:09 UTC (permalink / raw)
To: Darrick J. Wong; +Cc: fuse-devel, neal
On 9/29/26 04:43, Darrick J. Wong wrote:
> On Mon, Sep 28, 2026 at 01:02:16PM +0200, Bernd Schubert via B4 Relay wrote:
>> From: Bernd Schubert <bernd@bsbernd.com>
>>
>> Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
>> ---
>> doc/README.service-mount | 312 +++++++++++++++++++++++++++++
>> doc/README.service-mount-dev | 456 ++++++++++++++++++++++++++++++++++++++++++
>> doc/README.service-mount-flow | 201 +++++++++++++++++++
>> doc/fuservicemount3.8 | 150 +++++++++++++-
>> doc/mainpage.dox | 13 ++
>> doc/mount.fuse3.8 | 18 ++
>> 6 files changed, 1144 insertions(+), 6 deletions(-)
>>
>> diff --git a/doc/README.service-mount b/doc/README.service-mount
>> new file mode 100644
>> index 000000000000..859db863f3da
>> --- /dev/null
>> +++ b/doc/README.service-mount
>> @@ -0,0 +1,312 @@
>> +Mounting FUSE filesystems that run as a socket service
>> +======================================================
>> +
>> +This document is for administrators and end users who want to mount a FUSE
>> +filesystem whose server runs as a sandboxed systemd socket service, rather
>> +than as a process in the mount caller's own context.
>> +
>> +Developers who want to make their FUSE server runnable this way should read
>> +README.service-mount-dev instead.
>> +
>> +
>> +What a service mount is
>> +-----------------------
>> +
>> +A traditional FUSE filesystem runs as a child of whoever mounts it: it
>> +inherits that environment, needs mount permission, and can see the caller's
>> +files. A *service mount* instead keeps the FUSE server running as an
>> +independent systemd service. When someone mounts the filesystem, a small
>> +privileged helper (fuservicemount3) connects to the service over a UNIX
>> +socket, hands it the /dev/fuse device and any backing files it needs, and
>> +performs the mount on its behalf.
>> +
>> +The benefit is isolation. The server can run:
>> +
>> + - as a separate, unprivileged uid/gid (systemd DynamicUser),
>> + - with no capabilities at all,
>> + - in private mount, network, and pid namespaces,
>> + - with a restricted system-call filter,
>> +
>> +while still being mountable by an ordinary user. The server never gains mount
>> +permission and never runs in the caller's environment; the privileged work is
>> +confined to the fuservicemount3 helper. See example/service_ll@.service for a
>> +fully locked-down unit.
>> +
>> +
>> +Do I need this?
>> +---------------
>> +
>> +This feature exists for one specific goal: running a FUSE server with strong
>> +privilege separation, where the server itself is fully unprivileged and
>> +sandboxed while a separate setuid helper performs the mount. Getting that
>> +requires the server to be written to the fuse_service_* API (see
>> +README.service-mount-dev). An existing FUSE program that simply calls
>> +fuse_main() cannot be mounted this way unmodified: it opens /dev/fuse and
>> +performs the mount itself, which the sandbox does not allow.
>> +
>> +If all you want is to manage an ordinary FUSE filesystem with systemd --
>> +start/stop, journald logging, cgroup resource limits --
>> +you do NOT need this feature. Run the filesystem under a plain systemd service
>> +unit instead, launching it in the foreground so systemd can track it:
>> +
>> + # myfs.service
>> + [Service]
>> + ExecStart=/usr/bin/myfs ... -f <mountpoint>
>> +
>> +systemd-run(1) starts the same thing as a transient unit, without a unit
>> +file. It passes the command line on as typed, so the filesystem can get any
>> +number of arguments. A unit file fixes them in its ExecStart= line, and a
>> +template unit (myfs@.service) takes only one parameter, the instance name.
>> +Set unit directives with -p:
>> +
>> + sudo systemd-run -p MemoryMax=1G myfs <args> -f <mountpoint>
>> +
>> +This also works in the user's own service manager, as long as fusermount3 is
>> +installed setuid root:
>> +
>> + systemd-run --user -p MemoryMax=1G myfs <args> -f <mountpoint>
>> +
>> +That filesystem still mounts the traditional way (through fusermount3) and
>> +runs in the service's own context; it is not isolated from the mount the way a
>> +service mount is.
>> +
>> +Use a service mount when you specifically want:
>> +
>> + - the filesystem server to run as a separate, unprivileged uid with no
>> + mount permission of its own,
>> + - it confined to private mount/network/pid namespaces with no capabilities,
>> + - the privileged mount work isolated in the fuservicemount3 helper,
>> + - on-demand, socket-activated startup.
>> +
>> +In short: a plain systemd unit gives you lifecycle management; a service mount
>> +gives you lifecycle management AND isolation, at the cost of the fuse server
>> +author adapting the server to the service API.
>
> "...the fuse server author needing to adapt..."
>
> Everything below here looked ok to me, though I admit that there's a lot
> of documentation so I may have missed some fine details.
Sorry about about the verbose documentation, just painful to remember
all the details a few years later without it. And I also wanted to
explain to users why it is helpful.
Thanks again for looking it!
Cheers,
Bernd
^ permalink raw reply [flat|nested] 26+ messages in thread
end of thread, other threads:[~2026-09-30 13:09 UTC | newest]
Thread overview: 26+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-28 11:02 [PATCH v2 00/14] libfuse: Add mount service safety checks and tests Bernd Schubert via B4 Relay
2026-09-28 11:02 ` [PATCH v2 01/14] mount_service: move the command line check into arg_in_cmdline() Bernd Schubert via B4 Relay
2026-09-28 11:02 ` [PATCH v2 02/14] mount_service: warn about paths not named on the command line Bernd Schubert via B4 Relay
2026-09-29 2:10 ` Darrick J. Wong
2026-09-30 11:06 ` Bernd Schubert
2026-09-28 11:02 ` [PATCH v2 03/14] mount_service: refuse paths the user did not name Bernd Schubert via B4 Relay
2026-09-29 2:26 ` Darrick J. Wong
2026-09-30 11:46 ` Bernd Schubert
2026-09-28 11:02 ` [PATCH v2 04/14] mount_service: use openat to OPEN paths Bernd Schubert via B4 Relay
2026-09-29 2:27 ` Darrick J. Wong
2026-09-28 11:02 ` [PATCH v2 05/14] util: give fuservicemount3 an absolute build-tree runpath Bernd Schubert via B4 Relay
2026-09-28 11:02 ` [PATCH v2 06/14] mount.fuse: free the options on the service mount return path Bernd Schubert via B4 Relay
2026-09-28 11:02 ` [PATCH v2 07/14] example/single_file: take no sector size from a regular backing file Bernd Schubert via B4 Relay
2026-09-28 11:02 ` [PATCH v2 08/14] test: check which files fuservicemount3 opens for the server Bernd Schubert via B4 Relay
2026-09-29 3:52 ` Darrick J. Wong
2026-09-28 11:02 ` [PATCH v2 09/14] test: check what fuservicemount3 refuses Bernd Schubert via B4 Relay
2026-09-29 3:55 ` Darrick J. Wong
2026-09-28 11:02 ` [PATCH v2 10/14] test: mount the service examples through fuservicemount3 Bernd Schubert via B4 Relay
2026-09-28 11:02 ` [PATCH v2 11/14] test: run mkfs.ext4 through the service examples Bernd Schubert via B4 Relay
2026-09-28 11:02 ` [PATCH v2 12/14] fuse_service: bound argc and arg len read from the args memfd Bernd Schubert via B4 Relay
2026-09-29 2:33 ` Darrick J. Wong
2026-09-28 11:02 ` [PATCH v2 13/14] build: move the default service socket directory to /run/fuse Bernd Schubert via B4 Relay
2026-09-29 2:34 ` Darrick J. Wong
2026-09-28 11:02 ` [PATCH v2 14/14] Improve documentation for fuse service mount Bernd Schubert via B4 Relay
2026-09-29 2:43 ` Darrick J. Wong
2026-09-30 13:09 ` Bernd Schubert
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox