public inbox for linux-fsdevel@vger.kernel.org
 help / color / mirror / Atom feed
* [PATCHSET v8] libfuse: run fuse servers as a contained service
@ 2026-03-04  0:10 Darrick J. Wong
  2026-03-04  0:10 ` [PATCH 1/3] libfuse: add systemd/inetd socket service mounting helper Darrick J. Wong
                   ` (2 more replies)
  0 siblings, 3 replies; 4+ messages in thread
From: Darrick J. Wong @ 2026-03-04  0:10 UTC (permalink / raw)
  To: bschubert, djwong; +Cc: miklos, neal, joannelkoong, linux-fsdevel, bernd

Hi all,

This patchset defines the necessary communication protocols and library
code so that users can mount fuse servers that run in unprivileged
systemd service containers.  That in turn allows unprivileged untrusted
mounts, because the worst that can happen is that a malicious image
crashes the fuse server and the mount dies, instead of corrupting the
kernel.

Bernd indicated that he might be interested in looking at the fuse
system service containment patches sooner than later, so I've separated
them from the iomap stuff and here we are.  With this patchset, we can
at least shift fuse servers to contained systemd services, albeit
without any of the performance improvements of iomap.

With a bit of luck, this should all go splendidly.
Comments and questions are, as always, welcome.

--D
---
Commits in this patchset:
 * libfuse: add systemd/inetd socket service mounting helper
 * libfuse: integrate fuse services into mount.fuse3
 * example/service_ll: create a sample systemd service fuse server
---
 include/fuse_service.h       |  180 +++++++
 include/fuse_service_priv.h  |  118 +++++
 lib/fuse_i.h                 |    5 
 util/mount_service.h         |   41 ++
 doc/fuservicemount3.8        |   32 +
 doc/meson.build              |    3 
 example/meson.build          |    7 
 example/service_ll.c         |  823 +++++++++++++++++++++++++++++++++
 example/service_ll.socket.in |   16 +
 example/service_ll@.service  |   99 ++++
 include/meson.build          |    4 
 lib/fuse_service.c           |  859 ++++++++++++++++++++++++++++++++++
 lib/fuse_service_stub.c      |   91 ++++
 lib/fuse_versionscript       |   15 +
 lib/helper.c                 |   53 ++
 lib/meson.build              |   14 +
 lib/mount.c                  |   57 ++
 meson.build                  |   37 +
 meson_options.txt            |    6 
 util/fuservicemount.c        |   66 +++
 util/meson.build             |   13 -
 util/mount.fuse.c            |   58 +-
 util/mount_service.c         | 1056 ++++++++++++++++++++++++++++++++++++++++++
 23 files changed, 3617 insertions(+), 36 deletions(-)
 create mode 100644 include/fuse_service.h
 create mode 100644 include/fuse_service_priv.h
 create mode 100644 util/mount_service.h
 create mode 100644 doc/fuservicemount3.8
 create mode 100644 example/service_ll.c
 create mode 100644 example/service_ll.socket.in
 create mode 100644 example/service_ll@.service
 create mode 100644 lib/fuse_service.c
 create mode 100644 lib/fuse_service_stub.c
 create mode 100644 util/fuservicemount.c
 create mode 100644 util/mount_service.c


^ permalink raw reply	[flat|nested] 4+ messages in thread

* [PATCH 1/3] libfuse: add systemd/inetd socket service mounting helper
  2026-03-04  0:10 [PATCHSET v8] libfuse: run fuse servers as a contained service Darrick J. Wong
