FILESYSTEM IN USERSPACE (FUSE) development
 help / color / mirror / Atom feed
From: "Darrick J. Wong" <djwong@kernel.org>
To: bernd@bsbernd.com
Cc: fuse-devel@lists.linux.dev, neal@gompa.dev
Subject: Re: [PATCH v2 03/14] mount_service: refuse paths the user did not name
Date: Mon, 28 Sep 2026 19:26:36 -0700	[thread overview]
Message-ID: <20260929022636.GC6253@frogsfrogsfrogs> (raw)
In-Reply-To: <20260928-mount-service-bound-open-v2-3-0f9f501d05ce@bsbernd.com>

On Mon, Sep 28, 2026 at 01:02:05PM +0200, Bernd Schubert via B4 Relay wrote:
> From: Bernd Schubert <bernd@bsbernd.com>
> 
> fuservicemount3 warns about a path that the user did not name on the
> command line, but still opens it. A server can take a path in a form
> that the helper cannot split, for example "-journal/dev/sdb1" or its
> own option syntax. The administrator can now list such paths in
> /etc/fuse.conf, per filesystem type:
> 
>     service_open_path = ext4 /dev/sd*
> 
> The pattern is matched with fnmatch() and FNM_PATHNAME, so "*" does not
> match "/". A requested path with a "." or ".." component never matches,
> because "*" matches "..", and "/dev/*" would then open "/". The helper
> now refuses any other path with EPERM.
> 
> Assisted-by: LLM
> Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
> ---
>  doc/fuservicemount3.8  | 16 ++++++++
>  doc/mount.fuse3.8      |  7 ++++
>  include/fuse_service.h |  4 +-
>  test/test_fuser_conf.c | 55 ++++++++++++++++++++++++++++
>  util/fuse.conf         | 11 ++++++
>  util/fuser_conf.c      | 99 ++++++++++++++++++++++++++++++++++++++++++++++++++
>  util/fuser_conf.h      |  3 ++
>  util/mount_service.c   | 13 ++++---
>  8 files changed, 202 insertions(+), 6 deletions(-)
> 
> diff --git a/doc/fuservicemount3.8 b/doc/fuservicemount3.8
> index aa2167cb4872..18e285c1ab29 100644
> --- a/doc/fuservicemount3.8
> +++ b/doc/fuservicemount3.8
> @@ -19,6 +19,22 @@ Mount a filesystem using a FUSE server that runs as a socket service.
>  These servers can be contained using the platform's service management
>  framework.
>  
> +The FUSE server may ask fuservicemount3 to open files on its behalf.
> +fuservicemount3 opens a path only in these cases:
> +.IP \- 2
> +The path is a command line argument, for example /srv/disk.img.
> +.IP \- 2
> +The path is the value in a key=value option, for example /dev/sdb1 in
> +"-o journal_dev=/dev/sdb1".
> +.IP \- 2
> +The path directly follows a short option, for example /dev/sdb1 in
> +"-J/dev/sdb1".
> +.IP \- 2
> +A service_open_path line in /etc/fuse.conf lists the path for the filesystem
> +type.
> +.PP
> +It refuses any other request with EPERM.
> +
>  The second form checks if there is a FUSE service available for the given
>  filesystem type.
>  .SH "AUTHORS"
> diff --git a/doc/mount.fuse3.8 b/doc/mount.fuse3.8
> index 2e587458a06e..d55c96139d9f 100644
> --- a/doc/mount.fuse3.8
> +++ b/doc/mount.fuse3.8
> @@ -38,6 +38,13 @@ Allow non-root users to specify the \fBallow_other\fP or
>  \fBallow_root\fP mount options (see below).
>  .TP
>  These limits are enforced by the \fBfusermount3\fP helper, so they can be avoided by filesystems that run as root.
> +.TP
> +\fBservice_open_path = SUBTYPE PATTERN\fP
> +Allow \fBfuservicemount3\fP(8) to open paths that match \fIPATTERN\fP for the
> +server of a service mount of type \fBfuse.\fISUBTYPE\fR, in addition to the
> +paths on the mount command line. \fIPATTERN\fP is an absolute path in which "*"
> +does not match "/". A pattern that matches a directory gives the server every
> +file below it. The line can be repeated.
>  .SH OPTIONS
>  Most of the generic mount options described in \fBmount\fP are
>  supported (\fBro\fP, \fBrw\fP, \fBsuid\fP, \fBnosuid\fP, \fBdev\fP,
> diff --git a/include/fuse_service.h b/include/fuse_service.h
> index d6aedea8f0f8..2114e7772bf5 100644
> --- a/include/fuse_service.h
> +++ b/include/fuse_service.h
> @@ -139,6 +139,8 @@ int fuse_service_parse_cmdline_opts(struct fuse_args *args,
>  
>  /**
>   * Ask the mount.service helper to open a file on behalf of the fuse server.
> + * The helper refuses a path that the mount command line does not name and
> + * fuse.conf does not list; fuse_service_receive_file() then reports -EPERM.
>   *
>   * @param sf service context
>   * @param path the path to file
> @@ -153,7 +155,7 @@ int fuse_service_request_file(const struct fuse_service *sf, const char *path,
>  
>  /**
>   * Ask the mount.service helper to open a block device on behalf of the fuse
> - * server.
> + * server.  The helper refuses the same paths as for a file request.
>   *
>   * @param sf service context
>   * @param path the path to file
> diff --git a/test/test_fuser_conf.c b/test/test_fuser_conf.c
> index 4d974931cf58..6d95fe932039 100644
> --- a/test/test_fuser_conf.c
> +++ b/test/test_fuser_conf.c
> @@ -105,6 +105,59 @@ static int test_trimmed_options(void)
>  	return 0;
>  }
>  
> +static int test_service_open_path(void)
> +{
> +	const char *test = "service_open_path";
> +
> +	if (write_conf("service_open_path = ext4 /dev/sd*\n"
> +		       "service_open_path = ext4 /dev/nvme*\n"
> +		       "service_open_path = ext4 relative/path\n"
> +		       "service_open_path = xfs\t/srv/xfs.img\n"
> +		       "service_open_path = xfs /srv/img/*\n"
> +		       " \tservice_open_path = ext4 /srv/indented.img\n"
> +		       "service_open_path =\x20\n"
> +		       "service_open_path = ext4\n") == -1)
> +		return fail(test, "could not write the config file");
> +
> +	read_conf(progname);
> +
> +	if (!service_open_path_listed("ext4", "/dev/sda"))
> +		return fail(test, "/dev/sd* did not match /dev/sda");
> +	if (!service_open_path_listed("ext4", "/dev/nvme0n1"))
> +		return fail(test, "a second ext4 line was not recognised");
> +	if (!service_open_path_listed("ext4", "/srv/indented.img"))
> +		return fail(test, "an indented line was not recognised");
> +	if (service_open_path_listed("ext4", "/dev/sda/x"))
> +		return fail(test, "* matched a /");
> +	if (service_open_path_listed("xfs", "/dev/sda"))
> +		return fail(test, "an ext4 line matched for xfs");
> +	if (service_open_path_listed("ext4", "relative/path"))
> +		return fail(test, "a relative pattern was accepted");
> +	if (!service_open_path_listed("xfs", "/srv/xfs.img"))
> +		return fail(test, "a tab-separated line was not recognised");
> +	if (!service_open_path_listed("xfs", "/srv/img/a.img"))
> +		return fail(test, "/srv/img/* did not match /srv/img/a.img");
> +	if (service_open_path_listed("xfs", "/srv/img/..") ||
> +	    service_open_path_listed("xfs", "/srv/img/."))
> +		return fail(test, "a . or .. component was accepted");
> +	/* Either line, if stored, would match the empty path */
> +	if (service_open_path_listed("", ""))
> +		return fail(test, "a line without a subtype was accepted");
> +	if (service_open_path_listed("ext4", ""))
> +		return fail(test, "a line without a pattern was accepted");
> +
> +	if (write_conf("\n") == -1)
> +		return fail(test, "could not write the config file");
> +
> +	read_conf(progname);
> +
> +	if (service_open_path_listed("ext4", "/dev/sda"))
> +		return fail(test, "a line survived re-reading the config");
> +
> +	printf("PASS: %s\n", test);
> +	return 0;
> +}
> +
>  int main(void)
>  {
>  	char tempdir[] = "/tmp/test_fuser_conf.XXXXXX";
> @@ -123,6 +176,8 @@ int main(void)
>  		goto out_unlink;
>  	if (test_trimmed_options())
>  		goto out_unlink;
> +	if (test_service_open_path())
> +		goto out_unlink;
>  
>  	printf("All fuse.conf parser tests passed\n");
>  	result = 0;
> diff --git a/util/fuse.conf b/util/fuse.conf
> index ab048e0347b2..2c182ffa9d6a 100644
> --- a/util/fuse.conf
> +++ b/util/fuse.conf
> @@ -15,3 +15,14 @@
>  # equals sign).
>  
>  #mount_max = 1000
> +
> +
> +# service_open_path = <subtype> <pattern> - a FUSE server that runs as a socket
> +# service may ask fuservicemount3 to open paths that match <pattern>, in
> +# addition to the paths on the mount command line. <subtype> is the filesystem
> +# type after "fuse.", <pattern> an absolute path in which "*" does not match
> +# "/". The line can be repeated to allow different patterns and subtypes.
> +# A pattern that matches a directory gives the server every file below it.
> +
> +#service_open_path = ext4 /dev/sd*
> +#service_open_path = ext4 /dev/nvme*
> diff --git a/util/fuser_conf.c b/util/fuser_conf.c
> index 12688f6c42b7..5ec9d263ac2f 100644
> --- a/util/fuser_conf.c
> +++ b/util/fuser_conf.c
> @@ -18,6 +18,7 @@
>  #include <stdio.h>
>  #include <stdlib.h>
>  #include <errno.h>
> +#include <fnmatch.h>
>  #include <mntent.h>
>  #include <unistd.h>
>  #include <sys/fsuid.h>
> @@ -35,6 +36,14 @@ int mount_max = 1000;
>  static uid_t oldfsuid;
>  static gid_t oldfsgid;
>  
> +struct service_open_path {
> +	struct service_open_path *next;
> +	char *subtype;
> +	char *pattern;
> +};
> +
> +static struct service_open_path *service_open_paths;
> +
>  // Older versions of musl libc don't unescape entries in /etc/mtab
>  
>  // unescapes octal sequences like \040 in-place
> @@ -192,12 +201,100 @@ static void strip_line(char *line)
>  		memmove(line, s, strlen(s)+1);
>  }
>  
> +/*
> + * Store one service_open_path line. For the line
> + * "service_open_path = ext4 /dev/sd*", str is "ext4 /dev/sd*".
> + */
> +static void parse_service_open_path(const char *str, int linenum,
> +				    const char *progname)
> +{
> +	/* <subtype> ends at the first blank */
> +	const size_t subtype_len = strcspn(str, " \t");
> +	const char *pattern = str + subtype_len;
> +	struct service_open_path *entry;
> +
> +	/* The rest of the line is <pattern>, blanks inside it included */
> +	pattern += strspn(pattern, " \t");
> +	/* A relative pattern would depend on each user's working directory */
> +	if (!subtype_len || pattern[0] != '/') {
> +		fprintf(stderr,
> +			"%s: invalid service_open_path in %s at line %i\n",
> +			progname, FUSE_CONF, linenum);
> +		return;
> +	}
> +
> +	entry = calloc(1, sizeof(*entry));
> +	if (entry) {
> +		entry->subtype = strndup(str, subtype_len);
> +		entry->pattern = strdup(pattern);
> +	}
> +	/* Going on without the line would refuse paths the admin allowed */
> +	if (!entry || !entry->subtype || !entry->pattern) {
> +		fprintf(stderr, "%s: failed to allocate memory\n", progname);
> +		exit(1);
> +	}
> +
> +	/* Order does not matter, the lookup checks every entry */
> +	entry->next = service_open_paths;
> +	service_open_paths = entry;
> +}
> +
> +/* The config can be read more than once; drop the lines of the last read */
> +static void free_service_open_paths(void)
> +{
> +	while (service_open_paths) {
> +		struct service_open_path *entry = service_open_paths;
> +
> +		service_open_paths = entry->next;
> +		free(entry->subtype);
> +		free(entry->pattern);
> +		free(entry);
> +	}

Might want to null out service_open_paths here to avoid a UAF in case
this function ever gets used anywhere other than exit.

> +}
> +
> +/* @return true if a path component is "." or "..", as in "/srv/img/.." */
> +static bool has_dot_component(const char *path)
> +{
> +	const char *comp = path;
> +
> +	for (;;) {
> +		const size_t len = strcspn(comp, "/");
> +
> +		if ((len == 1 && comp[0] == '.') ||
> +		    (len == 2 && comp[0] == '.' && comp[1] == '.'))
> +			return true;
> +		if (!comp[len])
> +			return false;
> +		comp += len + 1;
> +	}
> +}
> +
> +bool service_open_path_listed(const char *subtype, const char *path)
> +{
> +	const struct service_open_path *entry;
> +
> +	/* "*" also matches "..", which reaches the parent directory */
> +	if (has_dot_component(path))
> +		return false;
> +
> +	for (entry = service_open_paths; entry; entry = entry->next)
> +		if (!strcmp(entry->subtype, subtype) &&

Would you consider allowing "*" for the subtype in the config file?

e.g.

service_open_path = * /proc/cpuinfo

So that we could (say) allowlist things like /proc/pressure that would
allow a fuse server to monitor memory stalls in the calling process'
namespaces and perhaps drop its caches?

(I don't know if PSI info is really useful for anyone, it's just a
thought I had while reading this patch.)

The code changes look good to me, modulo that question above. :)

Thanks for expanding this!

--D

> +		    !fnmatch(entry->pattern, path, FNM_PATHNAME))
> +			return true;
> +
> +	return false;
> +}
> +
>  static void parse_line(const char *line, int linenum, const char *progname)
>  {
>  	int tmp;
> +	int value_pos = -1;
>  
>  	if (strcmp(line, "user_allow_other") == 0)
>  		user_allow_other = 1;
> +	else if (sscanf(line, "service_open_path = %n", &value_pos) == 0 &&
> +		 value_pos >= 0)
> +		parse_service_open_path(line + value_pos, linenum, progname);
>  	else if (sscanf(line, "mount_max = %i", &tmp) == 1) {
>  		if (tmp < -1)
>  			fprintf(stderr,
> @@ -216,6 +313,8 @@ void read_conf(const char *progname)
>  {
>  	FILE *fp = fopen(FUSE_CONF, "r");
>  
> +	free_service_open_paths();
> +
>  	if (fp != NULL) {
>  		int linenum = 1;
>  		char line[256];
> diff --git a/util/fuser_conf.h b/util/fuser_conf.h
> index ea58537cc4c2..ccfc58877100 100644
> --- a/util/fuser_conf.h
> +++ b/util/fuser_conf.h
> @@ -8,6 +8,7 @@
>  #ifndef FUSER_CONF_H_
>  #define FUSER_CONF_H_
>  
> +#include <stdbool.h>
>  #include <sys/vfs.h>
>  #include <sys/stat.h>
>  
> @@ -40,6 +41,8 @@ int count_fuse_fs(const char *progname);
>  
>  void read_conf(const char *progname);
>  
> +bool service_open_path_listed(const char *subtype, const char *path);
> +
>  void drop_privs(void);
>  void restore_privs(void);
>  
> diff --git a/util/mount_service.c b/util/mount_service.c
> index 446f37f61916..b4081d53273e 100644
> --- a/util/mount_service.c
> +++ b/util/mount_service.c
> @@ -847,13 +847,16 @@ static int mount_service_open_path(const struct mount_service *mo,
>  	}
>  
>  	/*
> -	 * The file is opened outside the service sandbox, so report a path
> -	 * the user did not name.
> +	 * The file is opened outside the service sandbox, so only hand out
> +	 * what the user named or fuse.conf lists.
>  	 */
>  	if (!arg_in_cmdline(argc, argv, oc->path) &&
> -	    !option_value_in_cmdline(argc, argv, oc->path))
> -		fprintf(stderr, "%s: %s: warning: file not in command line arguments\n",
> -			mo->msgtag, oc->path);
> +	    !option_value_in_cmdline(argc, argv, oc->path) &&
> +	    !service_open_path_listed(mo->subtype, oc->path)) {
> +		fprintf(stderr, "%s: %s: file must be in command line arguments or in %s\n",
> +			mo->msgtag, oc->path, FUSE_CONF);
> +		return mount_service_send_file_error(mo, EPERM, oc->path);
> +	}
>  
>  	open_flags = ntohl(oc->open_flags) | O_CLOEXEC;
>  	drop_privs();
> 
> -- 
> 2.53.0
> 
> 
> 

  reply	other threads:[~2026-09-29  2:26 UTC|newest]

Thread overview: 26+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-28 11:02 [PATCH v2 00/14] libfuse: Add mount service safety checks and tests Bernd Schubert via B4 Relay
2026-09-28 11:02 ` [PATCH v2 01/14] mount_service: move the command line check into arg_in_cmdline() Bernd Schubert via B4 Relay
2026-09-28 11:02 ` [PATCH v2 02/14] mount_service: warn about paths not named on the command line Bernd Schubert via B4 Relay
2026-09-29  2:10   ` Darrick J. Wong
2026-09-30 11:06     ` Bernd Schubert
2026-09-28 11:02 ` [PATCH v2 03/14] mount_service: refuse paths the user did not name Bernd Schubert via B4 Relay
2026-09-29  2:26   ` Darrick J. Wong [this message]
2026-09-30 11:46     ` Bernd Schubert
2026-09-28 11:02 ` [PATCH v2 04/14] mount_service: use openat to OPEN paths Bernd Schubert via B4 Relay
2026-09-29  2:27   ` Darrick J. Wong
2026-09-28 11:02 ` [PATCH v2 05/14] util: give fuservicemount3 an absolute build-tree runpath Bernd Schubert via B4 Relay
2026-09-28 11:02 ` [PATCH v2 06/14] mount.fuse: free the options on the service mount return path Bernd Schubert via B4 Relay
2026-09-28 11:02 ` [PATCH v2 07/14] example/single_file: take no sector size from a regular backing file Bernd Schubert via B4 Relay
2026-09-28 11:02 ` [PATCH v2 08/14] test: check which files fuservicemount3 opens for the server Bernd Schubert via B4 Relay
2026-09-29  3:52   ` Darrick J. Wong
2026-09-28 11:02 ` [PATCH v2 09/14] test: check what fuservicemount3 refuses Bernd Schubert via B4 Relay
2026-09-29  3:55   ` Darrick J. Wong
2026-09-28 11:02 ` [PATCH v2 10/14] test: mount the service examples through fuservicemount3 Bernd Schubert via B4 Relay
2026-09-28 11:02 ` [PATCH v2 11/14] test: run mkfs.ext4 through the service examples Bernd Schubert via B4 Relay
2026-09-28 11:02 ` [PATCH v2 12/14] fuse_service: bound argc and arg len read from the args memfd Bernd Schubert via B4 Relay
2026-09-29  2:33   ` Darrick J. Wong
2026-09-28 11:02 ` [PATCH v2 13/14] build: move the default service socket directory to /run/fuse Bernd Schubert via B4 Relay
2026-09-29  2:34   ` Darrick J. Wong
2026-09-28 11:02 ` [PATCH v2 14/14] Improve documentation for fuse service mount Bernd Schubert via B4 Relay
2026-09-29  2:43   ` Darrick J. Wong
2026-09-30 13:09     ` Bernd Schubert

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260929022636.GC6253@frogsfrogsfrogs \
    --to=djwong@kernel.org \
    --cc=bernd@bsbernd.com \
    --cc=fuse-devel@lists.linux.dev \
    --cc=neal@gompa.dev \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox