From: "Darrick J. Wong" <djwong@kernel.org>
To: bernd@bsbernd.com
Cc: fuse-devel@lists.linux.dev, neal@gompa.dev
Subject: Re: [PATCH v2 08/14] test: check which files fuservicemount3 opens for the server
Date: Mon, 28 Sep 2026 20:52:39 -0700 [thread overview]
Message-ID: <20260929035239.GH6253@frogsfrogsfrogs> (raw)
In-Reply-To: <20260928-mount-service-bound-open-v2-8-0f9f501d05ce@bsbernd.com>
On Mon, Sep 28, 2026 at 01:02:10PM +0200, Bernd Schubert via B4 Relay wrote:
> From: Bernd Schubert <bernd@bsbernd.com>
>
> No test covered the OPEN and OPEN_BDEV commands of fuservicemount3.
> test_service is a fuse server that sends one request and prints the
> errno it got back. socket_activate.py starts it the way a systemd
> socket unit does, so the tests do not depend on systemd.
>
> Assisted-by: LLM
> Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
Good basic test of the file opening capabilities :)
Reviewed-by: "Darrick J. Wong" <djwong@kernel.org>
--D
> ---
> .gitignore | 1 +
> test/cases/lib/service.sh | 83 ++++++++++++++++
> test/cases/lib/socket_activate.py | 42 ++++++++
> test/cases/mount/service-open-bound.sh | 61 ++++++++++++
> test/meson.build | 6 ++
> test/test_service.c | 176 +++++++++++++++++++++++++++++++++
> 6 files changed, 369 insertions(+)
>
> diff --git a/.gitignore b/.gitignore
> index 877c2fe2ba87..ebf9b7cdae47 100644
> --- a/.gitignore
> +++ b/.gitignore
> @@ -42,6 +42,7 @@ TAGS
> /test/test_setattr
> /test/test_api_30
> /test/test_fuser_conf
> +/test/test_service
> /build/
> # run-tests.py output, when pointed at the source tree with --run-dir
> /fuse-tests/
> diff --git a/test/cases/lib/service.sh b/test/cases/lib/service.sh
> new file mode 100644
> index 000000000000..d0a0fa0183d0
> --- /dev/null
> +++ b/test/cases/lib/service.sh
> @@ -0,0 +1,83 @@
> +# lib/service.sh - start a fuse service server the way a systemd socket unit
> +# does, for the fuservicemount3 cases.
> +#
> +# Sourced after common.sh. The socket has to be in the build-time socket
> +# directory, so every caller runs as root.
> +
> +# service_setup <subtype>
> +# Set service_sock. At exit remove the socket and any mount on $TEST_MNT.
> +service_setup()
> +{
> + service_subtype=$1
> + service_runs=0
> + # A case may set it, to add arguments to the helper command line
> + service_helper_args=()
> + service_sock=$("$FUSE_TEST_BIN_DIR/test_service" socket-path "$1")
> + # Every service script binds its own socket here; removing the
> + # directory would break another script's bind()
> + mkdir -p "$(dirname "$service_sock")"
> + _at_exit "rm -f '$service_sock'"
> + _at_exit "umount -l '$TEST_MNT' 2>/dev/null"
> +}
> +
> +# service_start <log> <program> [args...]
> +# Listen on service_sock and run <program> for the first connection, with its
> +# output in <log>. Sets service_pid.
> +service_start()
> +{
> + local log=$1; shift
> +
> + rm -f "$service_sock"
> + python3 "$TEST_LIB/socket_activate.py" "$service_sock" "$@" \
> + >"$log" 2>&1 &
> + service_pid=$!
> + _wait_for 10 "grep -q '^listening' '$log'" ||
> + _fail "$service_sock never listened"
> +}
> +
> +# service_wait_exit
> +# Reap the server and set service_rc to its exit status. A helper that never
> +# connected leaves the activator in accept(), which is a failure.
> +service_wait_exit()
> +{
> + _wait_for 10 "! kill -0 $service_pid 2>/dev/null" || {
> + kill "$service_pid" 2>/dev/null || true
> + _fail "server (pid $service_pid) did not exit"
> + }
> + service_rc=0
> + wait "$service_pid" || service_rc=$?
> +}
> +
> +# service_mount <source> <mountpoint> <case> [args...]
> +# Run fuservicemount3 once against test_service <case> [args...] and reap the
> +# server. Sets service_log.
> +service_mount()
> +{
> + local source=$1 mnt=$2; shift 2
> +
> + service_log=$TEST_LOGDIR/fs-$service_runs-$1.out
> + service_runs=$((service_runs + 1))
> + service_start "$service_log" "$FUSE_TEST_BIN_DIR/test_service" "$@"
> +
> + # Its exit status depends on the case; the server's line is the verdict.
> + "$FUSE_UTIL_DIR/fuservicemount3" "$source" "$mnt" \
> + -t "fuse.$service_subtype" "${service_helper_args[@]}" || true
> +
> + service_wait_exit
> +}
> +
> +# service_result <what>
> +# The last server prints one "<what> result: <value>" line, for example
> +# "request result: EPERM". Echo <value>; fail on no such line or several.
> +service_result()
> +{
> + local count
> +
> + count=$(grep -c "^$1 result: " "$service_log") || true
> + if [ "$count" != 1 ]; then
> + cat "$service_log" >&2
> + _fail "$service_log: $count \"$1 result:\" lines, want 1"
> + fi
> + # -n and p: print only the line the substitution matched
> + sed -n "s/^$1 result: //p" "$service_log"
> +}
> diff --git a/test/cases/lib/socket_activate.py b/test/cases/lib/socket_activate.py
> new file mode 100755
> index 000000000000..c33ca3a61ff2
> --- /dev/null
> +++ b/test/cases/lib/socket_activate.py
> @@ -0,0 +1,42 @@
> +#!/usr/bin/env python3
> +"""socket_activate.py <socket-path> <program> [args...]
> +
> +Listen on a SOCK_SEQPACKET socket, accept one connection and exec <program>
> +with it, the way systemd starts a socket unit with Accept=yes: the connection
> +is fd 3, LISTEN_FDS=1 and LISTEN_PID is the pid that runs <program>.
> +
> +Prints "listening" once a connect() can succeed.
> +"""
> +
> +import os
> +import socket
> +import sys
> +
> +SD_LISTEN_FDS_START = 3
> +
> +
> +def main():
> + if len(sys.argv) < 3:
> + sys.exit(__doc__)
> + path = sys.argv[1]
> + program = sys.argv[2:]
> +
> + listener = socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET)
> + listener.bind(path)
> + listener.listen(1)
> + print('listening', flush=True)
> +
> + conn, _ = listener.accept()
> + listener.close()
> +
> + # conn itself is close-on-exec and goes away with the exec
> + os.dup2(conn.fileno(), SD_LISTEN_FDS_START)
> + os.set_inheritable(SD_LISTEN_FDS_START, True)
> + os.environ['LISTEN_FDS'] = '1'
> + # exec keeps the pid
> + os.environ['LISTEN_PID'] = str(os.getpid())
> + os.execv(program[0], program)
> +
> +
> +if __name__ == '__main__':
> + main()
> diff --git a/test/cases/mount/service-open-bound.sh b/test/cases/mount/service-open-bound.sh
> new file mode 100755
> index 000000000000..3ef3690a42e9
> --- /dev/null
> +++ b/test/cases/mount/service-open-bound.sh
> @@ -0,0 +1,61 @@
> +#!/usr/bin/env bash
> +# GROUP: mount
> +#
> +# fuservicemount3 opens a file for the fuse server only if the path is on its
> +# command line.
> +
> +_fuse_no_mount_needed=1
> +. "$TEST_LIB/common.sh"
> +
> +_require_linux "fuservicemount3"
> +_require_root
> +_require_fuse_device
> +_require_binary util/fuservicemount3
> +_require_binary test/test_service
> +
> +. "$TEST_LIB/service.sh"
> +
> +# One socket per run, so a parallel run does not connect to this one
> +subtype=test-open-$$
> +img=$TEST_SRC/img
> +
> +touch "$img"
> +service_setup "$subtype"
> +
> +# service_open_request <case> <path> <expected errno name, or 0>
> +# Mount $img through fuservicemount3 with the server started for <case>.
> +service_open_request()
> +{
> + local case=$1 path=$2 expected=$3
> +
> + service_mount "$img" "$TEST_MNT" "$case" "$path"
> + _assert_eq "$(service_result request)" "$expected" "$case $path"
> +}
> +
> +# $img is on the command line
> +service_open_request open "$img" 0
> +# Root can read /etc/passwd, so EPERM comes from the command line check
> +service_open_request open /etc/passwd EPERM
> +# Passes the command line check, fails the block device check
> +service_open_request open-bdev "$img" ENOTBLK
> +# Not ENOTBLK: OPEN_BDEV gets the command line check first
> +service_open_request open-bdev /etc/passwd EPERM
> +
> +# A path in an option value, as the server's option parser splits it
> +journal=$TEST_SRC/journal,img
> +touch "$journal"
> +service_helper_args=(-o "ro,journal_dev=${journal//,/\\,}")
> +service_open_request open "$journal" 0
> +# The directory of an option value is not a path the user named
> +service_open_request open "$TEST_SRC" EPERM
> +service_helper_args=("-J$journal")
> +service_open_request open "$journal" 0
> +service_helper_args=()
> +
> +# After MNTPT the helper runs inside $TEST_MNT. A relative path must still
> +# resolve in the directory the helper started in.
> +cd "$TEST_SRC"
> +service_mount img "$TEST_MNT" open-after-mount img
> +cd "$OLDPWD"
> +_assert_eq "$(service_result request)" 0 "open-after-mount img"
> +umount "$TEST_MNT"
> diff --git a/test/meson.build b/test/meson.build
> index 68e083f7885c..ec48dab44a61 100644
> --- a/test/meson.build
> +++ b/test/meson.build
> @@ -54,6 +54,12 @@ if build_utils
> c_args: '-DFUSE_CONF="fuse.conf"',
> install: false)
> endif
> +if private_cfg.get('HAVE_SERVICEMOUNT', false)
> + td += executable('test_service', 'test_service.c',
> + include_directories: include_dirs,
> + link_with: [ libfuse ],
> + install: false)
> +endif
>
> if meson.is_subproject()
> # Skipped rather than run: the tests mount filesystems, which is not
> diff --git a/test/test_service.c b/test/test_service.c
> new file mode 100644
> index 000000000000..0d54df3427a9
> --- /dev/null
> +++ b/test/test_service.c
> @@ -0,0 +1,176 @@
> +/*
> + * FUSE: Filesystem in Userspace
> + *
> + * This program can be distributed under the terms of the GNU GPLv2.
> + * See the file GPL2.txt.
> + *
> + * A fuse service server for the service mount tests. Each mode takes one
> + * step against fuservicemount3 and prints the name of the errno that came
> + * back, 0 for success.
> + *
> + * test_service socket-path <subtype>
> + * test_service open <path>
> + * test_service open-bdev <path>
> + * test_service open-after-mount <path>
> + */
> +
> +#define FUSE_USE_VERSION FUSE_MAKE_VERSION(3, 19)
> +
> +/* strerrorname_np() */
> +#ifndef _GNU_SOURCE
> +#define _GNU_SOURCE
> +#endif
> +
> +#include "fuse_config.h"
> +#include <fuse_lowlevel.h>
> +#include <fuse_service.h>
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <string.h>
> +#include <fcntl.h>
> +#include <unistd.h>
> +#include <sys/stat.h>
> +
> +static const struct fuse_lowlevel_ops test_service_oper = { };
> +
> +/* @return "EPERM" and so on, "0" for no error */
> +static const char *errno_name(int error)
> +{
> + const char *name;
> +
> + if (!error)
> + return "0";
> +
> + name = strerrorname_np(error);
> + return name ? name : "unknown errno";
> +}
> +
> +/* The first non-option argument is the mount source, not the mountpoint */
> +static int skip_source(void *data, const char *arg, int key,
> + struct fuse_args *outargs)
> +{
> + bool *source_seen = data;
> +
> + (void)arg;
> + (void)outargs;
> +
> + if (key == FUSE_OPT_KEY_NONOPT && !*source_seen) {
> + *source_seen = true;
> + return 0;
> + }
> + return 1;
> +}
> +
> +/*
> + * Mount through the helper so that it has a mount point when the file is
> + * requested.
> + *
> + * @return the mounted session, or NULL on failure
> + */
> +static struct fuse_session *session_mounted(struct fuse_service *service,
> + const char *argv0)
> +{
> + struct fuse_args args = FUSE_ARGS_INIT(0, NULL);
> + struct fuse_cmdline_opts opts = { };
> + struct fuse_session *se = NULL;
> + bool source_seen = false;
> +
> + if (fuse_opt_add_arg(&args, argv0) ||
> + fuse_service_append_args(service, &args) ||
> + fuse_opt_parse(&args, &source_seen, NULL, skip_source) ||
> + fuse_service_parse_cmdline_opts(&args, &opts))
> + goto out;
> +
> + se = fuse_session_new(&args, &test_service_oper,
> + sizeof(test_service_oper), NULL);
> + if (!se)
> + goto out;
> +
> + if (fuse_service_session_mount(service, se, S_IFDIR, &opts)) {
> + fuse_session_destroy(se);
> + se = NULL;
> + }
> +
> +out:
> + free(opts.mountpoint);
> + fuse_opt_free_args(&args);
> + return se;
> +}
> +
> +/* @return 0 when the result was printed, negative errno otherwise */
> +static int request_printed(const struct fuse_service *service,
> + const char *path, bool blockdev)
> +{
> + int fd;
> + int ret;
> +
> + if (blockdev)
> + ret = fuse_service_request_blockdev(service, path, O_RDONLY,
> + 0, 0, 0);
> + else
> + ret = fuse_service_request_file(service, path, O_RDONLY, 0, 0);
> + if (ret)
> + return ret;
> +
> + /* A refusal by the helper is a success return, with -errno in fd */
> + ret = fuse_service_receive_file(service, path, &fd);
> + if (ret)
> + return ret;
> +
> + if (fd >= 0) {
> + close(fd);
> + printf("request result: 0\n");
> + } else {
> + printf("request result: %s\n", errno_name(-fd));
> + }
> + fflush(stdout);
> + return 0;
> +}
> +
> +int main(int argc, char *argv[])
> +{
> + struct fuse_service *service = NULL;
> + struct fuse_session *se = NULL;
> + bool blockdev = false;
> + int ret = 1;
> +
> + if (argc != 3) {
> + fprintf(stderr, "usage: %s socket-path <subtype>\n", argv[0]);
> + fprintf(stderr, " %s open|open-bdev|open-after-mount <path>\n",
> + argv[0]);
> + return 1;
> + }
> +
> + if (!strcmp(argv[1], "socket-path")) {
> + printf("%s/%s\n", FUSE_SERVICE_SOCKET_DIR, argv[2]);
> + return 0;
> + }
> +
> + if (fuse_service_accept(&service) || !fuse_service_accepted(service)) {
> + fprintf(stderr, "%s: not started as a fuse service\n", argv[0]);
> + return 1;
> + }
> +
> + if (!strcmp(argv[1], "open-after-mount")) {
> + se = session_mounted(service, argv[0]);
> + if (!se)
> + goto out;
> + } else if (!strcmp(argv[1], "open-bdev")) {
> + blockdev = true;
> + } else if (strcmp(argv[1], "open")) {
> + fprintf(stderr, "%s: unknown case %s\n", argv[0], argv[1]);
> + goto out;
> + }
> +
> + if (request_printed(service, argv[2], blockdev))
> + goto out;
> +
> + ret = 0;
> +out:
> + fuse_service_send_goodbye(service, ret);
> + fuse_service_destroy(&service);
> + /* Closes /dev/fuse; the test script unmounts */
> + if (se)
> + fuse_session_destroy(se);
> + return ret;
> +}
>
> --
> 2.53.0
>
>
>
next prev parent reply other threads:[~2026-09-29 3:52 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 [this message]
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=20260929035239.GH6253@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