@ 2026-03-04  0:10 ` Darrick J. Wong
  2026-03-04  0:10 ` [PATCH 2/3] libfuse: integrate fuse services into mount.fuse3 Darrick J. Wong
  2026-03-04  0:10 ` [PATCH 3/3] example/service_ll: create a sample systemd service fuse server Darrick J. Wong
  2 siblings, 0 replies; 4+ messages in thread
From: Darrick J. Wong @ 2026-03-04  0:10 UTC (permalink / raw)
  To: bschubert, djwong; +Cc: miklos, neal, joannelkoong, linux-fsdevel, bernd

From: Darrick J. Wong <djwong@kernel.org>

Create a mount.service helper that can start a fuse server that runs
as a socket-based systemd (or inetd) service.  To make things simpler
for fuse server authors, define a new library interface to wrap all the
functionality so that they don't have to know the details

This enables untrusted ext4 mounts via systemd service containers, which
avoids the problem of malicious filesystems compromising the integrity
of the running kernel through memory corruption.

Signed-off-by: "Darrick J. Wong" <djwong@kernel.org>
---
 include/fuse_service.h      |  180 +++++++
 include/fuse_service_priv.h |  118 +++++
 lib/fuse_i.h                |    5 
 util/mount_service.h        |   32 +
 doc/fuservicemount3.8       |   24 +
 doc/meson.build             |    3 
 include/meson.build         |    4 
 lib/fuse_service.c          |  859 ++++++++++++++++++++++++++++++++++++
 lib/fuse_service_stub.c     |   91 ++++
 lib/fuse_versionscript      |   15 +
 lib/helper.c                |   53 ++
 lib/meson.build             |   14 +
 lib/mount.c                 |   57 ++
 meson.build                 |   36 +
 meson_options.txt           |    6 
 util/fuservicemount.c       |   18 +
 util/meson.build            |    9 
 util/mount_service.c        | 1038 +++++++++++++++++++++++++++++++++++++++++++
 18 files changed, 2548 insertions(+), 14 deletions(-)
 create mode 100644 include/fuse_service.h
 create mode 100644 include/fuse_service_priv.h
 create mode 100644 util/mount_service.h
 create mode 100644 doc/fuservicemount3.8
 create mode 100644 lib/fuse_service.c
 create mode 100644 lib/fuse_service_stub.c
 create mode 100644 util/fuservicemount.c
 create mode 100644 util/mount_service.c


diff --git a/include/fuse_service.h b/include/fuse_service.h
new file mode 100644
index 00000000000000..68bea44b4a3055
--- /dev/null
+++ b/include/fuse_service.h
@@ -0,0 +1,180 @@
+/*  FUSE: Filesystem in Userspace
+  Copyright (C) 2025-2026 Oracle.
+  Author: Darrick J. Wong <djwong@kernel.org>
+
+  This program can be distributed under the terms of the GNU LGPLv2.
+  See the file LGPL2.txt.
+*/
+#ifndef FUSE_SERVICE_H_
+#define FUSE_SERVICE_H_
+
+struct fuse_service;
+
+/**
+ * Accept a socket created by mount.service for information exchange.
+ *
+ * @param sfp pointer to pointer to a service context
+ * @return -1 on error, 0 on success
+ */
+int fuse_service_accept(struct fuse_service **sfp);
+
+/**
+ * Has the fuse server accepted a service context?
+ *
+ * @param sf service context
+ */
+static inline bool fuse_service_accepted(struct fuse_service *sf)
+{
+	return sf != NULL;
+}
+
+/**
+ * Release all resources associated with the service context.
+ *
+ * @param sfp service context
+ */
+void fuse_service_release(struct fuse_service *sf);
+
+/**
+ * Destroy a service context and release all resources
+ *
+ * @param sfp pointer to pointer to a service context
+ */
+void fuse_service_destroy(struct fuse_service **sfp);
+
+/**
+ * Append the command line arguments from the mount service helper to an
+ * existing fuse_args structure.  The fuse_args should have been initialized
+ * with the argc and argv passed to main().
+ *
+ * @param sfp service context
+ * @param args arguments to modify (input+output)
+ * @return -1 on error, 0 on success
+ */
+int fuse_service_append_args(struct fuse_service *sf, struct fuse_args *args);
+
+/**
+ * Generate the effective fuse server command line from the args structure.
+ * The args structure should be the outcome from fuse_service_append_args.
+ * The resulting string is suitable for setproctitle and must be freed by the
+ * callre.
+ *
+ * @param argc argument count passed to main()
+ * @param argv argument vector passed to main()
+ * @param args fuse args structure
+ * @return effective command line string, or NULL
+ */
+char *fuse_service_cmdline(int argc, char *argv[], struct fuse_args *args);
+
+/**
+ * Take the fuse device fd passed from the mount.service helper
+ *
+ * @return device fd on success, -1 on error
+ */
+int fuse_service_take_fusedev(struct fuse_service *sfp);
+
+/**
+ * Utility function to parse common options for simple file systems
+ * using the low-level API. A help text that describes the available
+ * options can be printed with `fuse_cmdline_help`. A single
+ * non-option argument is treated as the mountpoint. Multiple
+ * non-option arguments will result in an error.
+ *
+ * If neither -o subtype= or -o fsname= options are given, a new
+ * subtype option will be added and set to the basename of the program
+ * (the fsname will remain unset, and then defaults to "fuse").
+ *
+ * Known options will be removed from *args*, unknown options will
+ * remain. The mountpoint will not be checked here; that is the job of
+ * mount.service.
+ *
+ * @param args argument vector (input+output)
+ * @param opts output argument for parsed options
+ * @return 0 on success, -1 on failure
+ */
+int fuse_service_parse_cmdline_opts(struct fuse_args *args,
+				    struct fuse_cmdline_opts *opts);
+
+/**
+ * Ask the mount.service helper to open a file on behalf of the fuse server.
+ *
+ * @param sf service context
+ * @param path path to file
+ * @param open_flags O_ flags
+ * @param create_mode mode with which to create the file
+ * @param request_flags set of FUSE_SERVICE_REQUEST_* flags
+ * @return 0 on success, -1 on failure
+ */
+int fuse_service_request_file(struct fuse_service *sf, const char *path,
+			      int open_flags, mode_t create_mode,
+			      unsigned int request_flags);
+
+/**
+ * Ask the mount.service helper to open a block device on behalf of the fuse
+ * server.
+ *
+ * @param sf service context
+ * @param path path to file
+ * @param open_flags O_ flags
+ * @param create_mode mode with which to create the file
+ * @param request_flags set of FUSE_SERVICE_REQUEST_* flags
+ * @param block_size set the block device block size to this value
+ * @return 0 on success, -1 on failure
+ */
+int fuse_service_request_blockdev(struct fuse_service *sf, const char *path,
+				  int open_flags, mode_t create_mode,
+				  unsigned int request_flags,
+				  unsigned int block_size);
+
+/**
+ * Receive a file perviously requested.
+ *
+ * @param sf service context
+ * @param path to file
+ * @fdp pointer to file descriptor, which will be set to -1 if the file could
+ *      not be opened
+ * @return -1 on socket communication failure, 0 otherwise
+ */
+int fuse_service_receive_file(struct fuse_service *sf,
+			      const char *path, int *fdp);
+
+/**
+ * Prevent the mount.service server from sending us any more open files.
+ *
+ * @param sf service context
+ */
+int fuse_service_finish_file_requests(struct fuse_service *sf);
+
+/**
+ * Bind a FUSE file system to the fuse session inside a fuse service process,
+ * then ask the mount.service helper to mount the filesystem for us.  The fuse
+ * client will begin sending requests to the fuse server immediately after
+ * this.
+ *
+ * @param sf service context
+ * @param se fuse session
+ * @param opts command line options
+ * @return 0 on success, -1 on error
+ */
+int fuse_service_session_mount(struct fuse_service *sf, struct fuse_session *se,
+			       struct fuse_cmdline_opts *opts);
+
+/**
+ * Bid farewell to the mount.service helper.  It is still necessary to call
+ * fuse_service_destroy after this.
+ *
+ * @param sf service context
+ * @param error any additional errors to send to the mount helper
+ * @return 0 on success, -1 on error
+ */
+int fuse_service_send_goodbye(struct fuse_service *sf, int error);
+
+/**
+ * Exit routine for a fuse server running as a systemd service.
+ *
+ * @param ret 0 for success, nonzero for service failure.
+ * @return a value to be passed to exit() or returned from main
+ */
+int fuse_service_exit(int ret);
+
+#endif /* FUSE_SERVICE_H_ */
diff --git a/include/fuse_service_priv.h b/include/fuse_service_priv.h
new file mode 100644
index 00000000000000..c51b62f77858ad
--- /dev/null
+++ b/include/fuse_service_priv.h
@@ -0,0 +1,118 @@
+/*  FUSE: Filesystem in Userspace
+  Copyright (C) 2025-2026 Oracle.
+  Author: Darrick J. Wong <djwong@kernel.org>
+
+  This program can be distributed under the terms of the GNU LGPLv2.
+  See the file LGPL2.txt.
+*/
+#ifndef FUSE_SERVICE_PRIV_H_
+#define FUSE_SERVICE_PRIV_H_
+
+struct fuse_service_memfd_arg {
+	__be32 pos;
+	__be32 len;
+};
+
+struct fuse_service_memfd_argv {
+	__be32 magic;
+	__be32 argc;
+};
+
+#define FUSE_SERVICE_ARGS_MAGIC		0x41524753	/* ARGS */
+
+/* mount.service sends a hello to the server and it replies */
+#define FUSE_SERVICE_HELLO_CMD		0x53414654	/* SAFT */
+#define FUSE_SERVICE_HELLO_REPLY	0x4c415354	/* LAST */
+
+/* fuse servers send commands to mount.service */
+#define FUSE_SERVICE_OPEN_CMD		0x4f50454e	/* OPEN */
+#define FUSE_SERVICE_OPEN_BDEV_CMD	0x42444556	/* BDEV */
+#define FUSE_SERVICE_FSOPEN_CMD		0x54595045	/* TYPE */
+#define FUSE_SERVICE_SOURCE_CMD		0x4e414d45	/* NAME */
+#define FUSE_SERVICE_MNTOPTS_CMD	0x4f505453	/* OPTS */
+#define FUSE_SERVICE_MNTPT_CMD		0x4d4e5450	/* MNTP */
+#define FUSE_SERVICE_MOUNT_CMD		0x444f4954	/* DOIT */
+#define FUSE_SERVICE_BYE_CMD		0x42594545	/* BYEE */
+
+/* mount.service sends replies to the fuse server */
+#define FUSE_SERVICE_OPEN_REPLY		0x46494c45	/* FILE */
+#define FUSE_SERVICE_SIMPLE_REPLY	0x5245504c	/* REPL */
+
+struct fuse_service_packet {
+	__be32 magic;			/* FUSE_SERVICE_*_{CMD,REPLY} */
+};
+
+#define FUSE_SERVICE_PROTO	(1)
+#define FUSE_SERVICE_MIN_PROTO	(1)
+#define FUSE_SERVICE_MAX_PROTO	(1)
+
+struct fuse_service_hello {
+	struct fuse_service_packet p;
+	__be16 min_version;
+	__be16 max_version;
+};
+
+struct fuse_service_hello_reply {
+	struct fuse_service_packet p;
+	__be16 version;
+};
+
+struct fuse_service_simple_reply {
+	struct fuse_service_packet p;
+	__be32 error;
+};
+
+struct fuse_service_requested_file {
+	struct fuse_service_packet p;
+	__be32 error;			/* positive errno */
+	char path[];
+};
+
+static inline size_t sizeof_fuse_service_requested_file(size_t pathlen)
+{
+	return sizeof(struct fuse_service_requested_file) + pathlen + 1;
+}
+
+#define FUSE_SERVICE_OPEN_FLAGS		(0)
+
+struct fuse_service_open_command {
+	struct fuse_service_packet p;
+	__be32 open_flags;
+	__be32 create_mode;
+	__be32 request_flags;
+	__be32 block_size;
+	char path[];
+};
+
+static inline size_t sizeof_fuse_service_open_command(size_t pathlen)
+{
+	return sizeof(struct fuse_service_open_command) + pathlen + 1;
+}
+
+struct fuse_service_string_command {
+	struct fuse_service_packet p;
+	char value[];
+};
+
+static inline size_t sizeof_fuse_service_string_command(size_t len)
+{
+	return sizeof(struct fuse_service_string_command) + len + 1;
+}
+
+struct fuse_service_bye_command {
+	struct fuse_service_packet p;
+	__be32 error;
+};
+
+struct fuse_service_mount_command {
+	struct fuse_service_packet p;
+	__be32 flags;
+};
+
+int fuse_parse_cmdline_service(struct fuse_args *args,
+				 struct fuse_cmdline_opts *opts);
+
+#define FUSE_SERVICE_ARGV	"argv"
+#define FUSE_SERVICE_FUSEDEV	"fusedev"
+
+#endif /* FUSE_SERVICE_PRIV_H_ */
diff --git a/lib/fuse_i.h b/lib/fuse_i.h
index 65d2f68f7f3091..cfa36c40ad8276 100644
--- a/lib/fuse_i.h
+++ b/lib/fuse_i.h
@@ -222,6 +222,11 @@ unsigned get_max_read(struct mount_opts *o);
 void fuse_kern_unmount(const char *mountpoint, int fd);
 int fuse_kern_mount(const char *mountpoint, struct mount_opts *mo);
 
+char *fuse_mountopts_fstype(const struct mount_opts *mo);
+char *fuse_mountopts_source(const struct mount_opts *mo, const char *devname);
+char *fuse_mountopts_kernel_opts(const struct mount_opts *mo);
+unsigned int fuse_mountopts_flags(const struct mount_opts *mo);
+
 int fuse_send_reply_iov_nofree(fuse_req_t req, int error, struct iovec *iov,
 			       int count);
 void fuse_free_req(fuse_req_t req);
diff --git a/util/mount_service.h b/util/mount_service.h
new file mode 100644
index 00000000000000..16e120da10765e
--- /dev/null
+++ b/util/mount_service.h
@@ -0,0 +1,32 @@
+/*
+  FUSE: Filesystem in Userspace
+  Copyright (C) 2025-2026 Oracle.
+  Author: Darrick J. Wong <djwong@kernel.org>
+
+  This program can be distributed under the terms of the GNU GPLv2.
+  See the file GPL2.txt.
+*/
+#ifndef MOUNT_SERVICE_H_
+#define MOUNT_SERVICE_H_
+
+/**
+ * Connect to a fuse service socket and try to mount the filesystem as
+ * specified with the CLI arguments.
+ *
+ * @argc argument count
+ * @argv vector of argument strings
+ * @return EXIT_SUCCESS for success, EXIT_FAILURE if mount fails
+ */
+int mount_service_main(int argc, char *argv[]);
+
+/**
+ * Return the fuse filesystem subtype from a full fuse filesystem type
+ * specification.  IOWs, fuse.Y -> Y; fuseblk.Z -> Z; or A -> A.  The returned
+ * pointer is within the caller's string.
+ *
+ * @param fstype full fuse filesystem type
+ * @return fuse subtype
+ */
+const char *mount_service_subtype(const char *fstype);
+
+#endif /* MOUNT_SERVICE_H_ */
diff --git a/doc/fuservicemount3.8 b/doc/fuservicemount3.8
new file mode 100644
index 00000000000000..e45d6a89c8b81a
--- /dev/null
+++ b/doc/fuservicemount3.8
@@ -0,0 +1,24 @@
+.TH fuservicemount3 "8"
+.SH NAME
+fuservicemount3 \- mount a FUSE filesystem that runs as a system socket service
+.SH SYNOPSIS
+.B fuservicemount3
+.B source
+.B mountpoint
+.BI -t " fstype"
+[
+.I options
+]
+.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.
+.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 fusermount3 (1)
+.BR fusermount (1)
+.BR mount (8)
+.BR fuse (4)
diff --git a/doc/meson.build b/doc/meson.build
index db3e0b26f71975..c105cf3471fdf4 100644
--- a/doc/meson.build
+++ b/doc/meson.build
@@ -2,3 +2,6 @@ if not platform.endswith('bsd') and platform != 'dragonfly'
   install_man('fusermount3.1', 'mount.fuse3.8')
 endif
 
+if private_cfg.get('HAVE_SERVICEMOUNT', false)
+  install_man('fuservicemount3.8')
+endif
diff --git a/include/meson.build b/include/meson.build
index bf671977a5a6a9..da51180f87eea2 100644
--- a/include/meson.build
+++ b/include/meson.build
@@ -1,4 +1,8 @@
 libfuse_headers = [ 'fuse.h', 'fuse_common.h', 'fuse_lowlevel.h',
 	            'fuse_opt.h', 'cuse_lowlevel.h', 'fuse_log.h' ]
 
+if private_cfg.get('HAVE_SERVICEMOUNT', false)
+  libfuse_headers += [ 'fuse_service.h' ]
+endif
+
 install_headers(libfuse_headers, subdir: 'fuse3')
diff --git a/lib/fuse_service.c b/lib/fuse_service.c
new file mode 100644
index 00000000000000..30cd7521eedfc4
--- /dev/null
+++ b/lib/fuse_service.c
@@ -0,0 +1,859 @@
+/*
+  FUSE: Filesystem in Userspace
+  Copyright (C) 2025-2026 Oracle.
+  Author: Darrick J. Wong <djwong@kernel.org>
+
+  Library functions to support fuse servers that can be run as "safe" systemd
+  containers.
+
+  This program can be distributed under the terms of the GNU LGPLv2.
+  See the file LGPL2.txt
+*/
+
+#define _GNU_SOURCE
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <unistd.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <systemd/sd-daemon.h>
+#include <arpa/inet.h>
+
+#include "fuse_config.h"
+#include "fuse_i.h"
+#include "fuse_service_priv.h"
+#include "fuse_service.h"
+
+struct fuse_service {
+	/* socket fd */
+	int sockfd;
+
+	/* /dev/fuse device */
+	int fusedevfd;
+
+	/* memfd for cli arguments */
+	int argvfd;
+
+	/* do we own fusedevfd? */
+	int owns_fusedevfd:1;
+};
+
+static int __recv_fd(int sockfd, struct fuse_service_requested_file *buf,
+		     ssize_t bufsize, int *fdp)
+{
+	struct iovec iov = {
+		.iov_base = buf,
+		.iov_len = bufsize,
+	};
+	union {
+		struct cmsghdr cmsghdr;
+		char control[CMSG_SPACE(sizeof (int))];
+	} cmsgu;
+	struct msghdr msg = {
+		.msg_iov = &iov,
+		.msg_iovlen = 1,
+		.msg_control = cmsgu.control,
+		.msg_controllen = sizeof(cmsgu.control),
+	};
+	struct cmsghdr *cmsg;
+	ssize_t size;
+
+	memset(&cmsgu, 0, sizeof(cmsgu));
+
+	size = recvmsg(sockfd, &msg, MSG_TRUNC);
+	if (size < 0) {
+		perror("fuse: service file reply");
+		return -1;
+	}
+	if (size > bufsize ||
+	    size < offsetof(struct fuse_service_requested_file, path)) {
+		fprintf(stderr,
+ "fuse: wrong service file reply size %zd, expected %zd\n",
+			size, bufsize);
+		return -1;
+	}
+
+	cmsg = CMSG_FIRSTHDR(&msg);
+	if (!cmsg) {
+		/* no control message means mount.service sent us an error */
+		return 0;
+	}
+	if (cmsg->cmsg_len != CMSG_LEN(sizeof(int))) {
+		fprintf(stderr,
+ "fuse: wrong service file reply control data size %zd, expected %zd\n",
+			cmsg->cmsg_len, CMSG_LEN(sizeof(int)));
+		return -1;
+	}
+	if (cmsg->cmsg_level != SOL_SOCKET || cmsg->cmsg_type != SCM_RIGHTS) {
+		fprintf(stderr,
+ "fuse: wrong service file reply control data level %d type %d, expected %d and %d\n",
+			cmsg->cmsg_level, cmsg->cmsg_type, SOL_SOCKET,
+			SCM_RIGHTS);
+		return -1;
+	}
+
+	memcpy(fdp, (int *)CMSG_DATA(cmsg), sizeof(int));
+	return 0;
+}
+
+static int recv_requested_file(int sockfd, const char *path, int *fdp)
+{
+	struct fuse_service_requested_file *req;
+	const size_t req_sz = sizeof_fuse_service_requested_file(strlen(path));
+	int ret;
+
+	*fdp = -1;
+	req = calloc(1, req_sz + 1);
+	if (!req) {
+		perror("fuse: alloc service file reply");
+		return -1;
+	}
+
+	ret = __recv_fd(sockfd, req, req_sz, fdp);
+	if (ret)
+		goto out_req;
+
+	if (req->p.magic != ntohl(FUSE_SERVICE_OPEN_REPLY)) {
+		fprintf(stderr,
+ "fuse: service file reply contains wrong magic!\n");
+		ret = -1;
+		goto out_close;
+	}
+	if (strcmp(req->path, path)) {
+		fprintf(stderr,
+ "fuse: `%s': not the requested service file, got `%s'\n",
+			path, req->path);
+		ret = -1;
+		goto out_close;
+	}
+
+	if (req->error) {
+		errno = ntohl(req->error);
+		ret = 0;
+		goto out_req;
+	}
+
+	free(req);
+	return 0;
+
+out_close:
+	close(*fdp);
+	*fdp = -1;
+out_req:
+	free(req);
+	return ret;
+}
+
+int fuse_service_receive_file(struct fuse_service *sf, const char *path,
+			      int *fdp)
+{
+	return recv_requested_file(sf->sockfd, path, fdp);
+}
+
+#define FUSE_SERVICE_REQUEST_FILE_FLAGS	(0)
+
+static int fuse_service_request_path(struct fuse_service *sf, const char *path,
+				     mode_t mode, int open_flags,
+				     mode_t create_mode,
+				     unsigned int request_flags,
+				     unsigned int block_size)
+{
+	struct iovec iov = {
+		.iov_len = sizeof_fuse_service_open_command(strlen(path)),
+	};
+	struct msghdr msg = {
+		.msg_iov = &iov,
+		.msg_iovlen = 1,
+	};
+	struct fuse_service_open_command *cmd;
+	ssize_t size;
+	unsigned int rqflags = 0;
+	int ret;
+
+	if (request_flags & ~FUSE_SERVICE_REQUEST_FILE_FLAGS) {
+		fprintf(stderr,
+ "fuse: invalid fuse service file request flags 0x%x\n", request_flags);
+		errno = EINVAL;
+		return -1;
+	}
+
+	cmd = calloc(1, iov.iov_len);
+	if (!cmd) {
+		perror("fuse: alloc service file request");
+		return -1;
+	}
+	if (S_ISBLK(mode)) {
+		cmd->p.magic = htonl(FUSE_SERVICE_OPEN_BDEV_CMD);
+		cmd->block_size = htonl(block_size);
+	} else {
+		cmd->p.magic = htonl(FUSE_SERVICE_OPEN_CMD);
+	}
+	cmd->open_flags = htonl(open_flags);
+	cmd->create_mode = htonl(create_mode);
+	cmd->request_flags = htonl(rqflags);
+	strcpy(cmd->path, path);
+	iov.iov_base = cmd;
+
+	size = sendmsg(sf->sockfd, &msg, MSG_EOR | MSG_NOSIGNAL);
+	if (size < 0) {
+		perror("fuse: request service file");
+		ret = -1;
+		goto out_free;
+	}
+
+	ret = 0;
+out_free:
+	free(cmd);
+	return ret;
+}
+
+int fuse_service_request_file(struct fuse_service *sf, const char *path,
+			      int open_flags, mode_t create_mode,
+			      unsigned int request_flags)
+{
+	return fuse_service_request_path(sf, path, S_IFREG, open_flags,
+					 create_mode, request_flags, 0);
+}
+
+int fuse_service_request_blockdev(struct fuse_service *sf, const char *path,
+				  int open_flags, mode_t create_mode,
+				  unsigned int request_flags,
+				  unsigned int block_size)
+{
+	return fuse_service_request_path(sf, path, S_IFBLK, open_flags,
+					 create_mode, request_flags,
+					 block_size);
+}
+
+int fuse_service_send_goodbye(struct fuse_service *sf, int error)
+{
+	struct fuse_service_bye_command c = {
+		.p.magic = htonl(FUSE_SERVICE_BYE_CMD),
+		.error = htonl(error),
+	};
+	struct iovec iov = {
+		.iov_base = &c,
+		.iov_len = sizeof(c),
+	};
+	struct msghdr msg = {
+		.msg_iov = &iov,
+		.msg_iovlen = 1,
+	};
+	ssize_t size;
+
+	/* already gone? */
+	if (sf->sockfd < 0)
+		return 0;
+
+	size = sendmsg(sf->sockfd, &msg, MSG_EOR | MSG_NOSIGNAL);
+	if (size < 0) {
+		perror("fuse: send service goodbye");
+		return -1;
+	}
+
+	shutdown(sf->sockfd, SHUT_RDWR);
+	close(sf->sockfd);
+	sf->sockfd = -1;
+	return 0;
+}
+
+static int find_socket_fd(void)
+{
+	struct stat statbuf;
+	char *listen_fds;
+	int nr_fds;
+	int ret;
+
+	listen_fds = getenv("LISTEN_FDS");
+	if (!listen_fds)
+		return -2;
+
+	nr_fds = atoi(listen_fds);
+	if (nr_fds != 1) {
+		fprintf(stderr,
+ "fuse: can only handle 1 service socket, got %d.\n",
+			nr_fds);
+		return -1;
+	}
+
+	ret = fstat(SD_LISTEN_FDS_START, &statbuf);
+	if (ret) {
+		perror("fuse: service socket");
+		return -1;
+	}
+
+	if (!S_ISSOCK(statbuf.st_mode)) {
+		fprintf(stderr,
+ "fuse: expected service fd %d to be a socket\n",
+				SD_LISTEN_FDS_START);
+		return -1;
+	}
+
+	return SD_LISTEN_FDS_START;
+}
+
+static int negotiate_hello(int sockfd)
+{
+	struct fuse_service_hello hello = { };
+	struct fuse_service_hello_reply reply = {
+		.p.magic = htonl(FUSE_SERVICE_HELLO_REPLY),
+		.version = htons(FUSE_SERVICE_PROTO),
+	};
+	struct iovec iov = {
+		.iov_base = &hello,
+		.iov_len = sizeof(hello),
+	};
+	struct msghdr msg = {
+		.msg_iov = &iov,
+		.msg_iovlen = 1,
+	};
+	ssize_t size;
+
+	size = recvmsg(sockfd, &msg, MSG_TRUNC);
+	if (size < 0) {
+		perror("fuse: receive service hello");
+		return -1;
+	}
+	if (size != sizeof(hello)) {
+		fprintf(stderr,
+ "fuse: wrong service hello size %zd, expected %zd\n",
+			size, sizeof(hello));
+		return -1;
+	}
+
+	if (hello.p.magic != ntohl(FUSE_SERVICE_HELLO_CMD)) {
+		fprintf(stderr,
+ "fuse: service server did not send hello command\n");
+		return -1;
+	}
+
+	if (ntohs(hello.min_version) < FUSE_SERVICE_MIN_PROTO) {
+		fprintf(stderr,
+ "fuse: unsupported min service protocol version %u\n",
+			ntohs(hello.min_version));
+		return -1;
+	}
+
+	if (ntohs(hello.max_version) > FUSE_SERVICE_MAX_PROTO) {
+		fprintf(stderr,
+ "fuse: unsupported max service protocol version %u\n",
+			ntohs(hello.min_version));
+		return -1;
+	}
+
+	iov.iov_base = &reply;
+	iov.iov_len = sizeof(reply);
+
+	size = sendmsg(sockfd, &msg, MSG_EOR | MSG_NOSIGNAL);
+	if (size < 0) {
+		perror("fuse: service hello reply");
+		return -1;
+	}
+
+	return 0;
+}
+
+int fuse_service_accept(struct fuse_service **sfp)
+{
+	struct fuse_service *sf;
+	int ret = 0;
+
+	*sfp = NULL;
+
+	sf = calloc(1, sizeof(struct fuse_service));
+	if (!sf) {
+		perror("fuse: service alloc");
+		return -1;
+	}
+
+	/* Find the socket that connects us to mount.service */
+	sf->sockfd = find_socket_fd();
+	if (sf->sockfd == -2) {
+		/* magic code that means no service configured */
+		ret = 0;
+		goto out_sf;
+	}
+	if (sf->sockfd < 0) {
+		ret = -1;
+		goto out_sf;
+	}
+
+	ret = negotiate_hello(sf->sockfd);
+	if (ret)
+		goto out_sf;
+
+	/* Receive the two critical sockets */
+	ret = recv_requested_file(sf->sockfd, FUSE_SERVICE_ARGV, &sf->argvfd);
+	if (ret < 0)
+		goto out_sockfd;
+	if (sf->argvfd < 0) {
+		perror("fuse: service mount options file");
+		goto out_sockfd;
+	}
+
+	ret = recv_requested_file(sf->sockfd, FUSE_SERVICE_FUSEDEV,
+				  &sf->fusedevfd);
+	if (ret < 0)
+		goto out_argvfd;
+	if (sf->fusedevfd < 0) {
+		perror("fuse: service fuse device");
+		goto out_argvfd;
+	}
+
+	sf->owns_fusedevfd = 1;
+	*sfp = sf;
+	return 0;
+
+out_argvfd:
+	close(sf->argvfd);
+out_sockfd:
+	shutdown(sf->sockfd, SHUT_RDWR);
+	close(sf->sockfd);
+out_sf:
+	free(sf);
+	return ret;
+}
+
+int fuse_service_append_args(struct fuse_service *sf,
+			     struct fuse_args *existing_args)
+{
+	struct fuse_service_memfd_argv memfd_args = { };
+	struct fuse_args new_args = {
+		.allocated = 1,
+	};
+	char *str = NULL;
+	off_t memfd_pos = 0;
+	ssize_t received;
+	unsigned int i;
+	int ret;
+
+	/* Figure out how many arguments we're getting from the mount helper. */
+	received = pread(sf->argvfd, &memfd_args, sizeof(memfd_args), 0);
+	if (received < 0) {
+		perror("fuse: service args file");
+		return -1;
+	}
+	if (received < sizeof(memfd_args)) {
+		fprintf(stderr,
+ "fuse: service args file length unreadable\n");
+		return -1;
+	}
+	if (ntohl(memfd_args.magic) != FUSE_SERVICE_ARGS_MAGIC) {
+		fprintf(stderr, "fuse: service args file corrupt\n");
+		return -1;
+	}
+	memfd_args.magic = ntohl(memfd_args.magic);
+	memfd_args.argc = ntohl(memfd_args.argc);
+	memfd_pos += sizeof(memfd_args);
+
+	/* Allocate a new array of argv string pointers */
+	new_args.argv = calloc(memfd_args.argc + existing_args->argc,
+			       sizeof(char *));
+	if (!new_args.argv) {
+		perror("fuse: service new args");
+		return -1;
+	}
+
+	/*
+	 * Copy the fuse server's CLI arguments.  We'll leave new_args.argv[0]
+	 * unset for now, because we'll set it in the next step with the fstype
+	 * that the mount helper sent us.
+	 */
+	new_args.argc++;
+	for (i = 1; i < existing_args->argc; i++) {
+		if (existing_args->allocated) {
+			new_args.argv[new_args.argc] = existing_args->argv[i];
+			existing_args->argv[i] = NULL;
+		} else {
+			new_args.argv[new_args.argc] =
+						strdup(existing_args->argv[i]);
+			if (!new_args.argv[new_args.argc]) {
+				perror("fuse: service duplicate existing args");
+				ret = -1;
+				goto out_new_args;
+			}
+		}
+
+		new_args.argc++;
+	}
+
+	/* Copy the rest of the arguments from the helper */
+	for (i = 0; i < memfd_args.argc; i++) {
+		struct fuse_service_memfd_arg memfd_arg = { };
+
+		/* Read argv iovec */
+		received = pread(sf->argvfd, &memfd_arg, sizeof(memfd_arg),
+				 memfd_pos);
+		if (received < 0) {
+			perror("fuse: service args file iovec read");
+			ret = -1;
+			goto out_new_args;
+		}
+		if (received < sizeof(struct fuse_service_memfd_arg)) {
+			fprintf(stderr,
+ "fuse: service args file argv[%u] iovec short read %zd",
+				i, received);
+			ret = -1;
+			goto out_new_args;
+		}
+		memfd_arg.pos = ntohl(memfd_arg.pos);
+		memfd_arg.len = ntohl(memfd_arg.len);
+		memfd_pos += sizeof(memfd_arg);
+
+		/* read arg string from file */
+		str = calloc(1, memfd_arg.len + 1);
+		if (!str) {
+			perror("fuse: service arg alloc");
+			ret = -1;
+			goto out_new_args;
+		}
+
+		received = pread(sf->argvfd, str, memfd_arg.len, memfd_arg.pos);
+		if (received < 0) {
+			perror("fuse: service args file read");
+			ret = -1;
+			goto out_str;
+		}
+		if (received < memfd_arg.len) {
+			fprintf(stderr,
+ "fuse: service args file argv[%u] short read %zd",
+				i, received);
+			ret = -1;
+			goto out_str;
+		}
+
+		/* move string into the args structure */
+		if (i == 0) {
+			/* the first argument is the fs type */
+			new_args.argv[0] = str;
+		} else {
+			new_args.argv[new_args.argc] = str;
+			new_args.argc++;
+		}
+		str = NULL;
+	}
+
+	/* drop existing args, move new args to existing args */
+	fuse_opt_free_args(existing_args);
+	memcpy(existing_args, &new_args, sizeof(*existing_args));
+
+	close(sf->argvfd);
+	sf->argvfd = -1;
+
+	return 0;
+
+out_str:
+	free(str);
+out_new_args:
+	fuse_opt_free_args(&new_args);
+	return ret;
+}
+
+int fuse_service_take_fusedev(struct fuse_service *sfp)
+{
+	sfp->owns_fusedevfd = 0;
+	return sfp->fusedevfd;
+}
+
+int fuse_service_finish_file_requests(struct fuse_service *sf)
+{
+#ifdef SO_PASSRIGHTS
+	int zero = 0;
+
+	/* don't let a malicious mount helper send us more fds */
+	return setsockopt(sf->sockfd, SOL_SOCKET, SO_PASSRIGHTS, &zero,
+			  sizeof(zero));
+#else
+	/* shut up gcc */
+	sf = sf;
+	return 0;
+#endif
+}
+
+static int send_string(struct fuse_service *sf, uint32_t command,
+		       const char *value, int *error)
+{
+	struct fuse_service_simple_reply reply = { };
+	struct iovec iov = {
+		.iov_len = sizeof_fuse_service_string_command(strlen(value)),
+	};
+	struct msghdr msg = {
+		.msg_iov = &iov,
+		.msg_iovlen = 1,
+	};
+	struct fuse_service_string_command *cmd;
+	ssize_t size;
+
+	cmd = malloc(iov.iov_len);
+	if (!cmd) {
+		perror("fuse: alloc service string send");
+		return -1;
+	}
+	cmd->p.magic = ntohl(command);
+	strcpy(cmd->value, value);
+	iov.iov_base = cmd;
+
+	size = sendmsg(sf->sockfd, &msg, MSG_EOR | MSG_NOSIGNAL);
+	if (size < 0) {
+		perror("fuse: send service string");
+		return -1;
+	}
+	free(cmd);
+
+	iov.iov_base = &reply;
+	iov.iov_len = sizeof(reply);
+	size = recvmsg(sf->sockfd, &msg, MSG_TRUNC);
+	if (size < 0) {
+		perror("fuse: service string reply");
+		return -1;
+	}
+	if (size != sizeof(reply)) {
+		fprintf(stderr,
+ "fuse: wrong service string reply size %zd, expected %zd\n",
+			size, sizeof(reply));
+		return -1;
+	}
+
+	if (ntohl(reply.p.magic) != FUSE_SERVICE_SIMPLE_REPLY) {
+		fprintf(stderr,
+ "fuse: service string reply contains wrong magic!\n");
+		return -1;
+	}
+
+	*error = ntohl(reply.error);
+	return 0;
+}
+
+static int send_mount(struct fuse_service *sf, unsigned int flags, int *error)
+{
+	struct fuse_service_simple_reply reply = { };
+	struct fuse_service_mount_command c = {
+		.p.magic = htonl(FUSE_SERVICE_MOUNT_CMD),
+		.flags = htonl(flags),
+	};
+	struct iovec iov = {
+		.iov_base = &c,
+		.iov_len = sizeof(c),
+	};
+	struct msghdr msg = {
+		.msg_iov = &iov,
+		.msg_iovlen = 1,
+	};
+	ssize_t size;
+
+	size = sendmsg(sf->sockfd, &msg, MSG_EOR | MSG_NOSIGNAL);
+	if (size < 0) {
+		perror("fuse: send service mount command");
+		return -1;
+	}
+
+	iov.iov_base = &reply;
+	iov.iov_len = sizeof(reply);
+	size = recvmsg(sf->sockfd, &msg, MSG_TRUNC);
+	if (size < 0) {
+		perror("fuse: service mount reply");
+		return -1;
+	}
+	if (size != sizeof(reply)) {
+		fprintf(stderr,
+ "fuse: wrong service mount reply size %zd, expected %zd\n",
+			size, sizeof(reply));
+		return -1;
+	}
+
+	if (ntohl(reply.p.magic) != FUSE_SERVICE_SIMPLE_REPLY) {
+		fprintf(stderr,
+ "fuse: service mount reply contains wrong magic!\n");
+		return -1;
+	}
+
+	*error = ntohl(reply.error);
+	return 0;
+}
+
+int fuse_service_session_mount(struct fuse_service *sf, struct fuse_session *se,
+			       struct fuse_cmdline_opts *opts)
+{
+	char *fstype = fuse_mountopts_fstype(se->mo);
+	char *source = fuse_mountopts_source(se->mo, "???");
+	char *mntopts = fuse_mountopts_kernel_opts(se->mo);
+	char path[32];
+	int ret;
+	int error;
+
+	if (!fstype || !source) {
+		fprintf(stderr, "fuse: cannot allocate service strings\n");
+		ret = -1;
+		goto out_strings;
+	}
+
+	snprintf(path, sizeof(path), "/dev/fd/%d", sf->fusedevfd);
+	ret = fuse_session_mount(se, path);
+	if (ret)
+		goto out_strings;
+
+	ret = send_string(sf, FUSE_SERVICE_FSOPEN_CMD, fstype, &error);
+	if (ret)
+		goto out_strings;
+	if (error) {
+		fprintf(stderr, "fuse: service fsopen: %s\n",
+			strerror(error));
+		ret = -1;
+		goto out_strings;
+	}
+
+	ret = send_string(sf, FUSE_SERVICE_SOURCE_CMD, source, &error);
+	if (ret)
+		goto out_strings;
+	if (error) {
+		fprintf(stderr, "fuse: service fs source: %s\n",
+			strerror(error));
+		ret = -1;
+		goto out_strings;
+	}
+
+	ret = send_string(sf, FUSE_SERVICE_MNTPT_CMD, opts->mountpoint, &error);
+	if (ret)
+		goto out_strings;
+	if (error) {
+		fprintf(stderr, "fuse: service fs mountpoint: %s\n",
+			strerror(error));
+		ret = -1;
+		goto out_strings;
+	}
+
+	if (mntopts) {
+		ret = send_string(sf, FUSE_SERVICE_MNTOPTS_CMD, mntopts,
+				  &error);
+		if (ret)
+			goto out_strings;
+		if (error) {
+			fprintf(stderr,
+ "fuse: service fs mount options: %s\n",
+				strerror(error));
+			ret = -1;
+			goto out_strings;
+		}
+	}
+
+	ret = send_mount(sf, fuse_mountopts_flags(se->mo), &error);
+	if (ret)
+		goto out_strings;
+	if (error) {
+		fprintf(stderr, "fuse: service mount: %s\n", strerror(error));
+		ret = -1;
+		goto out_strings;
+	}
+
+	/*
+	 * foreground mode is needed so that systemd actually tracks the
+	 * service correctly and doesnt try to kill it; and so that
+	 * stdout/stderr don't get zapped
+	 */
+	opts->foreground = 1;
+
+out_strings:
+	free(mntopts);
+	free(source);
+	free(fstype);
+	return ret;
+}
+
+void fuse_service_release(struct fuse_service *sf)
+{
+	if (sf->owns_fusedevfd)
+		close(sf->fusedevfd);
+	sf->owns_fusedevfd = 0;
+	sf->fusedevfd = -1;
+	close(sf->argvfd);
+	sf->argvfd = -1;
+	shutdown(sf->sockfd, SHUT_RDWR);
+	close(sf->sockfd);
+	sf->sockfd = -1;
+}
+
+void fuse_service_destroy(struct fuse_service **sfp)
+{
+	struct fuse_service *sf = *sfp;
+
+	if (sf) {
+		fuse_service_release(*sfp);
+		free(sf);
+	}
+
+	*sfp = NULL;
+}
+
+char *fuse_service_cmdline(int argc, char *argv[], struct fuse_args *args)
+{
+	char *p, *dst;
+	size_t len = 1;
+	ssize_t ret;
+	char *argv0;
+	unsigned int i;
+
+	/* Try to preserve argv[0] */
+	if (argc > 0)
+		argv0 = argv[0];
+	else if (args->argc > 0)
+		argv0 = args->argv[0];
+	else
+		return NULL;
+
+	/* Pick up the alleged fstype from args->argv[0] */
+	if (args->argc == 0)
+		return NULL;
+
+	len += strlen(argv0) + 1;
+	len += 3; /* " -t" */
+	for (i = 0; i < args->argc; i++) {
+		len += strlen(args->argv[i]) + 1;
+	}
+
+	p = malloc(len);
+	if (!p)
+		return NULL;
+	dst = p;
+
+	/* Format: argv0 -t alleged_fstype [all other options...] */
+	ret = sprintf(dst, "%s -t", argv0);
+	dst += ret;
+	for (i = 0; i < args->argc; i++) {
+		ret = sprintf(dst, " %s", args->argv[i]);
+		dst += ret;
+	}
+
+	return p;
+}
+
+int fuse_service_parse_cmdline_opts(struct fuse_args *args,
+				    struct fuse_cmdline_opts *opts)
+{
+	return fuse_parse_cmdline_service(args, opts);
+}
+
+int fuse_service_exit(int ret)
+{
+	/*
+	 * We have to sleep 2 seconds here because journald uses the pid to
+	 * connect our log messages to the systemd service.  This is critical
+	 * for capturing all the log messages if the service fails, because
+	 * failure analysis tools use the service name to gather log messages
+	 * for reporting.
+	 */
+	sleep(2);
+
+	/*
+	 * If we're being run as a service, the return code must fit the LSB
+	 * init script action error guidelines, which is to say that we
+	 * compress all errors to 1 ("generic or unspecified error", LSB 5.0
+	 * section 22.2) and hope the admin will scan the log for what actually
+	 * happened.
+	 */
+	return ret != 0 ? EXIT_FAILURE : EXIT_SUCCESS;
+}
diff --git a/lib/fuse_service_stub.c b/lib/fuse_service_stub.c
new file mode 100644
index 00000000000000..79abeca9dfb70e
--- /dev/null
+++ b/lib/fuse_service_stub.c
@@ -0,0 +1,91 @@
+/*
+  FUSE: Filesystem in Userspace
+  Copyright (C) 2025-2026 Oracle.
+  Author: Darrick J. Wong <djwong@kernel.org>
+
+  Stub functions for platforms where we cannot have fuse servers run as "safe"
+  systemd containers.
+
+  This program can be distributed under the terms of the GNU LGPLv2.
+  See the file LGPL2.txt
+*/
+
+/* shut up gcc */
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+
+#define _GNU_SOURCE
+#include <errno.h>
+
+#include "fuse_config.h"
+#include "fuse_i.h"
+#include "fuse_service_priv.h"
+#include "fuse_service.h"
+
+int fuse_service_receive_file(struct fuse_service *sf, const char *path,
+			      int *fdp)
+{
+	errno = EOPNOTSUPP;
+	return -1;
+}
+
+int fuse_service_request_file(struct fuse_service *sf, const char *path,
+			      int open_flags, mode_t create_mode,
+			      unsigned int request_flags)
+{
+	errno = EOPNOTSUPP;
+	return -1;
+}
+
+int fuse_service_send_goodbye(struct fuse_service *sf, int error)
+{
+	errno = EOPNOTSUPP;
+	return -1;
+}
+
+int fuse_service_accept(struct fuse_service **sfp)
+{
+	*sfp = NULL;
+	errno = EOPNOTSUPP;
+	return -1;
+}
+
+int fuse_service_append_args(struct fuse_service *sf,
+			     struct fuse_args *existing_args)
+{
+	errno = EOPNOTSUPP;
+	return -1;
+}
+
+int fuse_service_take_fusedev(struct fuse_service *sfp)
+{
+	return -1;
+}
+
+int fuse_service_finish_file_requests(struct fuse_service *sf)
+{
+	errno = EOPNOTSUPP;
+	return -1;
+}
+
+int fuse_service_mount(struct fuse_service *sf, struct fuse_session *se,
+		       const char *mountpoint)
+{
+	errno = EOPNOTSUPP;
+	return -1;
+}
+
+void fuse_service_release(struct fuse_service *sf)
+{
+}
+
+void fuse_service_destroy(struct fuse_service **sfp)
+{
+	*sfp = NULL;
+}
+
+int fuse_service_parse_cmdline_opts(struct fuse_args *args,
+				    struct fuse_cmdline_opts *opts)
+{
+	errno = EOPNOTSUPP;
+	return -1;
+}
diff --git a/lib/fuse_versionscript b/lib/fuse_versionscript
index cce09610316f4b..a51d321ec7fb34 100644
--- a/lib/fuse_versionscript
+++ b/lib/fuse_versionscript
@@ -227,6 +227,21 @@ FUSE_3.19 {
 		fuse_session_start_teardown_watchdog;
 		fuse_session_stop_teardown_watchdog;
 		fuse_lowlevel_notify_prune;
+
+		fuse_service_accept;
+		fuse_service_append_args;
+		fuse_service_cmdline;
+		fuse_service_destroy;
+		fuse_service_exit;
+		fuse_service_finish_file_requests;
+		fuse_service_parse_cmdline_opts;
+		fuse_service_receive_file;
+		fuse_service_release;
+		fuse_service_request_file;
+		fuse_service_request_blockdev;
+		fuse_service_send_goodbye;
+		fuse_service_session_mount;
+		fuse_service_take_fusedev;
 } FUSE_3.18;
 
 # Local Variables:
