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 09/14] test: check what fuservicemount3 refuses
Date: Mon, 28 Sep 2026 20:55:16 -0700	[thread overview]
Message-ID: <20260929035516.GI6253@frogsfrogsfrogs> (raw)
In-Reply-To: <20260928-mount-service-bound-open-v2-9-0f9f501d05ce@bsbernd.com>

On Mon, Sep 28, 2026 at 01:02:11PM +0200, Bernd Schubert via B4 Relay wrote:
> From: Bernd Schubert <bernd@bsbernd.com>
> 
> fuservicemount3 runs setuid root and acts on requests from a fuse
> server it does not trust. No test covered its checks on the subtype,
> the mount point and its file type, fuseblk for a user who is not root,
> or a server that exits before its goodbye.
> 
> Assisted-by: LLM
> Signed-off-by: Bernd Schubert <bernd@bsbernd.com>

Seems reasonable to me,
Reviewed-by: "Darrick J. Wong" <djwong@kernel.org>

--D

> ---
>  test/cases/lib/service.sh               |  20 ++++--
>  test/cases/mount/service-caps.sh        |  20 ++++++
>  test/cases/mount/service-check.sh       |  38 ++++++++++++
>  test/cases/mount/service-mountpoint.sh  |  36 +++++++++++
>  test/cases/mount/service-nonroot.sh     |  54 +++++++++++++++++
>  test/cases/mount/service-server-exit.sh |  22 +++++++
>  test/test_service.c                     | 104 ++++++++++++++++++++++++--------
>  7 files changed, 266 insertions(+), 28 deletions(-)
> 
> diff --git a/test/cases/lib/service.sh b/test/cases/lib/service.sh
> index d0a0fa0183d0..60c6c72820ef 100644
> --- a/test/cases/lib/service.sh
> +++ b/test/cases/lib/service.sh
> @@ -10,6 +10,8 @@ service_setup()
>  {
>  	service_subtype=$1
>  	service_runs=0
> +	# A case may prefix it, to run the helper as another user
> +	service_helper=("$FUSE_UTIL_DIR/fuservicemount3")
>  	# A case may set it, to add arguments to the helper command line
>  	service_helper_args=()
>  	service_sock=$("$FUSE_TEST_BIN_DIR/test_service" socket-path "$1")
> @@ -33,6 +35,16 @@ service_start()
>  	service_pid=$!
>  	_wait_for 10 "grep -q '^listening' '$log'" ||
>  		_fail "$service_sock never listened"
> +	# connect() needs write permission, and a case may run as another user
> +	chmod 0666 "$service_sock"
> +}
> +
> +# service_stop
> +# Kill an activator that no helper connected to.
> +service_stop()
> +{
> +	kill "$service_pid" 2>/dev/null || true
> +	wait "$service_pid" 2>/dev/null || true
>  }
>  
>  # service_wait_exit
> @@ -50,7 +62,7 @@ service_wait_exit()
>  
>  # service_mount <source> <mountpoint> <case> [args...]
>  # Run fuservicemount3 once against test_service <case> [args...] and reap the
> -# server. Sets service_log.
> +# server. Sets service_log and service_helper_rc.
>  service_mount()
>  {
>  	local source=$1 mnt=$2; shift 2
> @@ -59,9 +71,9 @@ service_mount()
>  	service_runs=$((service_runs + 1))
>  	service_start "$service_log" "$FUSE_TEST_BIN_DIR/test_service" "$@"
>  
> -	# Its exit status depends on the case; the server's line is the verdict.
> -	"$FUSE_UTIL_DIR/fuservicemount3" "$source" "$mnt" \
> -		-t "fuse.$service_subtype" "${service_helper_args[@]}" || true
> +	service_helper_rc=0
> +	"${service_helper[@]}" "$source" "$mnt" -t "fuse.$service_subtype" \
> +		"${service_helper_args[@]}" || service_helper_rc=$?
>  
>  	service_wait_exit
>  }
> diff --git a/test/cases/mount/service-caps.sh b/test/cases/mount/service-caps.sh
> new file mode 100755
> index 000000000000..69bc2bdbef8d
> --- /dev/null
> +++ b/test/cases/mount/service-caps.sh
> @@ -0,0 +1,20 @@
> +#!/usr/bin/env bash
> +# GROUP: mount
> +#
> +# fuservicemount3 run by root offers the fuse server allow_other and fuseblk.
> +
> +_fuse_no_mount_needed=1
> +. "$TEST_LIB/common.sh"
> +
> +_require_linux "fuservicemount3"
> +_require_root
> +_require_fuse_device
> +_require_binary util/fuservicemount3
> +_require_binary test/test_service
> +
> +. "$TEST_LIB/service.sh"
> +
> +service_setup "test-caps-$$"
> +
> +service_mount "$service_subtype" "$TEST_MNT" caps
> +_assert_eq "$(service_result caps)" "allow_other=1 fuseblk=1" "caps as root"
> diff --git a/test/cases/mount/service-check.sh b/test/cases/mount/service-check.sh
> new file mode 100755
> index 000000000000..ec30031d039e
> --- /dev/null
> +++ b/test/cases/mount/service-check.sh
> @@ -0,0 +1,38 @@
> +#!/usr/bin/env bash
> +# GROUP: mount
> +#
> +# fuservicemount3 --check succeeds only for a socket named after the subtype,
> +# and never for a subtype that is a path.
> +
> +_fuse_no_mount_needed=1
> +. "$TEST_LIB/common.sh"
> +
> +_require_linux "fuservicemount3"
> +_require_root
> +_require_binary util/fuservicemount3
> +_require_binary test/test_service
> +
> +. "$TEST_LIB/service.sh"
> +
> +subtype=test-check-$$
> +service_setup "$subtype"
> +
> +# check_rc <fstype>
> +check_rc()
> +{
> +	local rc=0
> +
> +	"$FUSE_UTIL_DIR/fuservicemount3" -t "$1" --check || rc=$?
> +	echo "$rc"
> +}
> +
> +_assert_eq "$(check_rc "fuse.$subtype")" 1 "no socket"
> +
> +touch "$service_sock"
> +_assert_eq "$(check_rc "fuse.$subtype")" 1 "regular file"
> +
> +service_start "$TEST_LOGDIR/fs-check.out" "$FUSE_TEST_BIN_DIR/test_service" caps
> +_assert_eq "$(check_rc "fuse.$subtype")" 0 "listening socket"
> +# The same socket, named through a path
> +_assert_eq "$(check_rc "fuse../$subtype")" 1 "subtype ./$subtype"
> +service_stop
> diff --git a/test/cases/mount/service-mountpoint.sh b/test/cases/mount/service-mountpoint.sh
> new file mode 100755
> index 000000000000..0df4733389e0
> --- /dev/null
> +++ b/test/cases/mount/service-mountpoint.sh
> @@ -0,0 +1,36 @@
> +#!/usr/bin/env bash
> +# GROUP: mount
> +#
> +# The fuse server names the mount point, so fuservicemount3 has to refuse one
> +# that is not on its command line, and one of the wrong file type.
> +
> +_fuse_no_mount_needed=1
> +. "$TEST_LIB/common.sh"
> +
> +_require_linux "fuservicemount3"
> +_require_root
> +_require_fuse_device
> +_require_binary util/fuservicemount3
> +_require_binary test/test_service
> +
> +. "$TEST_LIB/service.sh"
> +
> +file=$TEST_SRC/file
> +
> +touch "$file"
> +service_setup "test-mntpt-$$"
> +
> +service_mount "$service_subtype" "$TEST_MNT" mount dir
> +_assert_eq "$(service_result mount)" 0 "mount dir on a directory"
> +_assert_fstype "$TEST_MNT" "fuse.$service_subtype" fuse
> +umount "$TEST_MNT"
> +
> +service_mount "$service_subtype" "$TEST_MNT" mount-elsewhere "$TEST_SRC"
> +_assert_eq "$(service_result mount)" EINVAL \
> +	"mount point not on the command line"
> +
> +service_mount "$service_subtype" "$TEST_MNT" mount file
> +_assert_eq "$(service_result mount)" EISDIR "mount file on a directory"
> +
> +service_mount "$service_subtype" "$file" mount dir
> +_assert_eq "$(service_result mount)" ENOTDIR "mount dir on a regular file"
> diff --git a/test/cases/mount/service-nonroot.sh b/test/cases/mount/service-nonroot.sh
> new file mode 100755
> index 000000000000..0c211d0a9965
> --- /dev/null
> +++ b/test/cases/mount/service-nonroot.sh
> @@ -0,0 +1,54 @@
> +#!/usr/bin/env bash
> +# GROUP: mount
> +#
> +# fuservicemount3 installed setuid and run by an unprivileged user mounts only
> +# on a directory that user can write, and never offers fuseblk.
> +
> +_fuse_no_mount_needed=1
> +. "$TEST_LIB/common.sh"
> +
> +_require_linux "fuservicemount3"
> +# Root installs the setuid copy and the socket
> +_require_root
> +_require_fuse_device
> +_require_binary util/fuservicemount3
> +_require_binary test/test_service
> +_require_prog setpriv
> +_require_prog findmnt
> +
> +user=nobody
> +uid=$(id -u "$user") || _notrun "no user $user"
> +gid=$(id -g "$user")
> +run_as=(setpriv --reuid="$uid" --regid="$gid" --clear-groups)
> +
> +helper=$TEST_WORKDIR/fuservicemount3
> +case ",$(findmnt -n -o OPTIONS -T "$TEST_WORKDIR")," in
> +*,nosuid,*) _notrun "$TEST_WORKDIR is on a nosuid mount" ;;
> +esac
> +cp "$FUSE_UTIL_DIR/fuservicemount3" "$helper"
> +_at_exit "rm -f '$helper'"
> +chmod 4755 "$helper"
> +"${run_as[@]}" test -x "$helper" || _notrun "$user cannot reach $helper"
> +
> +. "$TEST_LIB/service.sh"
> +
> +service_setup "test-nonroot-$$"
> +service_helper=("${run_as[@]}" "$helper")
> +root_dir=$TEST_WORKDIR/root-mnt
> +mkdir -m 0755 "$root_dir"
> +
> +chown "$uid" "$TEST_MNT"
> +service_mount "$service_subtype" "$TEST_MNT" mount dir
> +_assert_eq "$(service_result mount)" 0 "mount on a directory $user owns"
> +umount "$TEST_MNT"
> +
> +service_mount "$service_subtype" "$root_dir" mount dir
> +_assert_eq "$(service_result mount)" EPERM \
> +	"mount on a directory owned by root"
> +
> +# allow_other depends on user_allow_other in the system fuse.conf
> +service_mount "$service_subtype" "$TEST_MNT" caps
> +case $(service_result caps) in
> +*" fuseblk=0") ;;
> +*) _fail "caps as $user: $(service_result caps)" ;;
> +esac
> diff --git a/test/cases/mount/service-server-exit.sh b/test/cases/mount/service-server-exit.sh
> new file mode 100755
> index 000000000000..6304f20ff2cb
> --- /dev/null
> +++ b/test/cases/mount/service-server-exit.sh
> @@ -0,0 +1,22 @@
> +#!/usr/bin/env bash
> +# GROUP: mount
> +#
> +# A fuse server that exits without a goodbye makes fuservicemount3 fail, and
> +# leaves nothing mounted.
> +
> +_fuse_no_mount_needed=1
> +. "$TEST_LIB/common.sh"
> +
> +_require_linux "fuservicemount3"
> +_require_root
> +_require_fuse_device
> +_require_binary util/fuservicemount3
> +_require_binary test/test_service
> +
> +. "$TEST_LIB/service.sh"
> +
> +service_setup "test-exit-$$"
> +
> +service_mount "$service_subtype" "$TEST_MNT" exit-early
> +_assert_ne "$service_helper_rc" 0 "fuservicemount3 exit status"
> +_assert_eq "$(mountinfo_field "$TEST_MNT" fstype)" "" "$TEST_MNT mounted"
> diff --git a/test/test_service.c b/test/test_service.c
> index 0d54df3427a9..7bbd14e2650d 100644
> --- a/test/test_service.c
> +++ b/test/test_service.c
> @@ -12,6 +12,10 @@
>   *   test_service open <path>
>   *   test_service open-bdev <path>
>   *   test_service open-after-mount <path>
> + *   test_service mount dir|file
> + *   test_service mount-elsewhere <mountpoint>
> + *   test_service caps
> + *   test_service exit-early
>   */
>  
>  #define FUSE_USE_VERSION FUSE_MAKE_VERSION(3, 19)
> @@ -62,18 +66,25 @@ static int skip_source(void *data, const char *arg, int key,
>  }
>  
>  /*
> - * Mount through the helper so that it has a mount point when the file is
> - * requested.
> + * Mount through the helper. On success *sep is the mounted session, which
> + * keeps /dev/fuse open until it is destroyed.
>   *
> - * @return the mounted session, or NULL on failure
> + * @param fmt         mount point type the helper has to find
> + * @param mountpoint  sent in place of the one on the command line, or NULL
> + * @return 0 when the result was printed, -1 otherwise
>   */
> -static struct fuse_session *session_mounted(struct fuse_service *service,
> -					    const char *argv0)
> +static int mount_printed(struct fuse_service *service, const char *argv0,
> +			 mode_t fmt, const char *mountpoint,
> +			 struct fuse_session **sep)
>  {
>  	struct fuse_args args = FUSE_ARGS_INIT(0, NULL);
>  	struct fuse_cmdline_opts opts = { };
> -	struct fuse_session *se = NULL;
> +	struct fuse_session *se;
>  	bool source_seen = false;
> +	int printed = -1;
> +	int ret;
> +
> +	*sep = NULL;
>  
>  	if (fuse_opt_add_arg(&args, argv0) ||
>  	    fuse_service_append_args(service, &args) ||
> @@ -81,20 +92,30 @@ static struct fuse_session *session_mounted(struct fuse_service *service,
>  	    fuse_service_parse_cmdline_opts(&args, &opts))
>  		goto out;
>  
> +	if (mountpoint) {
> +		free(opts.mountpoint);
> +		opts.mountpoint = strdup(mountpoint);
> +		if (!opts.mountpoint)
> +			goto out;
> +	}
> +
>  	se = fuse_session_new(&args, &test_service_oper,
>  			      sizeof(test_service_oper), NULL);
>  	if (!se)
>  		goto out;
>  
> -	if (fuse_service_session_mount(service, se, S_IFDIR, &opts)) {
> +	ret = fuse_service_session_mount(service, se, fmt, &opts);
> +	if (ret)
>  		fuse_session_destroy(se);
> -		se = NULL;
> -	}
> +	else
> +		*sep = se;
>  
> +	printf("mount result: %s\n", errno_name(-ret));
> +	printed = 0;
>  out:
>  	free(opts.mountpoint);
>  	fuse_opt_free_args(&args);
> -	return se;
> +	return printed;
>  }
>  
>  /* @return 0 when the result was printed, negative errno otherwise */
> @@ -127,22 +148,40 @@ static int request_printed(const struct fuse_service *service,
>  	return 0;
>  }
>  
> +/* @return S_IFDIR or S_IFREG, 0 for an unknown name */
> +static mode_t mount_format(const char *name)
> +{
> +	if (!strcmp(name, "dir"))
> +		return S_IFDIR;
> +	if (!strcmp(name, "file"))
> +		return S_IFREG;
> +	return 0;
> +}
> +
>  int main(int argc, char *argv[])
>  {
>  	struct fuse_service *service = NULL;
>  	struct fuse_session *se = NULL;
> -	bool blockdev = false;
> +	const char *mode;
> +	const char *arg;
>  	int ret = 1;
>  
> -	if (argc != 3) {
> +	if (argc != 2 && argc != 3) {
>  		fprintf(stderr, "usage: %s socket-path <subtype>\n", argv[0]);
>  		fprintf(stderr, "       %s open|open-bdev|open-after-mount <path>\n",
>  			argv[0]);
> +		fprintf(stderr, "       %s mount dir|file\n", argv[0]);
> +		fprintf(stderr, "       %s mount-elsewhere <mountpoint>\n",
> +			argv[0]);
> +		fprintf(stderr, "       %s caps|exit-early\n", argv[0]);
>  		return 1;
>  	}
> +	mode = argv[1];
> +	/* argv[argc] is NULL */
> +	arg = argv[2];
>  
> -	if (!strcmp(argv[1], "socket-path")) {
> -		printf("%s/%s\n", FUSE_SERVICE_SOCKET_DIR, argv[2]);
> +	if (!strcmp(mode, "socket-path") && arg) {
> +		printf("%s/%s\n", FUSE_SERVICE_SOCKET_DIR, arg);
>  		return 0;
>  	}
>  
> @@ -151,19 +190,36 @@ int main(int argc, char *argv[])
>  		return 1;
>  	}
>  
> -	if (!strcmp(argv[1], "open-after-mount")) {
> -		se = session_mounted(service, argv[0]);
> -		if (!se)
> -			goto out;
> -	} else if (!strcmp(argv[1], "open-bdev")) {
> -		blockdev = true;
> -	} else if (strcmp(argv[1], "open")) {
> -		fprintf(stderr, "%s: unknown case %s\n", argv[0], argv[1]);
> -		goto out;
> +	if (!strcmp(mode, "exit-early")) {
> +		/* No goodbye, the helper only sees the connection close */
> +		fuse_service_destroy(&service);
> +		return 0;
>  	}
>  
> -	if (request_printed(service, argv[2], blockdev))
> +	if (!strcmp(mode, "caps")) {
> +		printf("caps result: allow_other=%d fuseblk=%d\n",
> +		       fuse_service_can_allow_other(service),
> +		       fuse_service_can_fuseblk(service));
> +	} else if (!strcmp(mode, "mount") && arg && mount_format(arg)) {
> +		if (mount_printed(service, argv[0], mount_format(arg), NULL,
> +				  &se))
> +			goto out;
> +	} else if (!strcmp(mode, "mount-elsewhere") && arg) {
> +		if (mount_printed(service, argv[0], S_IFDIR, arg, &se))
> +			goto out;
> +	} else if (!strcmp(mode, "open-after-mount") && arg) {
> +		if (mount_printed(service, argv[0], S_IFDIR, NULL, &se) || !se)
> +			goto out;
> +		if (request_printed(service, arg, false))
> +			goto out;
> +	} else if ((!strcmp(mode, "open") || !strcmp(mode, "open-bdev")) &&
> +		   arg) {
> +		if (request_printed(service, arg, !strcmp(mode, "open-bdev")))
> +			goto out;
> +	} else {
> +		fprintf(stderr, "%s: unknown case %s\n", argv[0], mode);
>  		goto out;
> +	}
>  
>  	ret = 0;
>  out:
> 
> -- 
> 2.53.0
> 
> 
> 

  reply	other threads:[~2026-09-29  3:55 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
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 [this message]
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=20260929035516.GI6253@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