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: 41+ 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 ` Bernd Schubert
2026-09-28 11:02 ` [PATCH v2 01/14] mount_service: move the command line check into arg_in_cmdline() Bernd Schubert via B4 Relay
2026-09-28 11:02 ` Bernd Schubert
2026-09-28 11:02 ` [PATCH v2 02/14] mount_service: warn about paths not named on the command line Bernd Schubert via B4 Relay
2026-09-28 11:02 ` Bernd Schubert
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-28 11:02 ` Bernd Schubert
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-28 11:02 ` Bernd Schubert
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 ` Bernd Schubert
2026-09-28 11:02 ` [PATCH v2 06/14] mount.fuse: free the options on the service mount return path Bernd Schubert via B4 Relay
2026-09-28 11:02 ` Bernd Schubert
2026-09-28 11:02 ` [PATCH v2 07/14] example/single_file: take no sector size from a regular backing file Bernd Schubert via B4 Relay
2026-09-28 11:02 ` Bernd Schubert
2026-09-28 11:02 ` [PATCH v2 08/14] test: check which files fuservicemount3 opens for the server Bernd Schubert via B4 Relay
2026-09-28 11:02 ` Bernd Schubert
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-28 11:02 ` Bernd Schubert
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 ` Bernd Schubert
2026-09-28 11:02 ` [PATCH v2 11/14] test: run mkfs.ext4 through the service examples Bernd Schubert via B4 Relay
2026-09-28 11:02 ` Bernd Schubert
2026-09-28 11:02 ` [PATCH v2 12/14] fuse_service: bound argc and arg len read from the args memfd Bernd Schubert via B4 Relay
2026-09-28 11:02 ` Bernd Schubert
2026-09-29 2:33 ` Darrick J. Wong
2026-09-28 11:02 ` [PATCH v2 13/14] build: move the default service socket directory to /run/fuse Bernd Schubert via B4 Relay
2026-09-28 11:02 ` Bernd Schubert
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-28 11:02 ` Bernd Schubert
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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.