diff --git a/lib/helper.c b/lib/helper.c
index 5c13b93a473181..3b57788621d902 100644
--- a/lib/helper.c
+++ b/lib/helper.c
@@ -26,6 +26,11 @@
 #include <errno.h>
 #include <sys/param.h>
 
+#ifdef HAVE_SERVICEMOUNT
+# include <linux/types.h>
+# include "fuse_service_priv.h"
+#endif
+
 #define FUSE_HELPER_OPT(t, p) \
 	{ t, offsetof(struct fuse_cmdline_opts, p), 1 }
 
@@ -174,6 +179,29 @@ static int fuse_helper_opt_proc(void *data, const char *arg, int key,
 	}
 }
 
+#ifdef HAVE_SERVICEMOUNT
+static int fuse_helper_opt_proc_service(void *data, const char *arg, int key,
+					struct fuse_args *outargs)
+{
+	(void) outargs;
+	struct fuse_cmdline_opts *opts = data;
+
+	switch (key) {
+	case FUSE_OPT_KEY_NONOPT:
+		if (!opts->mountpoint) {
+			return fuse_opt_add_opt(&opts->mountpoint, arg);
+		} else {
+			fuse_log(FUSE_LOG_ERR, "fuse: invalid argument `%s'\n", arg);
+			return -1;
+		}
+
+	default:
+		/* Pass through unknown options */
+		return 1;
+	}
+}
+#endif
+
 /* Under FreeBSD, there is no subtype option so this
    function actually sets the fsname */
 static int add_default_subtype(const char *progname, struct fuse_args *args)
