mirror of
https://github.com/freebsd/freebsd-src.git
synced 2026-10-28 11:25:21 +01:00
mountpoint(1): new utility, implemented as a stat(1) hardlink
Add mountpoint(1), a simple utility to tell whether the file pointed to by the argument is a mount point. It prints whether it is, unless -q is given, and exits 0 if it is, 1 if it is not, and 2 on error. The answer comes from the kernel with a single stat(2): the root vnode of a mounted file system is reported with SFBSD_MNTPOINT in st_bsdflags. Unlike comparing realpath(3) of the argument with that of statfs(2)'s f_mntonname, this works for arbitrarily deep hierarchies, and after chroot(2) or inside a jail. A chroot or jail root is reported as a mount point only if it is one. The argument does not have to be a directory: file systems such as nullfs(5) can be mounted over regular files and sockets, and stat(2) reports those mount points as well. Since all that is needed is one stat(2) call, make mountpoint a hardlink to stat(1), the same way readlink(1) is, and document it in stat.1. Add ATF tests, including chroot(8) and jail(8) roots that are and are not mount points, nullfs roots, file systems mounted inside the root, both from the host and from within the jail, and a nullfs mount of a file over a file. Suggested by: kib Differential revision: https://reviews.freebsd.org/D59906 Sponsored by: Sippy Software, Inc. MFC after: 2 weeks
This commit is contained in:
1 parent
5f275e5478
commit
43b0384bc7
5 files changed
+511
-5
No files matched your search
@@ -2,8 +2,9 @@
|
||||
|
||||
PROG= stat
|
||||
|
||||
LINKS= ${BINDIR}/stat ${BINDIR}/readlink
|
||||
MLINKS= stat.1 readlink.1
|
||||
LINKS= ${BINDIR}/stat ${BINDIR}/readlink \
|
||||
${BINDIR}/stat ${BINDIR}/mountpoint
|
||||
MLINKS= stat.1 readlink.1 stat.1 mountpoint.1
|
||||
|
||||
HAS_TESTS=
|
||||
SUBDIR.${MK_TESTS}+= tests
|
||||
|
||||
+58
-3
@@ -29,12 +29,13 @@
|
||||
.\" ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
.\" POSSIBILITY OF SUCH DAMAGE.
|
||||
.\"
|
||||
.Dd September 9, 2025
|
||||
.Dd October 1, 2026
|
||||
.Dt STAT 1
|
||||
.Os
|
||||
.Sh NAME
|
||||
.Nm stat ,
|
||||
.Nm readlink
|
||||
.Nm readlink ,
|
||||
.Nm mountpoint
|
||||
.Nd display file status
|
||||
.Sh SYNOPSIS
|
||||
.Nm
|
||||
@@ -45,6 +46,9 @@
|
||||
.Nm readlink
|
||||
.Op Fl fn
|
||||
.Op Ar
|
||||
.Nm mountpoint
|
||||
.Op Fl q
|
||||
.Ar file
|
||||
.Sh DESCRIPTION
|
||||
The
|
||||
.Nm
|
||||
@@ -75,9 +79,34 @@ corresponding to
|
||||
.Ar file .
|
||||
In this case, the argument does not need to be a symbolic link.
|
||||
.Pp
|
||||
When invoked as
|
||||
.Nm mountpoint ,
|
||||
the utility checks whether
|
||||
.Ar file
|
||||
is the root of a mounted file system.
|
||||
This is usually a directory, but file systems such as
|
||||
.Xr nullfs 5
|
||||
can also be mounted over regular files and sockets.
|
||||
Symbolic links are followed.
|
||||
Unless the
|
||||
.Fl q
|
||||
option is given, a message stating whether
|
||||
.Ar file
|
||||
is a mount point is written to the standard output.
|
||||
Error messages are written to the standard error even with
|
||||
.Fl q .
|
||||
.Pp
|
||||
The information displayed is obtained by calling
|
||||
.Xr lstat 2
|
||||
with the given argument and evaluating the returned structure.
|
||||
When invoked as
|
||||
.Nm mountpoint ,
|
||||
.Xr stat 2
|
||||
is called instead and the
|
||||
.Dv SFBSD_MNTPOINT
|
||||
flag in the
|
||||
.Fa st_bsdflags
|
||||
field is checked.
|
||||
The default format displays the
|
||||
.Fa st_dev ,
|
||||
.Fa st_ino ,
|
||||
@@ -197,6 +226,11 @@ fail.
|
||||
When run as
|
||||
.Nm readlink ,
|
||||
error messages are automatically suppressed.
|
||||
When run as
|
||||
.Nm mountpoint ,
|
||||
.Fl q
|
||||
suppresses the normal output instead;
|
||||
error messages are still written to the standard error.
|
||||
.It Fl f Ar format
|
||||
Display information using the specified format.
|
||||
See the
|
||||
@@ -513,6 +547,12 @@ which default to
|
||||
.Cm S .
|
||||
.Sh EXIT STATUS
|
||||
.Ex -std stat readlink
|
||||
.Pp
|
||||
The
|
||||
.Nm mountpoint
|
||||
utility exits 0 if
|
||||
.Ar file
|
||||
is a mount point, 1 if it is not, and 2 if an error occurred.
|
||||
.Sh EXAMPLES
|
||||
If no options are specified, the default format is
|
||||
"%d %i %Sp %l %Su %Sg %r %z \e"%Sa\e" \e"%Sm\e" \e"%Sc\e" \e"%SB\e" %k %b %#Xf %N".
|
||||
@@ -620,6 +660,19 @@ To display the same in UTC:
|
||||
$ TZ= stat -f %Sm -t %Y%m%d%H%M%S /tmp/foo
|
||||
20070427181533
|
||||
.Ed
|
||||
.Pp
|
||||
To check whether a directory is a mount point:
|
||||
.Bd -literal -offset indent
|
||||
\*[Gt] mountpoint /dev
|
||||
/dev is a mount point
|
||||
\*[Gt] mountpoint /usr/bin
|
||||
/usr/bin is not a mount point
|
||||
.Ed
|
||||
.Pp
|
||||
To mount a file system only if nothing is mounted there yet:
|
||||
.Bd -literal -offset indent
|
||||
$ mountpoint -q /mnt || mount /dev/md0 /mnt
|
||||
.Ed
|
||||
.Sh SEE ALSO
|
||||
.Xr file 1 ,
|
||||
.Xr ls 1 ,
|
||||
@@ -627,7 +680,9 @@ $ TZ= stat -f %Sm -t %Y%m%d%H%M%S /tmp/foo
|
||||
.Xr readlink 2 ,
|
||||
.Xr stat 2 ,
|
||||
.Xr printf 3 ,
|
||||
.Xr strftime 3
|
||||
.Xr strftime 3 ,
|
||||
.Xr nullfs 5 ,
|
||||
.Xr mount 8
|
||||
.Sh HISTORY
|
||||
The
|
||||
.Nm
|
||||
|
||||
@@ -139,6 +139,8 @@
|
||||
#define SHOW_sizerdev 'Z'
|
||||
|
||||
static void usage(const char *);
|
||||
static int mountpoint_main(int, char *[]);
|
||||
static void mountpoint_usage(void) __dead2;
|
||||
static void output(const struct stat *, const char *, const char *, int);
|
||||
static int format1(const struct stat *, /* stat info */
|
||||
const char *, /* the file name */
|
||||
@@ -185,6 +187,9 @@ main(int argc, char *argv[])
|
||||
statfmt = NULL;
|
||||
timefmt = NULL;
|
||||
|
||||
if (strcmp(getprogname(), "mountpoint") == 0)
|
||||
return (mountpoint_main(argc, argv));
|
||||
|
||||
if (strcmp(getprogname(), "readlink") == 0) {
|
||||
am_readlink = 1;
|
||||
options = "fn";
|
||||
@@ -392,6 +397,49 @@ usage(const char *synopsis)
|
||||
exit(1);
|
||||
}
|
||||
|
||||
/*
|
||||
* When invoked as mountpoint, report whether the file is the root of a
|
||||
* mounted file system. Exit with 0 if it is, 1 if it is not and
|
||||
* 2 on error.
|
||||
*/
|
||||
static int
|
||||
mountpoint_main(int argc, char *argv[])
|
||||
{
|
||||
struct stat st;
|
||||
int ch, mounted, quiet;
|
||||
|
||||
quiet = 0;
|
||||
while ((ch = getopt(argc, argv, "q")) != -1)
|
||||
switch (ch) {
|
||||
case 'q':
|
||||
quiet = 1;
|
||||
break;
|
||||
default:
|
||||
mountpoint_usage();
|
||||
}
|
||||
argc -= optind;
|
||||
argv += optind;
|
||||
if (argc != 1)
|
||||
mountpoint_usage();
|
||||
|
||||
if (stat(argv[0], &st) == -1)
|
||||
err(2, "%s", argv[0]);
|
||||
|
||||
mounted = (st.st_bsdflags & SFBSD_MNTPOINT) != 0;
|
||||
if (!quiet)
|
||||
printf("%s is %sa mount point\n", argv[0],
|
||||
mounted ? "" : "not ");
|
||||
return (mounted ? 0 : 1);
|
||||
}
|
||||
|
||||
static void
|
||||
mountpoint_usage(void)
|
||||
{
|
||||
|
||||
(void)fprintf(stderr, "usage: %s [-q] file\n", getprogname());
|
||||
exit(2);
|
||||
}
|
||||
|
||||
/*
|
||||
* Parses a format string.
|
||||
*/
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
ATF_TESTS_SH+= mountpoint_test
|
||||
ATF_TESTS_SH+= readlink_test
|
||||
ATF_TESTS_SH+= stat_test
|
||||
|
||||
|
||||
@@ -0,0 +1,401 @@
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-2-Clause
|
||||
#
|
||||
# Copyright (c) 2026 Maksym Sobolev <[email protected]>
|
||||
#
|
||||
|
||||
# Mount a file system and record its mount point for mp_cleanup.
|
||||
mp_mount()
|
||||
{
|
||||
local mntpt
|
||||
|
||||
for mntpt; do :; done
|
||||
[ -e "$mntpt" ] || atf_check mkdir -p "$mntpt"
|
||||
echo "$PWD/$mntpt" >> mounts
|
||||
atf_check mount "$@"
|
||||
}
|
||||
|
||||
mp_cleanup()
|
||||
{
|
||||
[ -f mounts ] || return 0
|
||||
tail -r mounts | while read mntpt; do
|
||||
umount -f "$mntpt" 2>/dev/null
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
# Populate a minimal root with sh, mountpoint and the given programs,
|
||||
# together with the shared libraries they need.
|
||||
mp_mkroot()
|
||||
{
|
||||
local root lib prog
|
||||
|
||||
root=$1
|
||||
shift
|
||||
atf_check mkdir -p "$root/bin" "$root/libexec" "$root/mnt"
|
||||
atf_check cp /libexec/ld-elf.so.1 "$root/libexec/"
|
||||
for prog in sh mountpoint "$@"; do
|
||||
prog=$(command -v "$prog") || atf_fail "$prog not found"
|
||||
atf_check cp "$prog" "$root/bin/"
|
||||
for lib in $(ldd -f '%p\n' "$prog" | grep '^/'); do
|
||||
atf_check mkdir -p "$root${lib%/*}"
|
||||
atf_check cp "$lib" "$root$lib"
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
mp_require_jail()
|
||||
{
|
||||
if [ "$(sysctl -n security.jail.jailed)" != 0 ]; then
|
||||
atf_skip "Cannot create jails from within a jail"
|
||||
fi
|
||||
}
|
||||
|
||||
# Run "mountpoint path" inside root, entered via chroot(8) or jail(8)
|
||||
# as selected by mode, and check that it reports rc. jail(8) does not
|
||||
# pass the exit status of the command through, so it is echoed back.
|
||||
mp_check()
|
||||
{
|
||||
local mode root path rc not
|
||||
|
||||
mode=$1
|
||||
root=$2
|
||||
path=$3
|
||||
rc=$4
|
||||
not=
|
||||
[ "$rc" -eq 0 ] || not="not "
|
||||
case "$mode" in
|
||||
chroot)
|
||||
set -- chroot "$root" /bin/sh
|
||||
;;
|
||||
jail)
|
||||
set -- jail -c path="$PWD/$root" command=/bin/sh
|
||||
;;
|
||||
esac
|
||||
atf_check -o inline:"$path is ${not}a mount point\nrc=$rc\n" \
|
||||
"$@" -c "/bin/mountpoint $path; echo rc=\$?"
|
||||
}
|
||||
|
||||
atf_test_case not_mountpoint
|
||||
not_mountpoint_head()
|
||||
{
|
||||
atf_set "descr" "Verify that a plain directory or file is not " \
|
||||
"reported as a mount point"
|
||||
}
|
||||
|
||||
not_mountpoint_body()
|
||||
{
|
||||
atf_check mkdir dir
|
||||
atf_check -s exit:1 -o inline:"dir is not a mount point\n" \
|
||||
mountpoint dir
|
||||
atf_check -s exit:1 mountpoint -q dir
|
||||
atf_check touch file
|
||||
atf_check -s exit:1 -o inline:"file is not a mount point\n" \
|
||||
mountpoint file
|
||||
}
|
||||
|
||||
atf_test_case errors
|
||||
errors_head()
|
||||
{
|
||||
atf_set "descr" "Verify that errors are reported with exit status 2"
|
||||
}
|
||||
|
||||
errors_body()
|
||||
{
|
||||
atf_check -s exit:2 -e match:"No such file" mountpoint nonexistent
|
||||
atf_check -s exit:2 -e match:"usage" mountpoint
|
||||
atf_check -s exit:2 -e match:"usage" mountpoint a b
|
||||
atf_check -s exit:2 -e match:"usage" mountpoint -x .
|
||||
}
|
||||
|
||||
atf_test_case mountpoint cleanup
|
||||
mountpoint_head()
|
||||
{
|
||||
atf_set "descr" "Verify that a mounted file system root is " \
|
||||
"reported as a mount point, also through a symlink"
|
||||
atf_set "require.user" "root"
|
||||
}
|
||||
|
||||
mountpoint_body()
|
||||
{
|
||||
mp_mount -t tmpfs tmpfs mnt
|
||||
atf_check -o inline:"mnt is a mount point\n" mountpoint mnt
|
||||
atf_check -o empty mountpoint -q mnt
|
||||
atf_check ln -s mnt link
|
||||
atf_check -o inline:"link is a mount point\n" mountpoint link
|
||||
atf_check mkdir mnt/sub
|
||||
atf_check -s exit:1 -o inline:"mnt/sub is not a mount point\n" \
|
||||
mountpoint mnt/sub
|
||||
}
|
||||
|
||||
mountpoint_cleanup()
|
||||
{
|
||||
mp_cleanup
|
||||
}
|
||||
|
||||
atf_test_case file_mountpoint cleanup
|
||||
file_mountpoint_head()
|
||||
{
|
||||
atf_set "descr" "Verify that a regular file with a file system " \
|
||||
"mounted over it is reported as a mount point"
|
||||
atf_set "require.user" "root"
|
||||
atf_set "require.kmods" "nullfs"
|
||||
}
|
||||
|
||||
file_mountpoint_body()
|
||||
{
|
||||
atf_check touch src dst
|
||||
mp_mount -t nullfs "$PWD/src" dst
|
||||
atf_check -o inline:"dst is a mount point\n" mountpoint dst
|
||||
atf_check ln -s dst link
|
||||
atf_check -o inline:"link is a mount point\n" mountpoint link
|
||||
atf_check -s exit:1 -o inline:"src is not a mount point\n" \
|
||||
mountpoint src
|
||||
}
|
||||
|
||||
file_mountpoint_cleanup()
|
||||
{
|
||||
mp_cleanup
|
||||
}
|
||||
|
||||
# The root is a plain directory on top of a mount point. Neither the
|
||||
# root nor its ".." may be reported as a mount point, so the mount point
|
||||
# above the root does not leak in.
|
||||
root_not_mountpoint_body()
|
||||
{
|
||||
mp_mount -t tmpfs tmpfs outer
|
||||
mp_mkroot outer/root
|
||||
mp_check $1 outer/root / 1
|
||||
mp_check $1 outer/root /.. 1
|
||||
mp_check $1 outer/root /bin 1
|
||||
}
|
||||
|
||||
# The root is itself a mount point.
|
||||
root_mountpoint_body()
|
||||
{
|
||||
mp_mount -t tmpfs tmpfs root
|
||||
mp_mkroot root
|
||||
mp_check $1 root / 0
|
||||
mp_check $1 root /.. 0
|
||||
mp_check $1 root /bin 1
|
||||
}
|
||||
|
||||
# The root is a nullfs mount, as is common for jails.
|
||||
root_nullfs_body()
|
||||
{
|
||||
mp_mkroot src
|
||||
mp_mount -t nullfs "$PWD/src" root
|
||||
mp_check $1 root / 0
|
||||
mp_check $1 root /bin 1
|
||||
}
|
||||
|
||||
# The root is a plain directory with a file system mounted inside it.
|
||||
submount_body()
|
||||
{
|
||||
mp_mkroot root
|
||||
mp_mount -t tmpfs tmpfs root/mnt
|
||||
mp_check $1 root / 1
|
||||
mp_check $1 root /mnt 0
|
||||
mp_check $1 root /mnt/.. 1
|
||||
}
|
||||
|
||||
atf_test_case chroot_root_not_mountpoint cleanup
|
||||
chroot_root_not_mountpoint_head()
|
||||
{
|
||||
atf_set "descr" "Verify that a chroot root which is not a mount " \
|
||||
"point is not reported as one"
|
||||
atf_set "require.user" "root"
|
||||
}
|
||||
|
||||
chroot_root_not_mountpoint_body()
|
||||
{
|
||||
root_not_mountpoint_body chroot
|
||||
}
|
||||
|
||||
chroot_root_not_mountpoint_cleanup()
|
||||
{
|
||||
mp_cleanup
|
||||
}
|
||||
|
||||
atf_test_case chroot_root_mountpoint cleanup
|
||||
chroot_root_mountpoint_head()
|
||||
{
|
||||
atf_set "descr" "Verify that a chroot root which is a mount point " \
|
||||
"is reported as one"
|
||||
atf_set "require.user" "root"
|
||||
}
|
||||
|
||||
chroot_root_mountpoint_body()
|
||||
{
|
||||
root_mountpoint_body chroot
|
||||
}
|
||||
|
||||
chroot_root_mountpoint_cleanup()
|
||||
{
|
||||
mp_cleanup
|
||||
}
|
||||
|
||||
atf_test_case chroot_root_nullfs cleanup
|
||||
chroot_root_nullfs_head()
|
||||
{
|
||||
atf_set "descr" "Verify that a chroot root which is a nullfs " \
|
||||
"mount is reported as a mount point"
|
||||
atf_set "require.user" "root"
|
||||
atf_set "require.kmods" "nullfs"
|
||||
}
|
||||
|
||||
chroot_root_nullfs_body()
|
||||
{
|
||||
root_nullfs_body chroot
|
||||
}
|
||||
|
||||
chroot_root_nullfs_cleanup()
|
||||
{
|
||||
mp_cleanup
|
||||
}
|
||||
|
||||
atf_test_case chroot_submount cleanup
|
||||
chroot_submount_head()
|
||||
{
|
||||
atf_set "descr" "Verify that a file system mounted inside a chroot " \
|
||||
"is reported as a mount point"
|
||||
atf_set "require.user" "root"
|
||||
}
|
||||
|
||||
chroot_submount_body()
|
||||
{
|
||||
submount_body chroot
|
||||
}
|
||||
|
||||
chroot_submount_cleanup()
|
||||
{
|
||||
mp_cleanup
|
||||
}
|
||||
|
||||
atf_test_case jail_root_not_mountpoint cleanup
|
||||
jail_root_not_mountpoint_head()
|
||||
{
|
||||
atf_set "descr" "Verify that a jail root which is not a mount " \
|
||||
"point is not reported as one"
|
||||
atf_set "require.user" "root"
|
||||
atf_set "require.progs" "jail"
|
||||
}
|
||||
|
||||
jail_root_not_mountpoint_body()
|
||||
{
|
||||
mp_require_jail
|
||||
root_not_mountpoint_body jail
|
||||
}
|
||||
|
||||
jail_root_not_mountpoint_cleanup()
|
||||
{
|
||||
mp_cleanup
|
||||
}
|
||||
|
||||
atf_test_case jail_root_mountpoint cleanup
|
||||
jail_root_mountpoint_head()
|
||||
{
|
||||
atf_set "descr" "Verify that a jail root which is a mount point " \
|
||||
"is reported as one"
|
||||
atf_set "require.user" "root"
|
||||
atf_set "require.progs" "jail"
|
||||
}
|
||||
|
||||
jail_root_mountpoint_body()
|
||||
{
|
||||
mp_require_jail
|
||||
root_mountpoint_body jail
|
||||
}
|
||||
|
||||
jail_root_mountpoint_cleanup()
|
||||
{
|
||||
mp_cleanup
|
||||
}
|
||||
|
||||
atf_test_case jail_root_nullfs cleanup
|
||||
jail_root_nullfs_head()
|
||||
{
|
||||
atf_set "descr" "Verify that a jail root which is a nullfs mount " \
|
||||
"is reported as a mount point"
|
||||
atf_set "require.user" "root"
|
||||
atf_set "require.progs" "jail"
|
||||
atf_set "require.kmods" "nullfs"
|
||||
}
|
||||
|
||||
jail_root_nullfs_body()
|
||||
{
|
||||
mp_require_jail
|
||||
root_nullfs_body jail
|
||||
}
|
||||
|
||||
jail_root_nullfs_cleanup()
|
||||
{
|
||||
mp_cleanup
|
||||
}
|
||||
|
||||
atf_test_case jail_submount cleanup
|
||||
jail_submount_head()
|
||||
{
|
||||
atf_set "descr" "Verify that a file system mounted inside a jail " \
|
||||
"from the host is reported as a mount point"
|
||||
atf_set "require.user" "root"
|
||||
atf_set "require.progs" "jail"
|
||||
}
|
||||
|
||||
jail_submount_body()
|
||||
{
|
||||
mp_require_jail
|
||||
submount_body jail
|
||||
}
|
||||
|
||||
jail_submount_cleanup()
|
||||
{
|
||||
mp_cleanup
|
||||
}
|
||||
|
||||
atf_test_case jail_mount_inside cleanup
|
||||
jail_mount_inside_head()
|
||||
{
|
||||
atf_set "descr" "Verify that a file system mounted from within " \
|
||||
"a jail is reported as a mount point there"
|
||||
atf_set "require.user" "root"
|
||||
atf_set "require.progs" "jail"
|
||||
}
|
||||
|
||||
jail_mount_inside_body()
|
||||
{
|
||||
local out
|
||||
|
||||
mp_require_jail
|
||||
mp_mkroot root mount umount
|
||||
echo "$PWD/root/mnt" >> mounts
|
||||
out="/mnt is a mount point\nrc=0\n/ is not a mount point\nrc=1\n"
|
||||
atf_check -o inline:"$out" jail -c path="$PWD/root" \
|
||||
allow.mount allow.mount.tmpfs enforce_statfs=1 \
|
||||
command=/bin/sh -c "/bin/mount -t tmpfs tmpfs /mnt &&
|
||||
/bin/mountpoint /mnt; echo rc=\$?;
|
||||
/bin/mountpoint /; echo rc=\$?;
|
||||
/bin/umount /mnt"
|
||||
}
|
||||
|
||||
jail_mount_inside_cleanup()
|
||||
{
|
||||
mp_cleanup
|
||||
}
|
||||
|
||||
atf_init_test_cases()
|
||||
{
|
||||
atf_add_test_case not_mountpoint
|
||||
atf_add_test_case errors
|
||||
atf_add_test_case mountpoint
|
||||
atf_add_test_case file_mountpoint
|
||||
atf_add_test_case chroot_root_not_mountpoint
|
||||
atf_add_test_case chroot_root_mountpoint
|
||||
atf_add_test_case chroot_root_nullfs
|
||||
atf_add_test_case chroot_submount
|
||||
atf_add_test_case jail_root_not_mountpoint
|
||||
atf_add_test_case jail_root_mountpoint
|
||||
atf_add_test_case jail_root_nullfs
|
||||
atf_add_test_case jail_submount
|
||||
atf_add_test_case jail_mount_inside
|
||||
}
|
||||
Reference in new issue
Block a user