From: "Darrick J. Wong" <djwong@kernel.org>
To: bernd@bsbernd.com
Cc: fuse-devel@lists.linux.dev
Subject: Re: [PATCH 02/10] Add tests to verify that mountinfo matches requested options
Date: Fri, 8 May 2026 16:59:55 -0700 [thread overview]
Message-ID: <20260508235955.GH2241589@frogsfrogsfrogs> (raw)
In-Reply-To: <20260508-new-mount-fixes-and-tests-v1-2-c67a0893ddbc@bsbernd.com>
On Fri, May 08, 2026 at 06:39:05PM +0200, Bernd Schubert via B4 Relay wrote:
> From: Bernd Schubert <bernd@bsbernd.com>
>
> This is especially for the new mount API, but does not hurt
> either for the traditional API.
>
> Assisted by Claude Opus 4.7
>
> Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
> ---
> test/meson.build | 3 +-
> test/test_mount_state.py | 116 +++++++++++++++++++++++++++++++++++++++++++++++
> test/util.py | 45 ++++++++++++++++++
> 3 files changed, 163 insertions(+), 1 deletion(-)
>
> diff --git a/test/meson.build b/test/meson.build
> index 87668d61..55018f92 100644
> --- a/test/meson.build
> +++ b/test/meson.build
> @@ -35,7 +35,8 @@ td += executable('test_loop_config', 'test_loop_config.c',
> install: false)
>
> test_scripts = [ 'conftest.py', 'pytest.ini', 'test_examples.py',
> - 'util.py', 'test_ctests.py', 'test_custom_io.py' ]
> + 'util.py', 'test_ctests.py', 'test_custom_io.py',
> + 'test_mount_state.py' ]
> td += custom_target('test_scripts', input: test_scripts,
> output: test_scripts, build_by_default: true,
> command: ['cp', '-fPp',
> diff --git a/test/test_mount_state.py b/test/test_mount_state.py
> new file mode 100644
> index 00000000..eacb7326
> --- /dev/null
> +++ b/test/test_mount_state.py
> @@ -0,0 +1,116 @@
> +#!/usr/bin/env python3
> +'''
> +Tests that observable mount state (as exposed by /proc/self/mountinfo)
> +matches the options requested at mount time.
> +
> +Existing tests check filesystem behavior (read/write/xattr/...) but
> +never inspect the post-mount metadata recorded by the kernel. That
> +metadata is populated differently by the legacy mount(2) path and the
> +new fsopen/fsconfig/fsmount path, so an option dropped on one path can
> +go undetected — the subtype regression in 5e9e16d6 is one example.
> +These tests assert what /proc/self/mountinfo reports for each mount,
> +so a parity bug between the two paths fails loudly.
That looks pretty useful to me! I wouldn't even have caught that
mount(8) turns "ro" into MOUNT_ATTR_RDONLY *and* fsconfig(..., "ro") if
you hadn't known that!
Reviewed-by: "Darrick J. Wong" <djwong@kernel.org>
--D
> +'''
> +
> +if __name__ == '__main__':
> + import pytest
> + import sys
> + sys.exit(pytest.main([__file__] + sys.argv[1:]))
> +
> +import os
> +import subprocess
> +import pytest
> +from contextlib import contextmanager
> +from os.path import join as pjoin
> +from util import (wait_for_mount, umount, cleanup, base_cmdline, basename,
> + fuse_test_marker, parse_mountinfo)
> +
> +pytestmark = fuse_test_marker()
> +
> +
> +@contextmanager
> +def hello_mount(tmpdir, output_checker, name, options=()):
> + mnt_dir = str(tmpdir)
> + cmdline = base_cmdline + [pjoin(basename, 'example', name),
> + '-f', mnt_dir]
> + if name == 'hello_ll':
> + cmdline.append('-s')
> + if options:
> + cmdline += ['-o', ','.join(options)]
> + mp = subprocess.Popen(cmdline, stdout=output_checker.fd,
> + stderr=output_checker.fd)
> + try:
> + wait_for_mount(mp, mnt_dir)
> + yield mnt_dir
> + except:
> + cleanup(mp, mnt_dir)
> + raise
> + else:
> + umount(mp, mnt_dir)
> +
> +
> +@pytest.mark.parametrize('name', ('hello', 'hello_ll'))
> +def test_mountinfo_baseline(tmpdir, output_checker, name):
> + # libfuse's add_default_subtype() (lib/helper.c) defaults the
> + # subtype to basename(argv[0]) when the caller didn't pass
> + # -o fsname=/-o subtype=, so the bare-mount fstype is
> + # 'fuse.<example-name>', not 'fuse'. The override case is what
> + # test_mountinfo_subtype below verifies; here we just assert the
> + # fuse-ness and the standard kernel-side identity options.
> + with hello_mount(tmpdir, output_checker, name) as mnt:
> + info = parse_mountinfo(mnt)
> + assert info is not None, 'mountpoint not found in /proc/self/mountinfo'
> + assert info['fstype'] in ('fuse', 'fuse.' + name), \
> + 'unexpected fstype %r (expected fuse or fuse.%s)' % \
> + (info['fstype'], name)
> + assert any(o.startswith('user_id=') for o in info['super_options'])
> + assert any(o.startswith('group_id=') for o in info['super_options'])
> +
> +
> +@pytest.mark.parametrize('name', ('hello', 'hello_ll'))
> +def test_mountinfo_subtype(tmpdir, output_checker, name):
> + # Regression guard for 5e9e16d6: the new mount API needs an
> + # explicit fsconfig(SET_STRING,"subtype",...). Without it the
> + # kernel records fstype=='fuse' (or whatever basename default
> + # leaks through) instead of the user-requested 'fuse.<subtype>'.
> + # An explicit -o subtype= must override the basename default.
> + with hello_mount(tmpdir, output_checker, name,
> + ('subtype=mysub',)) as mnt:
> + info = parse_mountinfo(mnt)
> + assert info is not None
> + assert info['fstype'] == 'fuse.mysub', \
> + 'explicit subtype not propagated: fstype=%r' % info['fstype']
> +
> +
> +@pytest.mark.parametrize('name', ('hello', 'hello_ll'))
> +def test_mountinfo_fsname(tmpdir, output_checker, name):
> + with hello_mount(tmpdir, output_checker, name,
> + ('fsname=myfsname',)) as mnt:
> + info = parse_mountinfo(mnt)
> + assert info is not None
> + assert info['source'] == 'myfsname', \
> + 'fsname not propagated: source=%r' % info['source']
> +
> +
> +@pytest.mark.parametrize('name', ('hello', 'hello_ll'))
> +def test_mountinfo_subtype_fsname(tmpdir, output_checker, name):
> + with hello_mount(tmpdir, output_checker, name,
> + ('subtype=mysub', 'fsname=myfsname')) as mnt:
> + info = parse_mountinfo(mnt)
> + assert info is not None
> + assert info['fstype'] == 'fuse.mysub'
> + # 'mysub#myfsname' is the ENODEV-fallback form when the kernel
> + # rejects fuse.<subtype>; accept either so the test isn't fragile.
> + assert info['source'] in ('myfsname', 'mysub#myfsname'), \
> + 'unexpected source: %r' % info['source']
> +
> +
> +@pytest.mark.parametrize('name', ('hello', 'hello_ll'))
> +def test_mountinfo_unprivileged_attrs(tmpdir, output_checker, name):
> + if os.getuid() == 0:
> + pytest.skip('only meaningful for unprivileged mounts via fusermount3')
> + with hello_mount(tmpdir, output_checker, name) as mnt:
> + info = parse_mountinfo(mnt)
> + assert info is not None
> + assert 'nosuid' in info['mount_options']
> + assert 'nodev' in info['mount_options']
> diff --git a/test/util.py b/test/util.py
> index 125fd50f..5afaf7d7 100644
> --- a/test/util.py
> +++ b/test/util.py
> @@ -125,6 +125,51 @@ def umount(mount_process, mnt_dir):
> pytest.fail('mount process did not terminate')
>
>
> +def parse_mountinfo(mnt_dir):
> + '''Return the /proc/self/mountinfo entry for *mnt_dir*, or None.
> +
> + Parses the line for the mountpoint and returns a dict with keys:
> + 'mountpoint' - the mount point path (str)
> + 'fstype' - filesystem type as the kernel sees it,
> + e.g. 'fuse' or 'fuse.<subtype>' (str)
> + 'source' - mount source field, e.g. 'hello' or
> + '<subtype>#<fsname>' fallback form (str)
> + 'mount_options' - per-mount options/attrs (set of str), e.g.
> + {'rw','nosuid','nodev','noatime','relatime'}
> + 'super_options' - superblock options from the filesystem (set of
> + str), e.g. {'rw','user_id=1000','group_id=1000',
> + 'default_permissions','allow_other','max_read=...'}
> +
> + These fields are exactly what /proc/self/mountinfo exposes (see
> + `man 5 proc.5_mountinfo`); they capture the post-mount state that
> + differs between the legacy mount(2) path and the new fsopen/fsconfig/
> + fsmount path. Asserting on this dict catches bugs where one path
> + drops a parameter the other passes (e.g. `subtype` showing up in
> + `fstype`, `user=` ending up only in /run/mount/utab, ...).
> + '''
> + target = os.path.realpath(mnt_dir)
> + with open('/proc/self/mountinfo') as fh:
> + for line in fh:
> + parts = line.rstrip('\n').split(' ')
> + try:
> + sep = parts.index('-')
> + except ValueError:
> + continue
> + if len(parts) < sep + 4 or sep < 6:
> + continue
> + mountpoint = parts[4].replace('\\040', ' ')
> + if mountpoint != target:
> + continue
> + return {
> + 'mountpoint': mountpoint,
> + 'fstype': parts[sep + 1],
> + 'source': parts[sep + 2].replace('\\040', ' '),
> + 'mount_options': set(parts[5].split(',')),
> + 'super_options': set(parts[sep + 3].split(',')),
> + }
> + return None
> +
> +
> def safe_sleep(secs):
> '''Like time.sleep(), but sleep for at least *secs*
>
>
> --
> 2.53.0
>
>
next prev parent reply other threads:[~2026-05-08 23:59 UTC|newest]
Thread overview: 25+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-05-08 16:39 [PATCH 00/10] libfuse: new mount API and SYNC_INIT fixes and tests Bernd Schubert via B4 Relay
2026-05-08 16:39 ` [PATCH 01/10] test: register pytest run as a meson test Bernd Schubert via B4 Relay
2026-05-08 23:53 ` Darrick J. Wong
2026-05-08 16:39 ` [PATCH 02/10] Add tests to verify that mountinfo matches requested options Bernd Schubert via B4 Relay
2026-05-08 23:59 ` Darrick J. Wong [this message]
2026-05-08 16:39 ` [PATCH 03/10] test: assert ro/rw, nosuid/suid, nodev/dev round-trip via fsmount Bernd Schubert via B4 Relay
2026-05-09 0:04 ` Darrick J. Wong
2026-05-08 16:39 ` [PATCH 04/10] New mount API: read-only option is for fsmount() and fsconfig() Bernd Schubert via B4 Relay
2026-05-09 0:17 ` Darrick J. Wong
2026-05-08 16:39 ` [PATCH 05/10] libfuse: don't use SYNC_INIT unless asked for Bernd Schubert via B4 Relay
2026-05-08 16:39 ` [PATCH 06/10] example: silence add_languages warning by setting 'native: false' Bernd Schubert via B4 Relay
2026-05-09 0:23 ` Darrick J. Wong
2026-05-08 16:39 ` [PATCH 07/10] mount: clarify kernel_opts vs mnt_opts vs flags in fuse_kern_fsmount Bernd Schubert via B4 Relay
2026-05-09 0:27 ` Darrick J. Wong
2026-05-10 17:21 ` Bernd Schubert
2026-05-08 16:39 ` [PATCH 08/10] fuse_daemonize_early_start: Disallow daemonization when already active Bernd Schubert via B4 Relay
2026-05-09 0:30 ` Darrick J. Wong
2026-05-10 16:53 ` Bernd Schubert
2026-05-10 17:01 ` Bernd Schubert
2026-05-08 16:39 ` [PATCH 09/10] fuse mount: Do not set sync_init when sync_init was not used Bernd Schubert via B4 Relay
2026-05-09 0:35 ` Darrick J. Wong
2026-05-10 17:04 ` Bernd Schubert
2026-05-08 16:39 ` [PATCH 10/10] highlevel: Switch fuse_main_real_versioned() to fuse_daemonize_early() Bernd Schubert via B4 Relay
2026-05-09 0:38 ` Darrick J. Wong
2026-05-10 17:19 ` 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=20260508235955.GH2241589@frogsfrogsfrogs \
--to=djwong@kernel.org \
--cc=bernd@bsbernd.com \
--cc=fuse-devel@lists.linux.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