@@ -228,6 +256,31 @@ int fuse_parse_cmdline_312(struct fuse_args *args,
 	return 0;
 }
 
+#ifdef HAVE_SERVICEMOUNT
+int fuse_parse_cmdline_service(struct fuse_args *args,
+			       struct fuse_cmdline_opts *opts)
+{
+	memset(opts, 0, sizeof(struct fuse_cmdline_opts));
+
+	opts->max_idle_threads = UINT_MAX; /* new default in fuse version 3.12 */
+	opts->max_threads = 10;
+
+	if (fuse_opt_parse(args, opts, fuse_helper_opts,
+			   fuse_helper_opt_proc_service) == -1)
+		return -1;
+
+	/* *Linux*: if neither -o subtype nor -o fsname are specified,
+	   set subtype to program's basename.
+	   *FreeBSD*: if fsname is not specified, set to program's
+	   basename. */
+	if (!opts->nodefault_subtype)
+		if (add_default_subtype(args->argv[0], args) == -1)
+			return -1;
+
+	return 0;
+}
+#endif
+
 /**
  * struct fuse_cmdline_opts got extended in libfuse-3.12
  */
diff --git a/lib/meson.build b/lib/meson.build
index fcd95741c9d374..81fb3d8c0afbc6 100644
--- a/lib/meson.build
+++ b/lib/meson.build
@@ -10,6 +10,12 @@ else
    libfuse_sources += [ 'mount_bsd.c' ]
 endif
 
+if private_cfg.get('HAVE_SERVICEMOUNT', false)
+  libfuse_sources += [ 'fuse_service.c' ]
+else
+  libfuse_sources += [ 'fuse_service_stub.c' ]
+endif
+
 deps = [ thread_dep ]
 if private_cfg.get('HAVE_ICONV')
    libfuse_sources += [ 'modules/iconv.c' ]
@@ -54,13 +60,19 @@ libfuse = library('fuse3',
                   link_args: ['-Wl,--version-script,' + meson.current_source_dir()
                               + '/fuse_versionscript' ])
 
+vars = []
+if private_cfg.get('HAVE_SERVICEMOUNT', false)
+  service_socket_dir = private_cfg.get_unquoted('FUSE_SERVICE_SOCKET_DIR', '')
+  vars += ['service_socket_dir=' + service_socket_dir]
+endif
 pkg = import('pkgconfig')
 pkg.generate(libraries: [ libfuse, '-lpthread' ],
              libraries_private: '-ldl',
              version: meson.project_version(),
              name: 'fuse3',
              description: 'Filesystem in Userspace',
-             subdirs: 'fuse3')
+             subdirs: 'fuse3',
+             variables: vars)
 
 libfuse_dep = declare_dependency(include_directories: include_dirs,
                                  link_with: libfuse, dependencies: deps)
diff --git a/lib/mount.c b/lib/mount.c
index 7a856c101a7fc4..d232fcd8649537 100644
--- a/lib/mount.c
+++ b/lib/mount.c
@@ -546,24 +546,13 @@ static int fuse_mount_sys(const char *mnt, struct mount_opts *mo,
 	if (res == -1)
 		goto out_close;
 
-	source = malloc((mo->fsname ? strlen(mo->fsname) : 0) +
-			(mo->subtype ? strlen(mo->subtype) : 0) +
-			strlen(devname) + 32);
-
-	type = malloc((mo->subtype ? strlen(mo->subtype) : 0) + 32);
+	type = fuse_mountopts_fstype(mo);
+	source = fuse_mountopts_source(mo, devname);
 	if (!type || !source) {
 		fuse_log(FUSE_LOG_ERR, "fuse: failed to allocate memory\n");
 		goto out_close;
 	}
 
-	strcpy(type, mo->blkdev ? "fuseblk" : "fuse");
-	if (mo->subtype) {
-		strcat(type, ".");
-		strcat(type, mo->subtype);
-	}
-	strcpy(source,
-	       mo->fsname ? mo->fsname : (mo->subtype ? mo->subtype : devname));
-
 	res = mount(source, mnt, type, mo->flags, mo->kernel_opts);
 	if (res == -1 && errno == ENODEV && mo->subtype) {
 		/* Probably missing subtype support */
@@ -674,6 +663,48 @@ void destroy_mount_opts(struct mount_opts *mo)
 	free(mo);
 }
 
+char *fuse_mountopts_fstype(const struct mount_opts *mo)
+{
+	char *type = malloc((mo->subtype ? strlen(mo->subtype) : 0) + 32);
+
+	if (!type)
+		return NULL;
+
+	strcpy(type, mo->blkdev ? "fuseblk" : "fuse");
+	if (mo->subtype) {
+		strcat(type, ".");
+		strcat(type, mo->subtype);
+	}
+
+	return type;
+}
+
+char *fuse_mountopts_source(const struct mount_opts *mo, const char *devname)
+{
+	char *source = malloc((mo->fsname ? strlen(mo->fsname) : 0) +
+			(mo->subtype ? strlen(mo->subtype) : 0) +
+			strlen(devname) + 32);
+
+	if (!source)
+		return NULL;
+
+	strcpy(source,
+	       mo->fsname ? mo->fsname : (mo->subtype ? mo->subtype : devname));
+
+	return source;
+}
+
+char *fuse_mountopts_kernel_opts(const struct mount_opts *mo)
+{
+	if (mo->kernel_opts)
+		return strdup(mo->kernel_opts);
+	return NULL;
+}
+
+unsigned int fuse_mountopts_flags(const struct mount_opts *mo)
+{
+	return mo->flags;
+}
 
 int fuse_kern_mount(const char *mountpoint, struct mount_opts *mo)
 {
diff --git a/meson.build b/meson.build
index 80c5f1dc0bd356..ed05a245b48df2 100644
--- a/meson.build
+++ b/meson.build
@@ -69,6 +69,11 @@ args_default = [ '-D_GNU_SOURCE' ]
 #
 private_cfg = configuration_data()
 private_cfg.set_quoted('PACKAGE_VERSION', meson.project_version())
+service_socket_dir = get_option('service-socket-dir')
+if service_socket_dir == ''
+  service_socket_dir = '/run/filesystems'
+endif
+private_cfg.set_quoted('FUSE_SERVICE_SOCKET_DIR', service_socket_dir)
 
 # Test for presence of some functions
 test_funcs = [ 'fork', 'fstatat', 'openat', 'readlinkat', 'pipe2',
@@ -180,6 +185,37 @@ if get_option('enable-io-uring') and liburing.found() and libnuma.found()
    endif
 endif
 
+# Check for systemd support
+systemd_system_unit_dir = get_option('systemdsystemunitdir')
+if systemd_system_unit_dir == ''
+  systemd = dependency('systemd', required: false)
+  if systemd.found()
+     systemd_system_unit_dir = systemd.get_variable(pkgconfig: 'systemd_system_unit_dir')
+  endif
+endif
+
+if systemd_system_unit_dir == ''
+  warning('could not determine systemdsystemunitdir, systemd stuff will not be installed')
+else
+  private_cfg.set_quoted('SYSTEMD_SYSTEM_UNIT_DIR', systemd_system_unit_dir)
+  private_cfg.set('HAVE_SYSTEMD', true)
+endif
+
+# Check for libc SCM_RIGHTS support (aka Linux)
+code = '''
+#include <sys/socket.h>
+int main(void) {
+    int moo = SCM_RIGHTS;
+    return moo;
+}'''
+if cc.links(code, name: 'libc SCM_RIGHTS support')
+  private_cfg.set('HAVE_SCM_RIGHTS', true)
+endif
+
+if private_cfg.get('HAVE_SCM_RIGHTS', false) and private_cfg.get('HAVE_SYSTEMD', false)
+  private_cfg.set('HAVE_SERVICEMOUNT', true)
+endif
+
 #
 # Compiler configuration
 #
diff --git a/meson_options.txt b/meson_options.txt
index c1f8fe69467184..95655a0d64895c 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -27,3 +27,9 @@ option('enable-usdt', type : 'boolean', value : false,
 
 option('enable-io-uring', type: 'boolean', value: true,
        description: 'Enable fuse-over-io-uring support')
+
+option('service-socket-dir', type : 'string', value : '',
+       description: 'Where to install fuse server sockets (if empty, /run/filesystems)')
+
+option('systemdsystemunitdir', type : 'string', value : '',
+       description: 'Where to install systemd unit files (if empty, query pkg-config(1))')
diff --git a/util/fuservicemount.c b/util/fuservicemount.c
new file mode 100644
index 00000000000000..860c512a34e30f
--- /dev/null
+++ b/util/fuservicemount.c
@@ -0,0 +1,18 @@
+/*
+  FUSE: Filesystem in Userspace
+  Copyright (C) 2025-2026 Oracle.
+  Author: Darrick J. Wong <djwong@kernel.org>
+
+  This program can be distributed under the terms of the GNU GPLv2.
+  See the file GPL2.txt.
+*/
+/* This program does the mounting of FUSE filesystems that run in systemd */
+
+#define _GNU_SOURCE
+#include "fuse_config.h"
+#include "mount_service.h"
+
+int main(int argc, char *argv[])
+{
+	return mount_service_main(argc, argv);
+}
diff --git a/util/meson.build b/util/meson.build
index 0e4b1cce95377e..68d8bb11f92955 100644
--- a/util/meson.build
+++ b/util/meson.build
@@ -6,6 +6,15 @@ executable('fusermount3', ['fusermount.c', '../lib/mount_util.c', '../lib/util.c
            install_dir: get_option('bindir'),
            c_args: '-DFUSE_CONF="@0@"'.format(fuseconf_path))
 
+if private_cfg.get('HAVE_SERVICEMOUNT', false)
+  executable('fuservicemount3', ['mount_service.c', 'fuservicemount.c'],
+             include_directories: include_dirs,
+             link_with: [ libfuse ],
+             install: true,
+             install_dir: get_option('sbindir'),
+             c_args: '-DFUSE_USE_VERSION=317')
+endif
+
 executable('mount.fuse3', ['mount.fuse.c'],
            include_directories: include_dirs,
            link_with: [ libfuse ],
diff --git a/util/mount_service.c b/util/mount_service.c
new file mode 100644
index 00000000000000..4bee10bba2b18b
--- /dev/null
+++ b/util/mount_service.c
@@ -0,0 +1,1038 @@
+/*
+  FUSE: Filesystem in Userspace
+  Copyright (C) 2025-2026 Oracle.
+  Author: Darrick J. Wong <djwong@kernel.org>
+
+  This program can be distributed under the terms of the GNU GPLv2.
+  See the file GPL2.txt.
+*/
+/* This program does the mounting of FUSE filesystems that run in systemd */
+
+#define _GNU_SOURCE
+#include "fuse_config.h"
+#include <stdint.h>
+#include <sys/mman.h>
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <sys/mount.h>
+#include <stdbool.h>
+#include <limits.h>
+#include <sys/stat.h>
+#include <arpa/inet.h>
+
+#include "mount_util.h"
+#include "util.h"
+#include "fuse_i.h"
+#include "fuse_service_priv.h"
+#include "mount_service.h"
+
+#define FUSE_KERN_DEVICE_ENV	"FUSE_KERN_DEVICE"
+#define FUSE_DEV		"/dev/fuse"
+
+struct mount_service {
+	/* alleged fuse subtype based on -t cli argument */
+	const char *subtype;
+
+	/* full fuse filesystem type we give to mount() */
+	char *fstype;
+
+	/* source argument to mount() */
+	char *source;
+
+	/* target argument (aka mountpoint) to mount() */
+	char *mountpoint;
+
+	/* mount options */
+	char *mntopts;
+
+	/* socket fd */
+	int sockfd;
+
+	/* /dev/fuse device */
+	int fusedevfd;
+
+	/* memfd for cli arguments */
+	int argvfd;
+
+	/* fd for fsopen */
+	int fsopenfd;
+};
+
+/* Filter out the subtype of the filesystem (e.g. fuse.Y -> Y) */
+const char *mount_service_subtype(const char *fstype)
+{
+	char *period = strrchr(fstype, '.');
+	if (period)
+		return period + 1;
+
+	return fstype;
+}
+
+static int mount_service_init(struct mount_service *mo, int argc,
+			      char *argv[])
+{
+	char *fstype = NULL;
+	int i;
+
+	mo->sockfd = -1;
+	mo->fsopenfd = -1;
+
+	for (i = 0; i < argc; i++) {
+		if (!strcmp(argv[i], "-t") && i + 1 < argc) {
+			fstype = argv[i + 1];
+			break;
+		}
+	}
+	if (!fstype)
+		return -1;
+
+	mo->subtype = mount_service_subtype(fstype);
+	return 0;
+}
+
+static int mount_service_connect(struct mount_service *mo)
+{
+	struct sockaddr_un name = {
+		.sun_family = AF_UNIX,
+	};
+	int sockfd;
+	ssize_t written;
+	int ret;
+
+	written = snprintf(name.sun_path, sizeof(name.sun_path),
+			FUSE_SERVICE_SOCKET_DIR "/%s", mo->subtype);
+	if (written > sizeof(name.sun_path)) {
+		fprintf(stderr,
+ "mount.service: filesystem type name (\"%s\") is too long.\n",
+			mo->subtype);
+		return -1;
+	}
+
+	sockfd = socket(AF_UNIX, SOCK_SEQPACKET, 0);
+	if (sockfd < 0) {
+		fprintf(stderr,
+ "mount.service: opening %s service socket: %s\n", mo->subtype,
+			strerror(errno));
+		return -1;
+	}
+
+	ret = connect(sockfd, (const struct sockaddr *)&name, sizeof(name));
+	if (ret) {
+		if (errno == ENOENT)
+			fprintf(stderr,
+ "mount.service: no safe filesystem driver for %s available.\n",
+				mo->subtype);
+		else
+			perror(name.sun_path);
+		goto out;
+	}
+
+#ifdef SO_PASSRIGHTS
+	{
+		int zero = 0;
+
+		/* don't let a malicious fuse server send us more fds */
+		setsockopt(sockfd, SOL_SOCKET, SO_PASSRIGHTS, &zero,
+			   sizeof(zero));
+	}
+#endif
+
+	mo->sockfd = sockfd;
+	return 0;
+out:
+	close(sockfd);
+	return -1;
+}
+
+static int mount_service_send_hello(struct mount_service *mo)
+{
+	struct fuse_service_hello hello = {
+		.p.magic = htonl(FUSE_SERVICE_HELLO_CMD),
+		.min_version = htons(FUSE_SERVICE_MIN_PROTO),
+		.max_version = htons(FUSE_SERVICE_MAX_PROTO),
+	};
+	struct fuse_service_hello_reply reply = { };
+	struct iovec iov = {
+		.iov_base = &hello,
+		.iov_len = sizeof(hello),
+	};
+	struct msghdr msg = {
+		.msg_iov = &iov,
+		.msg_iovlen = 1,
+	};
+	ssize_t size;
+
+	size = sendmsg(mo->sockfd, &msg, MSG_EOR | MSG_NOSIGNAL);
+	if (size < 0) {
+		perror("mount.service: send hello");
+		return -1;
+	}
+
+	iov.iov_base = &reply;
+	iov.iov_len = sizeof(reply);
+
+	size = recvmsg(mo->sockfd, &msg, MSG_TRUNC);
+	if (size < 0) {
+		perror("mount.service: hello reply");
+		return -1;
+	}
+	if (size != sizeof(reply)) {
+		fprintf(stderr,
+ "mount.service: wrong hello reply size %zd, expected %zd\n",
+			size, sizeof(reply));
+		return -1;
+	}
+
+	if (reply.p.magic != ntohl(FUSE_SERVICE_HELLO_REPLY)) {
+		fprintf(stderr,
+ "mount.service: %s service server did not reply to hello\n",
+			mo->subtype);
+		return -1;
+	}
+
+	if (ntohs(reply.version) < FUSE_SERVICE_MIN_PROTO ||
+	    ntohs(reply.version) > FUSE_SERVICE_MAX_PROTO) {
+		fprintf(stderr,
+ "mount.service: unsupported protocol version %u\n",
+			ntohs(reply.version));
+		return -1;
+	}
+
+	return 0;
+}
+
+static int mount_service_capture_arg(struct mount_service *mo,
+				     struct fuse_service_memfd_argv *args,
+				     const char *string, off_t *array_pos,
+				     off_t *string_pos)
+{
+	const size_t string_len = strlen(string) + 1;
+	struct fuse_service_memfd_arg arg = {
+		.pos = htonl(*string_pos),
+		.len = htonl(string_len),
+	};
+	ssize_t written;
+
+	written = pwrite(mo->argvfd, string, string_len, *string_pos);
+	if (written < 0) {
+		perror("mount.service: memfd argv write");
+		return -1;
+	}
+	if (written < string_len) {
+		fprintf(stderr, "mount.service: memfd argv[%u] write %zd\n",
+			args->argc, written);
+		return -1;
+	}
+
+	written = pwrite(mo->argvfd, &arg, sizeof(arg), *array_pos);
+	if (written < 0) {
+		perror("mount.service: memfd arg write");
+		return -1;
+	}
+	if (written < sizeof(arg)) {
+		fprintf(stderr, "mount.service: memfd arg[%u] write %zd\n",
+			args->argc, written);
+		return -1;
+	}
+
+	args->argc++;
+	*string_pos += string_len;
+	*array_pos += sizeof(arg);
+
+	return 0;
+}
+
+static int mount_service_capture_args(struct mount_service *mo, int argc,
+				      char *argv[])
+{
+	struct fuse_service_memfd_argv args = {
+		.magic = htonl(FUSE_SERVICE_ARGS_MAGIC),
+	};
+	off_t array_pos = sizeof(struct fuse_service_memfd_argv);
+	off_t string_pos = array_pos +
+			(argc * sizeof(struct fuse_service_memfd_arg));
+	ssize_t written;
+	int i;
+	int ret;
+
+	if (argc < 0) {
+		fprintf(stderr, "mount.service: argc cannot be negative\n");
+		return -1;
+	}
+
+	/*
+	 * Create the memfd in which we'll stash arguments, and set the write
+	 * pointer for the names.
+	 */
+	mo->argvfd = memfd_create("mount.service args", MFD_CLOEXEC);
+	if (mo->argvfd < 0) {
+		perror("mount.service: argvfd create");
+		return -1;
+	}
+
+	/*
+	 * Write the alleged subtype as if it were argv[0], then write the rest
+	 * of the argv arguments.
+	 */
+	ret = mount_service_capture_arg(mo, &args, mo->subtype, &array_pos,
+					&string_pos);
+	if (ret)
+		return ret;
+
+	for (i = 1; i < argc; i++) {
+		/* skip the -t(ype) argument */
+		if (!strcmp(argv[i], "-t")) {
+			i++;
+			continue;
+		}
+
+		ret = mount_service_capture_arg(mo, &args, argv[i],
+						&array_pos, &string_pos);
+		if (ret)
+			return ret;
+	}
+
+	/* Now write the header */
+	args.argc = htonl(args.argc);
+	written = pwrite(mo->argvfd, &args, sizeof(args), 0);
+	if (written < 0) {
+		perror("mount.service: memfd argv write");
+		return -1;
+	}
+	if (written < sizeof(args)) {
+		fprintf(stderr, "mount.service: memfd argv wrote %zd\n",
+			written);
+		return -1;
+	}
+
+	return 0;
+}
+
+static ssize_t __send_fd(int sockfd, struct fuse_service_requested_file *req,
+			 size_t req_sz, int fd)
+{
+	union {
+		struct cmsghdr cmsghdr;
+		char control[CMSG_SPACE(sizeof(int))];
+	} cmsgu;
+	struct iovec iov = {
+		.iov_base = req,
+		.iov_len = req_sz,
+	};
+	struct msghdr msg = {
+		.msg_iov = &iov,
+		.msg_iovlen = 1,
+		.msg_control = cmsgu.control,
+		.msg_controllen = sizeof(cmsgu.control),
+	};
+	struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
+
+	memset(&cmsgu, 0, sizeof(cmsgu));
+	cmsg->cmsg_len = CMSG_LEN(sizeof (int));
+	cmsg->cmsg_level = SOL_SOCKET;
+	cmsg->cmsg_type = SCM_RIGHTS;
+
+	*((int *)CMSG_DATA(cmsg)) = fd;
+
+	return sendmsg(sockfd, &msg, MSG_EOR | MSG_NOSIGNAL);
+}
+
+static int mount_service_send_file(struct mount_service *mo,
+				   const char *path, int fd)
+{
+	struct fuse_service_requested_file *req;
+	const size_t req_sz =
+			sizeof_fuse_service_requested_file(strlen(path));
+	ssize_t written;
+	int ret = 0;
+
+	req = malloc(req_sz);
+	if (!req) {
+		perror("mount.service: alloc send file reply");
+		return -1;
+	}
+	req->p.magic = htonl(FUSE_SERVICE_OPEN_REPLY);
+	req->error = 0;
+	strcpy(req->path, path);
+
+	written = __send_fd(mo->sockfd, req, req_sz, fd);
+	if (written < 0) {
+		perror("mount.service: send file reply");
+		ret = -1;
+	}
+
+	free(req);
+	return ret;
+}
+
+static ssize_t __send_packet(int sockfd, void *buf, ssize_t buflen)
+{
+	struct iovec iov = {
+		.iov_base = buf,
+		.iov_len = buflen,
+	};
+	struct msghdr msg = {
+		.msg_iov = &iov,
+		.msg_iovlen = 1,
+	};
+
+	return sendmsg(sockfd, &msg, MSG_EOR | MSG_NOSIGNAL);
+}
+
+static int mount_service_send_file_error(struct mount_service *mo, int error,
+					 const char *path)
+{
+	struct fuse_service_requested_file *req;
+	const size_t req_sz =
+			sizeof_fuse_service_requested_file(strlen(path));
+	ssize_t written;
+	int ret = 0;
+
+	req = malloc(req_sz);
+	if (!req) {
+		perror("mount.service: alloc send file error");
+		return -1;
+	}
+	req->p.magic = htonl(FUSE_SERVICE_OPEN_REPLY);
+	req->error = htonl(error);
+	strcpy(req->path, path);
+
+	written = __send_packet(mo->sockfd, req, req_sz);
+	if (written < 0) {
+		perror("mount.service: send file error");
+		ret = -1;
+	}
+
+	free(req);
+	return ret;
+}
+
+static int mount_service_send_required_files(struct mount_service *mo,
+					     const char *fusedev)
+{
+	int ret;
+
+	mo->fusedevfd = open(fusedev, O_RDWR | O_CLOEXEC);
+	if (mo->fusedevfd < 0) {
+		perror(fusedev);
+		return -1;
+	}
+
+	ret = mount_service_send_file(mo, FUSE_SERVICE_ARGV, mo->argvfd);
+	close(mo->argvfd);
+	mo->argvfd = -1;
+	if (ret)
+		return ret;
+
+	return mount_service_send_file(mo, FUSE_SERVICE_FUSEDEV,
+				       mo->fusedevfd);
+}
+
+static int
+mount_service_receive_command(struct mount_service *mo,
+			      struct fuse_service_packet **commandp)
+{
+	struct iovec iov = {
+	};
+	struct msghdr msg = {
+		.msg_iov = &iov,
+		.msg_iovlen = 1,
+	};
+	struct fuse_service_packet *command;
+	ssize_t size;
+
+	size = recvmsg(mo->sockfd, &msg, MSG_PEEK | MSG_TRUNC);
+	if (size < 0) {
+		perror("mount.service: peek service command");
+		return -1;
+	}
+	if (size == 0) {
+		/* fuse server probably exited early */
+		return -1;
+	}
+	if (size < sizeof(struct fuse_service_packet)) {
+		fprintf(stderr,
+ "mount.service: wrong command packet size %zd, expected at least %zd\n",
+			size, sizeof(struct fuse_service_packet));
+		return -1;
+	}
+
+	command = calloc(1, size + 1);
+	if (!command) {
+		perror("mount.service: alloc service command");
+		return -1;
+	}
+	iov.iov_base = command;
+	iov.iov_len = size;
+
+	size = recvmsg(mo->sockfd, &msg, MSG_TRUNC);
+	if (size < 0) {
+		perror("mount.service: receive service command");
+		return -1;
+	}
+	if (size != iov.iov_len) {
+		fprintf(stderr,
+ "mount.service: wrong service command size %zd, expected %zd\n",
+			size, iov.iov_len);
+		return -1;
+	}
+
+	*commandp = command;
+	return 0;
+}
+
+static int mount_service_send_reply(struct mount_service *mo, int error)
+{
+	struct fuse_service_simple_reply reply = {
+		.p.magic = htonl(FUSE_SERVICE_SIMPLE_REPLY),
+		.error = htonl(error),
+	};
+	struct iovec iov = {
+		.iov_base = &reply,
+		.iov_len = sizeof(reply),
+	};
+	struct msghdr msg = {
+		.msg_iov = &iov,
+		.msg_iovlen = 1,
+	};
+	ssize_t size;
+
+	size = sendmsg(mo->sockfd, &msg, MSG_EOR | MSG_NOSIGNAL);
+	if (size < 0) {
+		perror("mount.service: send service reply");
+		return -1;
+	}
+
+	return 0;
+}
+
+static int prepare_bdev(struct fuse_service_open_command *oc, int fd)
+{
+	struct stat statbuf;
+	int block_size;
+	int ret;
+
+	ret = fstat(fd, &statbuf);
+	if (ret) {
+		perror(oc->path);
+		return errno;
+	}
+
+	if (!S_ISBLK(statbuf.st_mode)) {
+		fprintf(stderr, "%s: not a block device\n", oc->path);
+		return ENOTBLK;
+	}
+
+	if (!oc->block_size)
+		return 0;
+	block_size = ntohl(oc->block_size);
+
+	ret = ioctl(fd, BLKBSZSET, &block_size);
+	if (ret) {
+		perror(oc->path);
+		return errno;
+	}
+
+	return 0;
+}
+
+static int mount_service_handle_open_path(struct mount_service *mo, mode_t mode,
+					  struct fuse_service_packet *p)
+{
+	struct fuse_service_open_command *oc =
+			container_of(p, struct fuse_service_open_command, p);
+	uint32_t request_flags = ntohl(oc->request_flags);
+	int ret;
+	int fd;
+
+	if (request_flags & ~FUSE_SERVICE_OPEN_FLAGS)
+		return mount_service_send_file_error(mo, EINVAL, oc->path);
+
+	fd = open(oc->path, ntohl(oc->open_flags), ntohl(oc->create_mode));
+	if (fd < 0) {
+		int error = errno;
+
+		/*
+		 * Don't print a busy device error report because the
+		 * filesystem might decide to retry.
+		 */
+		if (errno != EBUSY)
+			perror(oc->path);
+		return mount_service_send_file_error(mo, error, oc->path);
+	}
+
+	if (S_ISBLK(mode)) {
+		ret = prepare_bdev(oc, fd);
+		if (ret) {
+			close(fd);
+			return mount_service_send_file_error(mo, ret,
+							     oc->path);
+		}
+	}
+
+	ret = mount_service_send_file(mo, oc->path, fd);
+	close(fd);
+	return ret;
+}
+
+static int mount_service_handle_open_cmd(struct mount_service *mo,
+					 struct fuse_service_packet *p)
+{
+	return mount_service_handle_open_path(mo, 0, p);
+}
+
+static int mount_service_handle_open_bdev_cmd(struct mount_service *mo,
+					      struct fuse_service_packet *p)
+{
+	return mount_service_handle_open_path(mo, S_IFBLK, p);
+}
+
+static int
+mount_service_handle_fsopen_cmd(struct mount_service *mo,
+				const struct fuse_service_packet *p)
+{
+	struct fuse_service_string_command *oc =
+			container_of(p, struct fuse_service_string_command, p);
+
+	mo->fsopenfd = -1;
+#if 0
+	mo->fsopenfd = fsopen(oc->value, FSOPEN_CLOEXEC);
+#endif
+	if (mo->fsopenfd >= 0)
+		return mount_service_send_reply(mo, 0);
+
+	if (mo->fstype) {
+		fprintf(stderr, "mount.service: fstype respecified!\n");
+		mount_service_send_reply(mo, EINVAL);
+		return -1;
+	}
+
+	mo->fstype = strdup(oc->value);
+	if (!mo->fstype) {
+		perror("mount.service: alloc fstype string");
+		mount_service_send_reply(mo, errno);
+		return -1;
+	}
+
+	return mount_service_send_reply(mo, 0);
+}
+
+static int
+mount_service_handle_source_cmd(struct mount_service *mo,
+				const struct fuse_service_packet *p)
+{
+	struct fuse_service_string_command *oc =
+			container_of(p, struct fuse_service_string_command, p);
+	int ret;
+
+	if (mo->fsopenfd < 0) {
+		if (mo->source) {
+			fprintf(stderr, "mount.service: source respecified!\n");
+			mount_service_send_reply(mo, EINVAL);
+			return -1;
+		}
+
+		mo->source = strdup(oc->value);
+		if (!mo->source) {
+			perror("mount.service: alloc source string");
+			mount_service_send_reply(mo, errno);
+			return -1;
+		}
+
+		return mount_service_send_reply(mo, 0);
+	}
+
+	ret = fsconfig(mo->fsopenfd, FSCONFIG_SET_STRING, "source", oc->value,
+		       0);
+	if (ret) {
+		perror("mount.service: fsconfig source");
+		mount_service_send_reply(mo, errno);
+		return -1;
+	}
+
+	return mount_service_send_reply(mo, 0);
+}
+
+static int
+mount_service_handle_mntopts_cmd(struct mount_service *mo,
+				 const struct fuse_service_packet *p)
+{
+	struct fuse_service_string_command *oc =
+			container_of(p, struct fuse_service_string_command, p);
+	char *tokstr = oc->value;
+	char *tok, *savetok;
+	int ret;
+
+	if (mo->fsopenfd < 0) {
+		if (mo->mntopts) {
+			fprintf(stderr,
+ "mount.service: mount options respecified!\n");
+			mount_service_send_reply(mo, EINVAL);
+			return -1;
+		}
+
+		mo->mntopts = strdup(oc->value);
+		if (!mo->mntopts) {
+			perror("mount.service: alloc mount options string");
+			mount_service_send_reply(mo, errno);
+			return -1;
+		}
+
+		return mount_service_send_reply(mo, 0);
+	}
+
+	while ((tok = strtok_r(tokstr, ",", &savetok)) != NULL) {
+		char *equals = strchr(tok, '=');
+
+		if (equals) {
+			char oldchar = *equals;
+
+			*equals = 0;
+			ret = fsconfig(mo->fsopenfd, FSCONFIG_SET_STRING, tok,
+				       equals + 1, 0);
+			*equals = oldchar;
+		} else {
+			ret = fsconfig(mo->fsopenfd, FSCONFIG_SET_FLAG, tok,
+				       NULL, 0);
+		}
+		if (ret) {
+			perror("mount.service: set mount option");
+			mount_service_send_reply(mo, errno);
+			return -1;
+		}
+
+		tokstr = NULL;
+	}
+
+	return mount_service_send_reply(mo, 0);
+}
+
+static int
+mount_service_handle_mountpoint_cmd(struct mount_service *mo,
+				    const struct fuse_service_packet *p)
+{
+	struct fuse_service_string_command *oc =
+			container_of(p, struct fuse_service_string_command, p);
+
+	if (mo->mountpoint) {
+		fprintf(stderr, "mount.service: mount point respecified!\n");
+		mount_service_send_reply(mo, EINVAL);
+		return -1;
+	}
+
+	mo->mountpoint = strdup(oc->value);
+	if (!mo->mountpoint) {
+		perror("mount.service: alloc mount point string");
+		mount_service_send_reply(mo, errno);
+		return -1;
+	}
+
+	return mount_service_send_reply(mo, 0);
+}
+
+static inline int format_libfuse_mntopts(char *buf, size_t bufsz,
+					 const struct mount_service *mo,
+					 const struct stat *statbuf)
+{
+	if (mo->mntopts)
+		return snprintf(buf, bufsz,
+				"%s,fd=%i,rootmode=%o,user_id=%u,group_id=%u",
+				mo->mntopts, mo->fusedevfd,
+				statbuf->st_mode & S_IFMT,
+				getuid(), getgid());
+
+	return snprintf(buf, bufsz,
+			"fd=%i,rootmode=%o,user_id=%u,group_id=%u",
+			mo->fusedevfd, statbuf->st_mode & S_IFMT,
+			getuid(), getgid());
+}
+
+static int mount_service_regular_mount(struct mount_service *mo,
+				       struct fuse_service_mount_command *oc,
+				       struct stat *stbuf)
+{
+	char *realmopts;
+	int ret;
+
+	if (!mo->fstype) {
+		fprintf(stderr, "mount.service: missing mount type parameter\n");
+		mount_service_send_reply(mo, EINVAL);
+		return -1;
+	}
+
+	if (!mo->source) {
+		fprintf(stderr, "mount.service: missing mount source parameter\n");
+		mount_service_send_reply(mo, EINVAL);
+		return -1;
+	}
+
+	ret = format_libfuse_mntopts(NULL, 0, mo, stbuf);
+	if (ret < 0) {
+		perror("mount.service: mount option preformatting");
+		mount_service_send_reply(mo, errno);
+		return -1;
+	}
+
+	realmopts = malloc(ret + 1);
+	if (!realmopts) {
+		perror("mount.service: alloc real mount options string");
+		mount_service_send_reply(mo, errno);
+		return -1;
+	}
+
+	ret = format_libfuse_mntopts(realmopts, ret + 1, mo, stbuf);
+	if (ret < 0) {
+		free(realmopts);
+		perror("mount.service: mount options formatting");
+		mount_service_send_reply(mo, errno);
+		return -1;
+	}
+
+	ret = mount(mo->source, mo->mountpoint, mo->fstype, ntohl(oc->flags),
+		    realmopts);
+	free(realmopts);
+	if (ret) {
+		perror("mount.service");
+		mount_service_send_reply(mo, errno);
+		return -1;
+	}
+
+	return mount_service_send_reply(mo, 0);
+}
+
+static int mount_service_fsopen_mount(struct mount_service *mo,
+				      struct fuse_service_mount_command *oc,
+				      struct stat *stbuf)
+{
+	char tmp[64];
+	int mfd;
+	int ret;
+
+	snprintf(tmp, sizeof(tmp), "%i", mo->fusedevfd);
+	ret = fsconfig(mo->fsopenfd, FSCONFIG_SET_STRING, "fd", tmp, 0);
+	if (ret) {
+		perror("mount.service: set fd option");
+		mount_service_send_reply(mo, errno);
+		return -1;
+	}
+
+	snprintf(tmp, sizeof(tmp), "%o", stbuf->st_mode & S_IFMT);
+	ret = fsconfig(mo->fsopenfd, FSCONFIG_SET_STRING, "rootmode", tmp, 0);
+	if (ret) {
+		perror("mount.service: set rootmode option");
+		mount_service_send_reply(mo, errno);
+		return -1;
+	}
+
+	snprintf(tmp, sizeof(tmp), "%u", getuid());
+	ret = fsconfig(mo->fsopenfd, FSCONFIG_SET_STRING, "user_id", tmp, 0);
+	if (ret) {
+		perror("mount.service: set user_id option");
+		mount_service_send_reply(mo, errno);
+		return -1;
+	}
+
+	snprintf(tmp, sizeof(tmp), "%u", getgid());
+	ret = fsconfig(mo->fsopenfd, FSCONFIG_SET_STRING, "group_id", tmp, 0);
+	if (ret) {
+		perror("mount.service: set group_id option");
+		mount_service_send_reply(mo, errno);
+		return -1;
+	}
+
+	mfd = fsmount(mo->fsopenfd, FSMOUNT_CLOEXEC, ntohl(oc->flags));
+	if (mfd < 0) {
+		perror("mount.service");
+		mount_service_send_reply(mo, errno);
+		return -1;
+	}
+
+	ret = move_mount(mfd, "", AT_FDCWD, mo->mountpoint,
+			 MOVE_MOUNT_F_EMPTY_PATH);
+	close(mfd);
+	if (ret) {
+		perror("mount.service: move_mount");
+		mount_service_send_reply(mo, errno);
+		return -1;
+	}
+
+	return mount_service_send_reply(mo, 0);
+}
+
+static int mount_service_handle_mount_cmd(struct mount_service *mo,
+					  struct fuse_service_packet *p)
+{
+	struct stat stbuf;
+	char mountpoint[PATH_MAX] = "";
+	struct fuse_service_mount_command *oc =
+			container_of(p, struct fuse_service_mount_command, p);
+	int ret;
+
+	if (!mo->mountpoint) {
+		fprintf(stderr, "mount.service: missing mount point parameter\n");
+		mount_service_send_reply(mo, EINVAL);
+		return -1;
+	}
+
+	if (realpath(mo->mountpoint, mountpoint) == NULL) {
+		int error = errno;
+
+		fprintf(stderr, "mount.service: bad mount point `%s': %s\n",
+			mo->mountpoint, strerror(error));
+		mount_service_send_reply(mo, error);
+		return -1;
+	}
+
+	ret = stat(mo->mountpoint, &stbuf);
+	if (ret == -1) {
+		perror(mo->mountpoint);
+		mount_service_send_reply(mo, errno);
+		return -1;
+	}
+
+	if (mo->fsopenfd >= 0)
+		return mount_service_fsopen_mount(mo, oc, &stbuf);
+	return mount_service_regular_mount(mo, oc, &stbuf);
+}
+
+static int mount_service_handle_bye_cmd(struct fuse_service_packet *p)
+{
+	int error;
+
+	struct fuse_service_bye_command *bc =
+			container_of(p, struct fuse_service_bye_command, p);
+
+	error = ntohl(bc->error);
+	if (error) {
+		fprintf(stderr, "mount.service: initialization failed: %s\n",
+			strerror(error));
+		return -1;
+	}
+
+	return 0;
+}
+
+static void mount_service_destroy(struct mount_service *mo)
+{
+	close(mo->fusedevfd);
+	close(mo->argvfd);
+	close(mo->fsopenfd);
+	shutdown(mo->sockfd, SHUT_RDWR);
+	close(mo->sockfd);
+
+	free(mo->source);
+	free(mo->mountpoint);
+	free(mo->mntopts);
+	free(mo->fstype);
+
+	memset(mo, 0, sizeof(*mo));
+	mo->fsopenfd = -1;
+	mo->sockfd = -1;
+	mo->argvfd = -1;
+	mo->fusedevfd = -1;
+}
+
+int mount_service_main(int argc, char *argv[])
+{
+	const char *fusedev = getenv(FUSE_KERN_DEVICE_ENV) ?: FUSE_DEV;
+	struct mount_service mo = { };
+	bool running = true;
+	int ret;
+
+	if (argc < 3 || !strcmp(argv[1], "--help")) {
+		printf("Usage: %s source mountpoint -t type [-o options]\n",
+				argv[0]);
+		return EXIT_FAILURE;
+	}
+
+	ret = mount_service_init(&mo, argc, argv);
+	if (ret) {
+		fprintf(stderr, "%s: cannot determine filesystem type.\n",
+			argv[0]);
+		return EXIT_FAILURE;
+	}
+
+	ret = mount_service_connect(&mo);
+	if (ret) {
+		ret = EXIT_FAILURE;
+		goto out;
+	}
+
+	ret = mount_service_send_hello(&mo);
+	if (ret) {
+		ret = EXIT_FAILURE;
+		goto out;
+	}
+
+	ret = mount_service_capture_args(&mo, argc, argv);
+	if (ret) {
+		ret = EXIT_FAILURE;
+		goto out;
+	}
+
+	ret = mount_service_send_required_files(&mo, fusedev);
+	if (ret) {
+		ret = EXIT_FAILURE;
+		goto out;
+	}
+
+	while (running) {
+		struct fuse_service_packet *p = NULL;
+
+		ret = mount_service_receive_command(&mo, &p);
+		if (ret) {
+			ret = EXIT_FAILURE;
+			goto out;
+		}
+
+		switch (ntohl(p->magic)) {
+		case FUSE_SERVICE_OPEN_CMD:
+			ret = mount_service_handle_open_cmd(&mo, p);
+			break;
+		case FUSE_SERVICE_OPEN_BDEV_CMD:
+			ret = mount_service_handle_open_bdev_cmd(&mo, p);
+			break;
+		case FUSE_SERVICE_FSOPEN_CMD:
+			ret = mount_service_handle_fsopen_cmd(&mo, p);
+			break;
+		case FUSE_SERVICE_SOURCE_CMD:
+			ret = mount_service_handle_source_cmd(&mo, p);
+			break;
+		case FUSE_SERVICE_MNTOPTS_CMD:
+			ret = mount_service_handle_mntopts_cmd(&mo, p);
+			break;
+		case FUSE_SERVICE_MNTPT_CMD:
+			ret = mount_service_handle_mountpoint_cmd(&mo, p);
+			break;
+		case FUSE_SERVICE_MOUNT_CMD:
+			ret = mount_service_handle_mount_cmd(&mo, p);
+			break;
+		case FUSE_SERVICE_BYE_CMD:
+			ret = mount_service_handle_bye_cmd(p);
+			running = false;
+			break;
+		default:
+			fprintf(stderr, "unrecognized packet 0x%x\n",
+				ntohl(p->magic));
+			ret = EXIT_FAILURE;
+			break;
+		}
+		free(p);
+
+		if (ret) {
+			ret = EXIT_FAILURE;
+			goto out;
+		}
+	}
+
+	ret = EXIT_SUCCESS;
+out:
+	mount_service_destroy(&mo);
+	return ret;
+}


^ permalink raw reply related	[flat|nested] 4+ messages in thread

* [PATCH 2/3] libfuse: integrate fuse services into mount.fuse3
  2026-03-04  0:10 [PATCHSET v8] libfuse: run fuse servers as a contained service Darrick J. Wong
  2026-03-04  0:10 ` [PATCH 1/3] libfuse: add systemd/inetd socket service mounting helper Darrick J. Wong
@ 2026-03-04  0:10 ` Darrick J. Wong
  2026-03-04  0:10 ` [PATCH 3/3] example/service_ll: create a sample systemd service fuse server Darrick J. Wong
  2 siblings, 0 replies; 4+ messages in thread
From: Darrick J. Wong @ 2026-03-04  0:10 UTC (permalink / raw)
  To: bschubert, djwong; +Cc: miklos, neal, joannelkoong, linux-fsdevel, bernd

From: Darrick J. Wong <djwong@kernel.org>

Teach mount.fuse3 how to start fuse via service, if present.

Signed-off-by: "Darrick J. Wong" <djwong@kernel.org>
---
 util/mount_service.h  |    9 ++++++++
 doc/fuservicemount3.8 |   10 ++++++++
 util/fuservicemount.c |   48 +++++++++++++++++++++++++++++++++++++++++
 util/meson.build      |    4 +++
 util/mount.fuse.c     |   58 +++++++++++++++++++++++++++++++------------------
 util/mount_service.c  |   18 +++++++++++++++
 6 files changed, 124 insertions(+), 23 deletions(-)


diff --git a/util/mount_service.h b/util/mount_service.h
index 16e120da10765e..b211c7ede4e034 100644
--- a/util/mount_service.h
+++ b/util/mount_service.h
@@ -29,4 +29,13 @@ int mount_service_main(int argc, char *argv[]);
  */
 const char *mount_service_subtype(const char *fstype);
 
+/**
+ * Discover if there is a fuse service socket for the given fuse subtype.
+ *
+ * @param subtype subtype of a fuse filesystem type (e.g. Y from
+ *                mount_service_subtype)
+ * @return true if available, false if not
+ */
+bool mount_service_present(const char *subtype);
+
 #endif /* MOUNT_SERVICE_H_ */
diff --git a/doc/fuservicemount3.8 b/doc/fuservicemount3.8
index e45d6a89c8b81a..aa2167cb4872c6 100644
--- a/doc/fuservicemount3.8
+++ b/doc/fuservicemount3.8
@@ -7,12 +7,20 @@ .SH SYNOPSIS
 .B mountpoint
 .BI -t " fstype"
 [
-.I options
+.BI -o " options"
 ]
+
+.B fuservicemount3
+.BI -t " fstype"
+.B --check
+
 .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.
+
+The second form checks if there is a FUSE service available for the given
+filesystem type.
 .SH "AUTHORS"
 .LP
 The author of the fuse socket service code is Darrick J. Wong <djwong@kernel.org>.
diff --git a/util/fuservicemount.c b/util/fuservicemount.c
index 860c512a34e30f..13b80b3150e3bf 100644
--- a/util/fuservicemount.c
+++ b/util/fuservicemount.c
@@ -9,10 +9,58 @@
 /* This program does the mounting of FUSE filesystems that run in systemd */
 
 #define _GNU_SOURCE
+#include <stdbool.h>
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
 #include "fuse_config.h"
 #include "mount_service.h"
 
+static int check_service(const char *fstype)
+{
+	const char *subtype;
+
+	if (!fstype) {
+		fprintf(stderr,
+			"fuservicemount: expected fs type for --check\n");
+		return EXIT_FAILURE;
+	}
+
+	subtype = mount_service_subtype(fstype);
+	return mount_service_present(subtype) ? EXIT_SUCCESS : EXIT_FAILURE;
+}
+
 int main(int argc, char *argv[])
 {
+	char *fstype = NULL;
+	bool check = false;
+	int i;
+
+	/*
+	 * If the user passes us exactly the args -t FSTYPE --check then
+	 * we'll just check if there's a service for the FSTYPE fuse server.
+	 */
+	for (i = 1; i < argc; i++) {
+		if (!strcmp(argv[i], "--check")) {
+			if (check) {
+				check = false;
+				break;
+			}
+			check = true;
+		} else if (!strcmp(argv[i], "-t") && i + 1 < argc) {
+			if (fstype) {
+				check = false;
+				break;
+			}
+			fstype = argv[i + 1];
+			i++;
+		} else {
+			check = false;
+			break;
+		}
+	}
+	if (check)
+		return check_service(fstype);
+
 	return mount_service_main(argc, argv);
 }
diff --git a/util/meson.build b/util/meson.build
index 68d8bb11f92955..3adf395bfb6386 100644
--- a/util/meson.build
+++ b/util/meson.build
@@ -6,7 +6,9 @@ executable('fusermount3', ['fusermount.c', '../lib/mount_util.c', '../lib/util.c
            install_dir: get_option('bindir'),
            c_args: '-DFUSE_CONF="@0@"'.format(fuseconf_path))
 
+mount_fuse3_sources = ['mount.fuse.c']
 if private_cfg.get('HAVE_SERVICEMOUNT', false)
+  mount_fuse3_sources += ['mount_service.c']
   executable('fuservicemount3', ['mount_service.c', 'fuservicemount.c'],
              include_directories: include_dirs,
              link_with: [ libfuse ],
@@ -15,7 +17,7 @@ if private_cfg.get('HAVE_SERVICEMOUNT', false)
              c_args: '-DFUSE_USE_VERSION=317')
 endif
 
-executable('mount.fuse3', ['mount.fuse.c'],
+executable('mount.fuse3', mount_fuse3_sources,
            include_directories: include_dirs,
            link_with: [ libfuse ],
            install: true,
diff --git a/util/mount.fuse.c b/util/mount.fuse.c
index f1a90fe8abae7c..b6a55eebb7f88b 100644
--- a/util/mount.fuse.c
+++ b/util/mount.fuse.c
@@ -49,6 +49,9 @@
 #endif
 
 #include "fuse.h"
+#ifdef HAVE_SERVICEMOUNT
+# include "mount_service.h"
+#endif
 
 static char *progname;
 
@@ -280,9 +283,7 @@ int main(int argc, char *argv[])
 	mountpoint = argv[2];
 
 	for (i = 3; i < argc; i++) {
-		if (strcmp(argv[i], "-v") == 0) {
-			continue;
-		} else if (strcmp(argv[i], "-t") == 0) {
+		if (strcmp(argv[i], "-t") == 0) {
 			i++;
 
 			if (i == argc) {
@@ -303,6 +304,39 @@ int main(int argc, char *argv[])
 					progname);
 				exit(1);
 			}
+		}
+	}
+
+	if (!type) {
+		if (source) {
+			dup_source = xstrdup(source);
+			type = dup_source;
+			source = strchr(type, '#');
+			if (source)
+				*source++ = '\0';
+			if (!type[0]) {
+				fprintf(stderr, "%s: empty filesystem type\n",
+					progname);
+				exit(1);
+			}
+		} else {
+			fprintf(stderr, "%s: empty source\n", progname);
+			exit(1);
+		}
+	}
+
+#ifdef HAVE_SERVICEMOUNT
+	/*
+	 * Now that we know the desired filesystem type, see if we can find
+	 * a socket service implementing that.
+	 */
+	if (mount_service_present(type))
+		return mount_service_main(argc, argv);
+#endif
+
+	for (i = 3; i < argc; i++) {
+		if (strcmp(argv[i], "-v") == 0) {
+			continue;
 		} else	if (strcmp(argv[i], "-o") == 0) {
 			char *opts;
 			char *opt;
@@ -366,24 +400,6 @@ int main(int argc, char *argv[])
 	if (suid)
 		options = add_option("suid", options);
 
-	if (!type) {
-		if (source) {
-			dup_source = xstrdup(source);
-			type = dup_source;
-			source = strchr(type, '#');
-			if (source)
-				*source++ = '\0';
-			if (!type[0]) {
-				fprintf(stderr, "%s: empty filesystem type\n",
-					progname);
-				exit(1);
-			}
-		} else {
-			fprintf(stderr, "%s: empty source\n", progname);
-			exit(1);
-		}
-	}
-
 	if (setuid_name && setuid_name[0]) {
 #ifdef linux
 		if (drop_privileges) {
diff --git a/util/mount_service.c b/util/mount_service.c
index 4bee10bba2b18b..02901443553f89 100644
--- a/util/mount_service.c
+++ b/util/mount_service.c
@@ -1036,3 +1036,21 @@ int mount_service_main(int argc, char *argv[])
 	mount_service_destroy(&mo);
 	return ret;
 }
+
+bool mount_service_present(const char *fstype)
+{
+	struct stat stbuf;
+	char path[PATH_MAX];
+	int ret;
+
+	snprintf(path, sizeof(path), FUSE_SERVICE_SOCKET_DIR "/%s", fstype);
+	ret = stat(path, &stbuf);
+	if (ret)
+		return false;
+
+	if (!S_ISSOCK(stbuf.st_mode))
+		return false;
+
+	ret = access(path, R_OK | W_OK);
+	return ret == 0;
+}


^ permalink raw reply related	[flat|nested] 4+ messages in thread

* [PATCH 3/3] example/service_ll: create a sample systemd service fuse server
  2026-03-04  0:10 [PATCHSET v8] libfuse: run fuse servers as a contained service Darrick J. Wong
  2026-03-04  0:10 ` [PATCH 1/3] libfuse: add systemd/inetd socket service mounting helper Darrick J. Wong
  2026-03-04  0:10 ` [PATCH 2/3] libfuse: integrate fuse services into mount.fuse3 Darrick J. Wong
@ 2026-03-04  0:10 ` Darrick J. Wong
  2 siblings, 0 replies; 4+ messages in thread
From: Darrick J. Wong @ 2026-03-04  0:10 UTC (permalink / raw)
  To: bschubert, djwong; +Cc: miklos, neal, joannelkoong, linux-fsdevel, bernd

From: Darrick J. Wong <djwong@kernel.org>

Create a simple fuse server that can be run as a systemd service.

Signed-off-by: "Darrick J. Wong" <djwong@kernel.org>
---
 example/meson.build          |    7 
 example/service_ll.c         |  823 ++++++++++++++++++++++++++++++++++++++++++
 example/service_ll.socket.in |   16 +
 example/service_ll@.service  |   99 +++++
 meson.build                  |    1 
 5 files changed, 946 insertions(+)
 create mode 100644 example/service_ll.c
 create mode 100644 example/service_ll.socket.in
 create mode 100644 example/service_ll@.service


diff --git a/example/meson.build b/example/meson.build
index 76cf2d96db0349..90730252aa6377 100644
--- a/example/meson.build
+++ b/example/meson.build
@@ -12,6 +12,13 @@ if not platform.endswith('bsd') and platform != 'dragonfly'
     examples += [ 'null' ]
 endif
 
+if platform.endswith('linux')
+    examples += [ 'service_ll' ]
+    configure_file(input: 'service_ll.socket.in',
+                   output: 'service_ll.socket',
+                   configuration: private_cfg)
+endif
+
 threaded_examples = [ 'notify_inval_inode',
                       'invalidate_path',
                       'notify_store_retrieve',
diff --git a/example/service_ll.c b/example/service_ll.c
new file mode 100644
index 00000000000000..6e6518f0107bd3
--- /dev/null
+++ b/example/service_ll.c
@@ -0,0 +1,823 @@
+/*
+  FUSE: Filesystem in Userspace
+  Copyright (C) 2026 Oracle.
+
+  This program can be distributed under the terms of the GNU GPLv2.
+  See the file GPL2.txt.
+*/
+
+/** @file
+ *
+ * minimal example filesystem using low-level API and systemd service api
+ *
+ * Compile with:
+ *
+ *     gcc -Wall service_ll.c `pkg-config fuse3 --cflags --libs` -o service_ll
+ *
+ * Note: If the pkg-config command fails due to the absence of the fuse3.pc
+ *     file, you should configure the path to the fuse3.pc file in the
+ *     PKG_CONFIG_PATH variable.
+ *
+ * ## Source code ##
+ * \include service_ll.c
+ */
+
+#define FUSE_USE_VERSION FUSE_MAKE_VERSION(3, 19)
+
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#endif
+
+#include <fuse_lowlevel.h>
+#include <fuse_service.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <assert.h>
+#include <pthread.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <linux/fs.h>
+#include <linux/stat.h>
+
+struct service_ll {
+	struct fuse_session *se;
+	char *device;
+	uint64_t isize;
+	uint64_t blocks;
+	struct fuse_service *service;
+	int fusedev_fd;
+	int fd;
+	mode_t mode;
+
+	/* really booleans */
+	int debug;
+	int ro;
+	int allow_dio;
+	int sync;
+
+	int dev_index;
+	unsigned int blocksize;
+
+	struct timespec atime;
+	struct timespec mtime;
+
+	pthread_mutex_t lock;
+};
+
+static struct service_ll ll = {
+	.fd = -1,
+	.allow_dio = 1,
+	.mode = S_IFREG | 0444,
+	.lock = PTHREAD_MUTEX_INITIALIZER,
+};
+static const char *file_name = "svc_bdev";
+
+struct service_ll_stat {
+	struct fuse_entry_param entry;
+};
+
+static inline uint64_t round_up(uint64_t b, unsigned int align)
+{
+	unsigned int m;
+
+	if (align == 0)
+		return b;
+	m = b % align;
+	if (m)
+		b += align - m;
+	return b;
+}
+
+static inline uint64_t round_down(uint64_t b, unsigned int align)
+{
+	unsigned int m;
+
+	if (align == 0)
+		return b;
+	m = b % align;
+	return b - m;
+}
+
+static inline uint64_t howmany(uint64_t b, unsigned int align)
+{
+	unsigned int m;
+
+	if (align == 0)
+		return b;
+	m = (b % align) ? 1 : 0;
+	return (b / align) + m;
+}
+
+static inline uint64_t b_to_fsbt(uint64_t off)
+{
+	return off / ll.blocksize;
+}
+
+static inline uint64_t b_to_fsb(uint64_t off)
+{
+	return (off + ll.blocksize - 1) / ll.blocksize;
+}
+
+static inline uint64_t fsb_to_b(uint64_t fsb)
+{
+	return fsb * ll.blocksize;
+}
+
+static int service_stat(fuse_ino_t ino, struct service_ll_stat *llstat)
+{
+	struct fuse_entry_param *entry = &llstat->entry;
+	struct stat *stbuf = &entry->attr;
+
+	stbuf->st_ino = ino;
+	switch (ino) {
+	case 1:
+		stbuf->st_mode = S_IFDIR | 0755;
+		stbuf->st_nlink = 2;
+		break;
+
+	case 2:
+		stbuf->st_mode = ll.mode;
+		stbuf->st_nlink = 1;
+		stbuf->st_size = ll.isize;
+		stbuf->st_blksize = ll.blocksize;
+		stbuf->st_blocks = howmany(ll.isize, 512);
+		stbuf->st_atim = ll.atime;
+		stbuf->st_mtim = ll.mtime;
+		break;
+
+	default:
+		return ENOENT;
+	}
+
+	entry->generation = ino + 1;
+	entry->attr_timeout = 0.0;
+	entry->entry_timeout = 0.0;
+	entry->ino = ino;
+
+	return 0;
+}
+
+#if defined(STATX_BASIC_STATS)
+static inline void service_set_statx_attr(struct statx *stx,
+					    uint64_t statx_flag, int set)
+{
+	if (set)
+		stx->stx_attributes |= statx_flag;
+	stx->stx_attributes_mask |= statx_flag;
+}
+
+static void service_statx_directio(struct statx *stx)
+{
+	struct statx devx;
+	int ret;
+
+	ret = statx(ll.fd, "", AT_EMPTY_PATH, STATX_DIOALIGN, &devx);
+	if (ret)
+		return;
+	if (!(devx.stx_mask & STATX_DIOALIGN))
+		return;
+
+	stx->stx_mask |= STATX_DIOALIGN;
+	stx->stx_dio_mem_align = devx.stx_dio_mem_align;
+	stx->stx_dio_offset_align = devx.stx_dio_offset_align;
+}
+
+static int service_statx(fuse_ino_t ino, int statx_mask, struct statx *stx)
+{
+	(void)statx_mask;
+
+	stx->stx_mask = STATX_BASIC_STATS;
+	stx->stx_ino = ino;
+	switch (ino) {
+	case 1:
+		stx->stx_mode = S_IFDIR | 0755;
+		stx->stx_nlink = 2;
+		break;
+
+	case 2:
+		stx->stx_mode = ll.mode;
+		stx->stx_nlink = 1;
+		stx->stx_size = ll.isize;
+		stx->stx_blksize = ll.blocksize;
+		stx->stx_blocks = howmany(ll.isize, 512);
+		stx->stx_atime.tv_sec = ll.atime.tv_sec;
+		stx->stx_atime.tv_nsec = ll.atime.tv_nsec;
+		stx->stx_mtime.tv_sec = ll.mtime.tv_sec;
+		stx->stx_mtime.tv_nsec = ll.mtime.tv_nsec;
+		break;
+
+	default:
+		return ENOENT;
+	}
+
+	service_set_statx_attr(stx, STATX_ATTR_IMMUTABLE, ll.ro);
+	service_statx_directio(stx);
+
+	return 0;
+}
+
+static void service_ll_statx(fuse_req_t req, fuse_ino_t ino, int flags,
+			       int mask, struct fuse_file_info *fi)
+{
+	struct statx stx = { };
+	int ret = 0;
+
+	(void)flags;
+	(void)fi;
+
+	pthread_mutex_lock(&ll.lock);
+	ret = service_statx(ino, mask, &stx);
+	pthread_mutex_unlock(&ll.lock);
+	if (ret)
+		fuse_reply_err(req, ret);
+	else
+		fuse_reply_statx(req, 0, &stx, 0.0);
+}
+#else
+# define service_ll_statx		NULL
+#endif /* STATX_BASIC_STATS */
+
+static void service_ll_statfs(fuse_req_t req, fuse_ino_t ino)
+{
+	struct statvfs buf;
+
+	(void)ino;
+
+	pthread_mutex_lock(&ll.lock);
+	buf.f_bsize = ll.blocksize;
+	buf.f_frsize = 0;
+
+	buf.f_blocks = ll.blocks;
+	buf.f_bfree = 0;
+	buf.f_bavail = 0;
+	buf.f_files = 1;
+	buf.f_ffree = 0;
+	buf.f_favail = 0;
+	buf.f_fsid = 0x50C00L;
+	buf.f_flag = 0;
+	if (ll.ro)
+		buf.f_flag |= ST_RDONLY;
+	buf.f_namemax = 255;
+	pthread_mutex_unlock(&ll.lock);
+
+	fuse_reply_statfs(req, &buf);
+}
+
+static void service_ll_init(void *userdata, struct fuse_conn_info *conn)
+{
+	(void)userdata;
+
+	conn->time_gran = 1;
+}
+
+static void service_ll_getattr(fuse_req_t req, fuse_ino_t ino,
+				 struct fuse_file_info *fi)
+{
+	struct service_ll_stat llstat;
+	int ret;
+
+	(void) fi;
+
+	memset(&llstat, 0, sizeof(llstat));
+	pthread_mutex_lock(&ll.lock);
+	ret = service_stat(ino, &llstat);
+	pthread_mutex_unlock(&ll.lock);
+	if (ret)
+		fuse_reply_err(req, ret);
+	else
+		fuse_reply_attr(req, &llstat.entry.attr,
+				llstat.entry.attr_timeout);
+}
+
+static void service_ll_setattr(fuse_req_t req, fuse_ino_t ino,
+				 struct stat *attr, int to_set,
+				 struct fuse_file_info *fi)
+{
+	pthread_mutex_lock(&ll.lock);
+	if (to_set & FUSE_SET_ATTR_MODE)
+		ll.mode = attr->st_mode;
+	if (to_set & FUSE_SET_ATTR_ATIME)
+		ll.atime = attr->st_atim;
+	if (to_set & FUSE_SET_ATTR_MTIME)
+		ll.mtime = attr->st_mtim;
+	pthread_mutex_unlock(&ll.lock);
+
+	service_ll_getattr(req, ino, fi);
+}
+
+static void service_ll_lookup(fuse_req_t req, fuse_ino_t parent,
+				const char *name)
+{
+	struct service_ll_stat llstat;
+	int ret = ENOENT;
+
+	if (parent != 1 || strcmp(name, file_name) != 0)
+		goto enoent;
+
+	memset(&llstat, 0, sizeof(llstat));
+	pthread_mutex_lock(&ll.lock);
+	ret = service_stat(2, &llstat);
+	pthread_mutex_unlock(&ll.lock);
+	if (ret)
+		goto enoent;
+
+	fuse_reply_entry(req, &llstat.entry);
+	return;
+
+enoent:
+	fuse_reply_err(req, ret);
+}
+
+struct dirbuf {
+	char *p;
+	size_t size;
+};
+
+static void dirbuf_add(fuse_req_t req, struct dirbuf *b, const char *name,
+		       fuse_ino_t ino)
+{
+	struct stat stbuf;
+	size_t oldsize = b->size;
+	b->size += fuse_add_direntry(req, NULL, 0, name, NULL, 0);
+	b->p = (char *) realloc(b->p, b->size);
+	memset(&stbuf, 0, sizeof(stbuf));
+	stbuf.st_ino = ino;
+	fuse_add_direntry(req, b->p + oldsize, b->size - oldsize, name, &stbuf,
+			  b->size);
+}
+
+#define max(x, y) ((x) > (y) ? (x) : (y))
+#define min(x, y) ((x) < (y) ? (x) : (y))
+
+static int reply_buf_limited(fuse_req_t req, const char *buf, size_t bufsize,
+			     off_t off, size_t maxsize)
+{
+	if (off < bufsize)
+		return fuse_reply_buf(req, buf + off,
+				      min(bufsize - off, maxsize));
+	else
+		return fuse_reply_buf(req, NULL, 0);
+}
+
+static void service_ll_readdir(fuse_req_t req, fuse_ino_t ino, size_t size,
+				 off_t off, struct fuse_file_info *fi)
+{
+	(void) fi;
+
+	if (ino != 1)
+		fuse_reply_err(req, ENOTDIR);
+	else {
+		struct dirbuf b;
+
+		memset(&b, 0, sizeof(b));
+		dirbuf_add(req, &b, ".", 1);
+		dirbuf_add(req, &b, "..", 1);
+		dirbuf_add(req, &b, file_name, 2);
+		reply_buf_limited(req, b.p, b.size, off, size);
+		free(b.p);
+	}
+}
+
+static void service_ll_open(fuse_req_t req, fuse_ino_t ino,
+			      struct fuse_file_info *fi)
+{
+	if (ino != 2)
+		fuse_reply_err(req, EISDIR);
+	else if (ll.ro && (fi->flags & O_ACCMODE) != O_RDONLY)
+		fuse_reply_err(req, EACCES);
+	else
+		fuse_reply_open(req, fi);
+}
+
+static void service_ll_fsync(fuse_req_t req, fuse_ino_t ino, int datasync,
+			       struct fuse_file_info *fp)
+{
+	int ret = 0;
+
+	(void)datasync;
+	(void)fp;
+
+	if (ino == 2) {
+		ret = fsync(ll.fd);
+		if (ret)
+			ret = -errno;
+	}
+
+	fuse_reply_err(req, ret);
+}
+
+static void service_ll_read(fuse_req_t req, fuse_ino_t ino, size_t count,
+			    off_t pos, struct fuse_file_info *fp)
+{
+	void *buf = NULL;
+	ssize_t got;
+	int ret;
+
+	if (ino != 2) {
+		ret = EIO;
+		goto out_reply;
+	}
+
+	if (ll.debug)
+		fprintf(stderr, "%s: pos 0x%llx count 0x%llx\n",
+			__func__,
+			(unsigned long long)pos,
+			(unsigned long long)count);
+
+	if (!ll.allow_dio && fp->direct_io) {
+		ret = ENOSYS;
+		goto out_reply;
+	}
+
+	buf = malloc(count);
+	if (!buf) {
+		ret = ENOMEM;
+		goto out_reply;
+	}
+
+	got = pread(ll.fd, buf, count, pos);
+	if (got < 0) {
+		ret = -errno;
+		goto out_reply;
+	}
+
+	fuse_reply_buf(req, buf, got);
+	goto out_buf;
+
+out_reply:
+	fuse_reply_err(req, ret);
+out_buf:
+	free(buf);
+}
+
+static void service_ll_write(fuse_req_t req, fuse_ino_t ino, const char *buf,
+			     size_t count, off_t pos,
+			     struct fuse_file_info *fp)
+{
+	ssize_t got;
+	int ret;
+
+	if (ino != 2) {
+		ret = EIO;
+		goto out_reply;
+	}
+
+	if (ll.debug)
+		fprintf(stderr, "%s: pos 0x%llx count 0x%llx\n",
+			__func__,
+			(unsigned long long)pos,
+			(unsigned long long)count);
+
+	if (!ll.allow_dio && fp->direct_io) {
+		ret = ENOSYS;
+		goto out_reply;
+	}
+
+	if (pos >= ll.isize) {
+		ret = EFBIG;
+		goto out_reply;
+	}
+
+	if (pos >= ll.isize - count)
+		count = ll.isize - pos;
+
+	got = pwrite(ll.fd, buf, count, pos);
+	if (got < 0) {
+		ret = -errno;
+		goto out_reply;
+	}
+
+	if (ll.sync) {
+		ret = fsync(ll.fd);
+		if (ret < 0) {
+			ret = -errno;
+			goto out_reply;
+		}
+	}
+
+	fuse_reply_write(req, got);
+	return;
+
+out_reply:
+	fuse_reply_err(req, ret);
+}
+
+static const struct fuse_lowlevel_ops service_ll_oper = {
+	.init		= service_ll_init,
+	.lookup		= service_ll_lookup,
+	.getattr	= service_ll_getattr,
+	.setattr	= service_ll_setattr,
+	.readdir	= service_ll_readdir,
+	.open		= service_ll_open,
+	.fsync		= service_ll_fsync,
+	.statfs		= service_ll_statfs,
+	.statx		= service_ll_statx,
+	.read		= service_ll_read,
+	.write		= service_ll_write,
+};
+
+enum {
+	SERVICE_LL_SIZE,
+	SERVICE_LL_BLOCKSIZE,
+};
+
+#define SERVICE_LL_OPT(t, p, v) { t, offsetof(struct service_ll, p), v }
+
+static struct fuse_opt service_ll_opts[] = {
+	SERVICE_LL_OPT("debug",		debug,			1),
+	SERVICE_LL_OPT("ro",		ro,			1),
+	SERVICE_LL_OPT("rw",		ro,			0),
+	SERVICE_LL_OPT("dio",		allow_dio,		1),
+	SERVICE_LL_OPT("nodio",		allow_dio,		0),
+	SERVICE_LL_OPT("sync",		sync,			1),
+	SERVICE_LL_OPT("nosync",	sync,			0),
+	FUSE_OPT_KEY("size=%s",		SERVICE_LL_SIZE),
+	FUSE_OPT_KEY("blocksize=%s",	SERVICE_LL_BLOCKSIZE),
+	FUSE_OPT_END
+};
+
+static unsigned long long parse_num_blocks2(const char *arg, int log_block_size)
+{
+	char *p;
+	unsigned long long num;
+
+	num = strtoull(arg, &p, 0);
+
+	if (p[0] && p[1])
+		return 0;
+
+	switch (*p) {		/* Using fall-through logic */
+	case 'T': case 't':
+		num <<= 10;
+		/* fallthrough */
+	case 'G': case 'g':
+		num <<= 10;
+		/* fallthrough */
+	case 'M': case 'm':
+		num <<= 10;
+		/* fallthrough */
+	case 'K': case 'k':
+		if (log_block_size < 0)
+			num <<= 10;
+		else
+			num >>= log_block_size;
+		break;
+	case 's':
+		if (log_block_size < 0)
+			num <<= 9;
+		else
+			num >>= (1+log_block_size);
+		break;
+	case '\0':
+		break;
+	default:
+		return 0;
+	}
+	return num;
+}
+
+static int service_ll_opt_proc(void *data, const char *arg, int key,
+				 struct fuse_args *outargs)
+{
+	(void)data;
+	(void)outargs;
+
+	switch (key) {
+	case FUSE_OPT_KEY_NONOPT:
+		if (!ll.device) {
+			ll.device = strdup(arg);
+			return 0;
+		}
+		return 1;
+	case SERVICE_LL_BLOCKSIZE:
+		ll.blocksize = parse_num_blocks2(arg + 10, -1);
+		if (ll.blocksize < 1 || ll.blocksize > INT32_MAX ||
+		    (ll.blocksize & (ll.blocksize - 1)) != 0) {
+			fprintf(stderr,
+ "%s: block size must be power of two between 1 block and 2GB.\n",
+				arg + 10);
+			return -1;
+		}
+
+		/* do not pass through to libfuse */
+		return 0;
+	case SERVICE_LL_SIZE:
+		ll.isize = parse_num_blocks2(arg + 5, -1);
+		if (ll.isize < 1 || (ll.isize & 511) != 0) {
+			fprintf(stderr,
+ "%s: size must be multiple of 512 and larger than zero.\n\n",
+				arg + 5);
+			return -1;
+		}
+
+		/* do not pass through to libfuse */
+		return 0;
+	}
+
+	return 1;
+}
+
+static int service_get_config(void)
+{
+	int open_flags = (ll.ro ? O_RDONLY : O_RDWR) | O_EXCL;
+	int ret;
+
+again:
+	if (ll.blocksize)
+		ret = fuse_service_request_blockdev(ll.service, ll.device,
+						    open_flags, 0, 0,
+						    ll.blocksize);
+	else
+		ret = fuse_service_request_file(ll.service, ll.device,
+						open_flags, 0, 0);
+	if (ret)
+		return ret;
+
+	ret = fuse_service_receive_file(ll.service, ll.device, &ll.fd);
+	if (ret)
+		return ret;
+
+	if (ll.fd < 0 &&
+	    (errno == EPERM || errno == EACCES) &&
+	    (open_flags & O_ACCMODE) != O_RDONLY) {
+		open_flags = O_RDONLY | O_EXCL;
+		goto again;
+	}
+
+	if (ll.fd < 0) {
+		printf("%s: opening device: %s.\n", ll.device,
+		       strerror(errno));
+		return -1;
+	}
+
+	ret = fuse_service_finish_file_requests(ll.service);
+	if (ret)
+		return ret;
+
+	ll.fusedev_fd = fuse_service_take_fusedev(ll.service);
+	return 0;
+}
+
+
+int main(int argc, char *argv[])
+{
+	struct fuse_args args = FUSE_ARGS_INIT(argc, argv);
+	struct fuse_cmdline_opts opts;
+	struct fuse_loop_config *config;
+	struct stat statbuf;
+	unsigned long long bdev_size;
+	int lbasize;
+	int ret = -1;
+
+	if (fuse_service_accept(&ll.service) != 0) {
+		printf("service acceptance failed\n");
+		return 1;
+	}
+	if (!fuse_service_accepted(ll.service)) {
+		printf("service not accepted\n");
+		return 1;
+	}
+	fuse_service_append_args(ll.service, &args);
+
+	if (fuse_opt_parse(&args, &ll, service_ll_opts,
+			   service_ll_opt_proc) != 0) {
+		printf("parsing existing cli options failed\n");
+		return 1;
+	}
+	if (fuse_service_parse_cmdline_opts(&args, &opts) != 0) {
+		printf("parsing service cli options failed\n");
+		return 1;
+	}
+
+	if (opts.show_help) {
+		printf("usage: %s [options] <mountpoint>\n\n", argv[0]);
+		fuse_cmdline_help();
+		fuse_lowlevel_help();
+		ret = 0;
+		goto err_out1;
+	} else if (opts.show_version) {
+		printf("FUSE library version %s\n", fuse_pkgversion());
+		fuse_lowlevel_version();
+		ret = 0;
+		goto err_out1;
+	}
+
+	if (opts.mountpoint == NULL || !ll.device) {
+		printf("usage: %s [options] <device> <mountpoint>\n", argv[0]);
+		printf("       %s --help\n", argv[0]);
+		ret = 1;
+		goto err_out1;
+	}
+
+	ret = service_get_config();
+	if (ret) {
+		printf("could not get service config: %s\n", strerror(-errno));
+		ret = -1;
+		goto err_out1;
+	}
+
+	if (!ll.blocksize)
+		ll.blocksize = sysconf(_SC_PAGESIZE);
+
+	ret = fstat(ll.fd, &statbuf);
+	if (ret) {
+		perror(ll.device);
+		ret = -1;
+		goto err_out1;
+	}
+
+	if (S_ISBLK(statbuf.st_mode)) {
+		ret = ioctl(ll.fd, BLKSSZGET, &lbasize);
+		if (ret) {
+			perror(ll.device);
+			ret = -1;
+			goto err_out1;
+		}
+
+		ret = ioctl(ll.fd, BLKGETSIZE64, &bdev_size);
+		if (ret) {
+			perror(ll.device);
+			ret = -1;
+			goto err_out1;
+		}
+	} else {
+		lbasize = statbuf.st_blksize;
+		bdev_size = statbuf.st_size;
+	}
+	if (lbasize > ll.blocksize) {
+		fprintf(stderr,
+ "%s: lba size %u smaller than blocksize %u\n",
+		       ll.device, lbasize, ll.blocksize);
+		ret = -1;
+		goto err_out1;
+	}
+	if (ll.isize % ll.blocksize > 0) {
+		fprintf(stderr,
+ "%s: size parameter %llu not congruent with blocksize %u\n",
+			ll.device, (unsigned long long)ll.isize,
+			ll.blocksize);
+		ret = -1;
+		goto err_out1;
+	}
+	if (ll.isize > bdev_size) {
+		fprintf(stderr,
+ "%s: block device size %llu smaller than size param %llu\n",
+			ll.device, bdev_size,
+			(unsigned long long)ll.isize);
+		ret = -1;
+		goto err_out1;
+	}
+	if (!ll.isize)
+		ll.isize = bdev_size;
+	ll.isize = round_down(ll.isize, ll.blocksize);
+	ll.blocks = ll.isize / ll.blocksize;
+
+	ll.se = fuse_session_new(&args, &service_ll_oper,
+				 sizeof(service_ll_oper), NULL);
+	if (ll.se == NULL)
+	    goto err_out1;
+
+	if (fuse_set_signal_handlers(ll.se) != 0)
+	    goto err_out2;
+
+	if (fuse_service_session_mount(ll.service, ll.se, &opts) != 0) {
+		printf("%s: could not mount fuse filesystem: %s\n",
+		       ll.device, strerror(errno));
+		ret = -1;
+		goto err_out3;
+	}
+
+	fuse_service_send_goodbye(ll.service, 0);
+	fuse_service_release(ll.service);
+
+	fuse_daemonize(opts.foreground);
+
+	/* Block until ctrl+c or fusermount -u */
+	if (opts.singlethread)
+		ret = fuse_session_loop(ll.se);
+	else {
+		config = fuse_loop_cfg_create();
+		fuse_loop_cfg_set_clone_fd(config, opts.clone_fd);
+		fuse_loop_cfg_set_max_threads(config, opts.max_threads);
+		ret = fuse_session_loop_mt(ll.se, config);
+		fuse_loop_cfg_destroy(config);
+		config = NULL;
+	}
+
+	fuse_session_unmount(ll.se);
+err_out3:
+	fuse_remove_signal_handlers(ll.se);
+err_out2:
+	fuse_session_destroy(ll.se);
+err_out1:
+	fuse_service_destroy(&ll.service);
+	free(opts.mountpoint);
+	free(ll.device);
+	close(ll.fd);
+	fuse_opt_free_args(&args);
+	return fuse_service_exit(ret);
+}
diff --git a/example/service_ll.socket.in b/example/service_ll.socket.in
new file mode 100644
index 00000000000000..16bcaf1f21df36
--- /dev/null
+++ b/example/service_ll.socket.in
@@ -0,0 +1,16 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+#
+# Copyright (C) 2026 Oracle.  All Rights Reserved.
+# Author: Darrick J. Wong <djwong@kernel.org>
+[Unit]
+Description=Socket for service_ll Service
+
+[Socket]
+# Typically the socket path is /run/filesystems/service_ll
+ListenSequentialPacket=@FUSE_SERVICE_SOCKET_DIR_RAW@/service_ll
+Accept=yes
+SocketMode=0660
+RemoveOnStop=yes
+
+[Install]
+WantedBy=sockets.target
diff --git a/example/service_ll@.service b/example/service_ll@.service
new file mode 100644
index 00000000000000..0f460ea88c9fcf
--- /dev/null
+++ b/example/service_ll@.service
@@ -0,0 +1,99 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+#
+# Copyright (C) 2026 Oracle.  All Rights Reserved.
+# Author: Darrick J. Wong <djwong@kernel.org>
+[Unit]
+Description=service_ll Sample Fuse Service
+
+[Service]
+Type=exec
+ExecStart=/path/to/service_ll
+
+# Try to capture core dumps
+LimitCORE=infinity
+
+SyslogIdentifier=%N
+
+# No realtime CPU scheduling
+RestrictRealtime=true
+
+# Don't let us see anything in the regular system, and don't run as root
+DynamicUser=true
+ProtectSystem=strict
+ProtectHome=true
+PrivateTmp=true
+PrivateDevices=true
+PrivateUsers=true
+
+# No network access
+PrivateNetwork=true
+ProtectHostname=true
+RestrictAddressFamilies=none
+IPAddressDeny=any
+
+# Don't let the program mess with the kernel configuration at all
+ProtectKernelLogs=true
+ProtectKernelModules=true
+ProtectKernelTunables=true
+ProtectControlGroups=true
+ProtectProc=invisible
+RestrictNamespaces=true
+RestrictFileSystems=
+
+# Hide everything in /proc, even /proc/mounts
+ProcSubset=pid
+
+# Only allow the default personality Linux
+LockPersonality=true
+
+# No writable memory pages
+MemoryDenyWriteExecute=true
+
+# Don't let our mounts leak out to the host
+PrivateMounts=true
+
+# Restrict system calls to the native arch and only enough to get things going
+SystemCallArchitectures=native
+SystemCallFilter=@system-service
+SystemCallFilter=~@privileged
+SystemCallFilter=~@resources
+
+SystemCallFilter=~@clock
+SystemCallFilter=~@cpu-emulation
+SystemCallFilter=~@debug
+SystemCallFilter=~@module
+SystemCallFilter=~@reboot
+SystemCallFilter=~@swap
+
+SystemCallFilter=~@mount
+
+# libfuse io_uring wants to pin cores and memory
+SystemCallFilter=mbind
+SystemCallFilter=sched_setaffinity
+
+# Leave a breadcrumb if we get whacked by the system call filter
+SystemCallErrorNumber=EL3RST
+
+# Log to the kernel dmesg, just like an in-kernel filesystem driver
+StandardOutput=append:/dev/ttyprintk
+StandardError=append:/dev/ttyprintk
+
+# Run with no capabilities at all
+CapabilityBoundingSet=
+AmbientCapabilities=
+NoNewPrivileges=true
+
+# We don't create files
+UMask=7777
+
+# No access to hardware /dev files at all
+ProtectClock=true
+DevicePolicy=closed
+
+# Don't mess with set[ug]id anything.
+RestrictSUIDSGID=true
+
+# Don't let OOM kills of processes in this containment group kill the whole
+# service, because we don't want filesystem drivers to go down.
+OOMPolicy=continue
+OOMScoreAdjust=-1000
diff --git a/meson.build b/meson.build
index ed05a245b48df2..e9d57216f07170 100644
--- a/meson.build
+++ b/meson.build
@@ -73,6 +73,7 @@ service_socket_dir = get_option('service-socket-dir')
 if service_socket_dir == ''
   service_socket_dir = '/run/filesystems'
 endif
+private_cfg.set('FUSE_SERVICE_SOCKET_DIR_RAW', service_socket_dir)
 private_cfg.set_quoted('FUSE_SERVICE_SOCKET_DIR', service_socket_dir)
 
 # Test for presence of some functions


^ permalink raw reply related	[flat|nested] 4+ messages in thread

end of thread, other threads:[~2026-03-04  0:10 UTC | newest]

Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-03-04  0:10 [PATCHSET v8] libfuse: run fuse servers as a contained service Darrick J. Wong
2026-03-04  0:10 ` [PATCH 1/3] libfuse: add systemd/inetd socket service mounting helper Darrick J. Wong
2026-03-04  0:10 ` [PATCH 2/3] libfuse: integrate fuse services into mount.fuse3 Darrick J. Wong
2026-03-04  0:10 ` [PATCH 3/3] example/service_ll: create a sample systemd service fuse server Darrick J. Wong

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox