git: c3ac06161dd5 - main - nmount: Introduce the "check_fsid" option

From: Gleb Popov <arrowd_at_FreeBSD.org>
Date: Sat, 10 Oct 2026 14:42:22 UTC
The branch main has been updated by arrowd:

URL: https://cgit.FreeBSD.org/src/commit/?id=c3ac06161dd5b1ac61bcf097060d1e7ae5f0cd57

commit c3ac06161dd5b1ac61bcf097060d1e7ae5f0cd57
Author:     Gleb Popov <arrowd@FreeBSD.org>
AuthorDate: 2026-09-23 15:35:42 +0000
Commit:     Gleb Popov <arrowd@FreeBSD.org>
CommitDate: 2026-10-10 14:42:08 +0000

    nmount: Introduce the "check_fsid" option
    
    See the manpage addition for rationale
    
    Reviewed by:    ziaee, kib
    Approved by:    ziaee, kib
    Differential Revision:  https://reviews.freebsd.org/D59930
---
 lib/libsys/mount.2                |  34 +++++-
 sys/kern/vfs_mount.c              |  53 +++++++--
 tests/sys/vfs/Makefile            |   5 +
 tests/sys/vfs/nmount_check_fsid.c | 238 ++++++++++++++++++++++++++++++++++++++
 4 files changed, 316 insertions(+), 14 deletions(-)

diff --git a/lib/libsys/mount.2 b/lib/libsys/mount.2
index 62e82c0b8184..e9d420fac201 100644
--- a/lib/libsys/mount.2
+++ b/lib/libsys/mount.2
@@ -83,13 +83,33 @@ containing
 .Fa niov
 elements.
 The following options are required by all file systems:
-.Bl -column fstype -offset indent
-.It
-.Li fstype Ta file system type name (e.g., Dq Li procfs )
-.It
-.Li fspath Ta mount point pathname (e.g., Dq Li /proc )
+.Bl -tag -width "check_fsid" -offset indent
+.It Li fstype
+file system type name
+.Pq e.g., Dq Li procfs
+.It Li fspath
+mount point pathname
+.Pq e.g., Dq Li /proc
 .El
 .Pp
+The following options may be specified when mounting any file system:
+.Bl -tag -width "check_fsid" -offset indent
+.It Li check_fsid
+FSID of a mount point being covered by the mount in the
+.Dq Li FSID:%d:%d
+format
+.El
+.Pp
+If the
+.Dq Li check_fsid
+option is specified, and the FSID of the mount point the covered vnode belongs
+to does not match the passed value, the
+.Fn nmount
+call will fail.
+.Dq Li check_fsid
+may also be passed when doing a mount update, in which case the caller should
+pass a FSID of the FS being updated, not the one covered.
+.Pp
 Depending on the file system type, other options may be
 recognized or required;
 for example, most disk-based file systems require a
@@ -278,6 +298,10 @@ An I/O error occurred while reading data from
 The backing store for
 .Fa special
 detected corrupted data while reading.
+.It Bq Er ENOENT
+The file system ID specified using
+.Fa check_fsid
+did not match the target path.
 .El
 .Pp
 The following errors can occur for a
diff --git a/sys/kern/vfs_mount.c b/sys/kern/vfs_mount.c
index b784c148ff1d..69b448f8fe39 100644
--- a/sys/kern/vfs_mount.c
+++ b/sys/kern/vfs_mount.c
@@ -79,6 +79,7 @@
 
 static int	vfs_domount(struct thread *td, const char *fstype, char *fspath,
 		    uint64_t fsflags, bool only_export, bool jail_export,
+		    char *check_fsid_str, char **bailmsg_p,
 		    struct vfsoptlist **optlist);
 static void	free_mntarg(struct mntarg *ma);
 static void	pnfsd_waitreplenish(struct mount *mp);
@@ -809,11 +810,11 @@ vfs_donmount(struct thread *td, uint64_t fsflags, struct uio *fsoptions)
 {
 	struct vfsoptlist *optlist;
 	struct vfsopt *opt, *tmp_opt;
-	char *fstype, *fspath, *errmsg;
+	char *fstype, *fspath, *errmsg, *bailmsg, *check_fsid_str;
 	int error, fstypelen, fspathlen, errmsg_len, errmsg_pos;
 	bool autoro, has_nonexport, only_export, jail_export;
 
-	errmsg = fspath = NULL;
+	errmsg = fspath = check_fsid_str = bailmsg = NULL;
 	errmsg_len = fspathlen = 0;
 	errmsg_pos = -1;
 	autoro = default_autoro;
@@ -977,6 +978,9 @@ vfs_donmount(struct thread *td, uint64_t fsflags, struct uio *fsoptions)
 		} else if (strcmp(opt->name, "noemptydir") == 0) {
 			fsflags &= ~MNT_EMPTYDIR;
 			do_freeopt = 1;
+		} else if (strcmp(opt->name, "check_fsid") == 0) {
+			check_fsid_str = strndup(opt->value, opt->len, M_MOUNT);
+			do_freeopt = 1;
 		}
 		if (do_freeopt)
 			vfs_freeopt(optlist, opt);
@@ -1008,11 +1012,10 @@ vfs_donmount(struct thread *td, uint64_t fsflags, struct uio *fsoptions)
 		jail_export = true;
 
 	error = vfs_domount(td, fstype, fspath, fsflags, only_export,
-	    jail_export, &optlist);
-	if (error == ENODEV) {
-		error = EINVAL;
+	    jail_export, check_fsid_str, &bailmsg, &optlist);
+	if (bailmsg != NULL) {
 		if (errmsg != NULL)
-			strncpy(errmsg, "Invalid fstype", errmsg_len);
+			strncpy(errmsg, bailmsg, errmsg_len);
 		goto bail;
 	}
 
@@ -1027,7 +1030,7 @@ vfs_donmount(struct thread *td, uint64_t fsflags, struct uio *fsoptions)
 		    " trying R/O mount\n", __func__);
 		fsflags |= MNT_RDONLY;
 		error = vfs_domount(td, fstype, fspath, fsflags, only_export,
-		    jail_export, &optlist);
+		    jail_export, check_fsid_str, &bailmsg, &optlist);
 	}
 bail:
 	/* copyout the errmsg */
@@ -1629,6 +1632,9 @@ vfs_domount(
 	uint64_t fsflags,		/* Flags common to all filesystems. */
 	bool only_export,		/* Got export option. */
 	bool jail_export,		/* Got export option in vnet prison. */
+	char *check_fsid_str,		/* fsid of an inode we're expecting
+					   to mount over. */
+	char **bailmsg_p,		/* textual error returned to caller */
 	struct vfsoptlist **optlist	/* Options local to the filesystem. */
 	)
 {
@@ -1636,6 +1642,7 @@ vfs_domount(
 	struct nameidata nd;
 	struct vnode *vp;
 	char *pathbuf;
+	fsid_t check_fsid;
 	int error;
 
 	/*
@@ -1646,6 +1653,18 @@ vfs_domount(
 	if (strlen(fstype) >= MFSNAMELEN || strlen(fspath) >= MNAMELEN)
 		return (ENAMETOOLONG);
 
+	if (check_fsid_str != NULL) {
+		bool parsed;
+
+		parsed = sscanf(check_fsid_str, "FSID:%d:%d",
+		    &check_fsid.val[0], &check_fsid.val[1]) == 2;
+		free(check_fsid_str, M_MOUNT);
+		if (!parsed) {
+			*bailmsg_p = "Invalid check_fsid value";
+			return (ENOENT);
+		}
+	}
+
 	if (jail_export) {
 		error = priv_check(td, PRIV_NFS_DAEMON);
 		if (error)
@@ -1681,8 +1700,10 @@ vfs_domount(
 	if ((fsflags & MNT_UPDATE) == 0) {
 		/* Don't try to load KLDs if we're mounting the root. */
 		if (fsflags & MNT_ROOTFS) {
-			if ((vfsp = vfs_byname(fstype)) == NULL)
-				return (ENODEV);
+			if ((vfsp = vfs_byname(fstype)) == NULL) {
+				*bailmsg_p = "Invalid fstype";
+				return (EINVAL);
+			}
 		} else {
 			if ((vfsp = vfs_byname_kld(fstype, td, &error)) == NULL)
 				return (error);
@@ -1698,6 +1719,20 @@ vfs_domount(
 	if (error != 0)
 		return (error);
 	vp = nd.ni_vp;
+	if (check_fsid_str != NULL) {
+		struct mount *mp;
+
+		mp = vfs_getvfs(&check_fsid);
+		if (mp != vp->v_mount) {
+			vput(vp);
+			error = ENOENT;
+			*bailmsg_p = "vnode's FSID does not match check_fsid";
+		}
+		if (mp != NULL)
+			vfs_rel(mp);
+		if (error != 0)
+			goto out;
+	}
 	/*
 	 * Don't allow stacking file mounts to work around problems with the way
 	 * that namei sets nd.ni_dvp to vp_crossmp for these.
diff --git a/tests/sys/vfs/Makefile b/tests/sys/vfs/Makefile
index f34616e22ebf..5a21fc20cca1 100644
--- a/tests/sys/vfs/Makefile
+++ b/tests/sys/vfs/Makefile
@@ -5,8 +5,13 @@ TESTSDIR=		${TESTSBASE}/sys/vfs
 ATF_TESTS_C+=	lookup_cap_dotdot
 CFLAGS.lookup_cap_dotdot.c+=	-I${SRCTOP}/tests
 
+ATF_TESTS_C+=	nmount_check_fsid
+CFLAGS.nmount_check_fsid.c+=	-I${SRCTOP}/tests
+
 ATF_TESTS_SH+=	abi_root_symlink
 
+LDFLAGS=	-lutil
+
 #ATF_TESTS_SH+=	lookup_test
 
 TAP_TESTS_SH+=	trailing_slash
diff --git a/tests/sys/vfs/nmount_check_fsid.c b/tests/sys/vfs/nmount_check_fsid.c
new file mode 100644
index 000000000000..4185a9d1b832
--- /dev/null
+++ b/tests/sys/vfs/nmount_check_fsid.c
@@ -0,0 +1,238 @@
+/*-
+ * Copyright (c) 2026 Gleb Popov <arrowd@FreeBSD.org>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <sys/param.h>
+#include <sys/mount.h>
+#include <sys/uio.h>
+#include <sys/stat.h>
+
+#include <fcntl.h>
+#include <mntopts.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "freebsd_test_suite/macros.h"
+
+
+static char *
+fsid_to_str(fsid_t fsid)
+{
+	char* ret;
+	asprintf(&ret, "FSID:%d:%d", fsid.val[0], fsid.val[1]);
+	ATF_REQUIRE(ret != NULL);
+	return ret;
+}
+
+ATF_TC_WITH_CLEANUP(mount_check_fsid_positive);
+ATF_TC_HEAD(mount_check_fsid_positive, tc)
+{
+	atf_tc_set_md_var(tc, "descr", "Positive testcase for nmount() with check_fsid");
+}
+ATF_TC_BODY(mount_check_fsid_positive, tc)
+{
+	int mnt_fd = -1;
+	char mnt[PATH_MAX];
+	char *fsid;
+	struct statfs statfs_buf;
+	struct iovec *iov = NULL;
+	int iovlen = 0;
+
+	ATF_REQUIRE_INTEQ(0, mkdir("mnt", S_IRWXU));
+	ATF_REQUIRE(realpath("mnt", mnt) != NULL);
+
+	ATF_REQUIRE((mnt_fd = open(mnt, O_RDONLY)) >= 0);
+
+	ATF_REQUIRE_INTEQ(0, fstatfs(mnt_fd, &statfs_buf));
+
+	fsid = fsid_to_str(statfs_buf.f_fsid);
+
+	build_iovec(&iov, &iovlen, "fstype", __DECONST(void *, "tmpfs"), -1);
+	build_iovec(&iov, &iovlen, "from", __DECONST(void *, "tmpfs"), -1);
+	build_iovec(&iov, &iovlen, "fspath", mnt, -1);
+	build_iovec(&iov, &iovlen, "check_fsid", fsid, -1);
+
+	ATF_REQUIRE_INTEQ(0, nmount(iov, iovlen, 0));
+
+	free_iovec(&iov, &iovlen);
+	free(fsid);
+}
+ATF_TC_CLEANUP(mount_check_fsid_positive, tc)
+{
+	unmount("mnt", MNT_FORCE);
+}
+
+ATF_TC_WITH_CLEANUP(mount_check_fsid_negative);
+ATF_TC_HEAD(mount_check_fsid_negative, tc)
+{
+	atf_tc_set_md_var(tc, "descr", "Negative testcase for nmount() with check_fsid");
+}
+ATF_TC_BODY(mount_check_fsid_negative, tc)
+{
+	int mnt_fd = -1;
+	char mnt[PATH_MAX];
+	char *fsid;
+	struct statfs statfs_buf;
+	struct iovec *iov = NULL;
+	int iovlen = 0;
+
+	ATF_REQUIRE_INTEQ(0, mkdir("mnt", S_IRWXU));
+	ATF_REQUIRE(realpath("mnt", mnt) != NULL);
+
+	ATF_REQUIRE((mnt_fd = open("mnt", O_RDONLY)) >= 0);
+
+	ATF_REQUIRE_INTEQ(0, fstatfs(mnt_fd, &statfs_buf));
+
+	/* create invalid fsid */
+	statfs_buf.f_fsid.val[0]++;
+	statfs_buf.f_fsid.val[1]--;
+
+	fsid = fsid_to_str(statfs_buf.f_fsid);
+
+	build_iovec(&iov, &iovlen, "fstype", __DECONST(void *, "tmpfs"), -1);
+	build_iovec(&iov, &iovlen, "from", __DECONST(void *, "tmpfs"), -1);
+	build_iovec(&iov, &iovlen, "fspath", mnt, -1);
+	build_iovec(&iov, &iovlen, "check_fsid", fsid, -1);
+
+	ATF_REQUIRE_ERRNO(ENOENT, nmount(iov, iovlen, 0) < 0);
+
+	free_iovec(&iov, &iovlen);
+	free(fsid);
+}
+ATF_TC_CLEANUP(mount_check_fsid_negative, tc)
+{
+	unmount("mnt", MNT_FORCE);
+}
+
+ATF_TC_WITH_CLEANUP(update_check_fsid_positive);
+ATF_TC_HEAD(update_check_fsid_positive, tc)
+{
+	atf_tc_set_md_var(tc, "descr", "Positive testcase for updating a mount with check_fsid");
+}
+ATF_TC_BODY(update_check_fsid_positive, tc)
+{
+	int mnt_fd = -1;
+	char mnt[PATH_MAX];
+	char *fsid;
+	struct statfs statfs_buf;
+	struct iovec *iov = NULL;
+	int iovlen = 0;
+
+	ATF_REQUIRE_INTEQ(0, mkdir("mnt", S_IRWXU));
+	ATF_REQUIRE(realpath("mnt", mnt) != NULL);
+
+	build_iovec(&iov, &iovlen, "fstype", __DECONST(void *, "tmpfs"), -1);
+	build_iovec(&iov, &iovlen, "from", __DECONST(void *, "tmpfs"), -1);
+	build_iovec(&iov, &iovlen, "fspath", mnt, -1);
+
+	ATF_REQUIRE_INTEQ(0, nmount(iov, iovlen, 0));
+
+	free_iovec(&iov, &iovlen);
+
+	ATF_REQUIRE((mnt_fd = open(mnt, O_RDONLY)) >= 0);
+
+	ATF_REQUIRE_INTEQ(0, fstatfs(mnt_fd, &statfs_buf));
+
+	fsid = fsid_to_str(statfs_buf.f_fsid);
+
+	build_iovec(&iov, &iovlen, "fstype", __DECONST(void *, "tmpfs"), -1);
+	build_iovec(&iov, &iovlen, "from", __DECONST(void *, "tmpfs"), -1);
+	build_iovec(&iov, &iovlen, "fspath", mnt, -1);
+	build_iovec(&iov, &iovlen, "ro", NULL, -1);
+	build_iovec(&iov, &iovlen, "update", NULL, -1);
+	build_iovec(&iov, &iovlen, "check_fsid", fsid, -1);
+
+	ATF_REQUIRE_INTEQ(0, nmount(iov, iovlen, 0));
+
+	free_iovec(&iov, &iovlen);
+	free(fsid);
+}
+ATF_TC_CLEANUP(update_check_fsid_positive, tc)
+{
+	unmount("mnt", MNT_FORCE);
+}
+
+ATF_TC_WITH_CLEANUP(update_check_fsid_negative);
+ATF_TC_HEAD(update_check_fsid_negative, tc)
+{
+	atf_tc_set_md_var(tc, "descr", "Negative testcase for updating a mount with check_fsid");
+}
+ATF_TC_BODY(update_check_fsid_negative, tc)
+{
+	int mnt_fd = -1;
+	char mnt[PATH_MAX];
+	char *fsid;
+	struct statfs statfs_buf;
+	struct iovec *iov = NULL;
+	int iovlen = 0;
+
+	ATF_REQUIRE_INTEQ(0, mkdir("mnt", S_IRWXU));
+	ATF_REQUIRE(realpath("mnt", mnt) != NULL);
+
+	build_iovec(&iov, &iovlen, "fstype", __DECONST(void *, "tmpfs"), -1);
+	build_iovec(&iov, &iovlen, "from", __DECONST(void *, "tmpfs"), -1);
+	build_iovec(&iov, &iovlen, "fspath", mnt, -1);
+
+	ATF_REQUIRE_INTEQ(0, nmount(iov, iovlen, 0));
+
+	free_iovec(&iov, &iovlen);
+
+	ATF_REQUIRE((mnt_fd = open(mnt, O_RDONLY)) >= 0);
+
+	ATF_REQUIRE_INTEQ(0, fstatfs(mnt_fd, &statfs_buf));
+
+	/* create invalid fsid */
+	statfs_buf.f_fsid.val[0]++;
+	statfs_buf.f_fsid.val[1]--;
+
+	fsid = fsid_to_str(statfs_buf.f_fsid);
+
+	build_iovec(&iov, &iovlen, "fstype", __DECONST(void *, "tmpfs"), -1);
+	build_iovec(&iov, &iovlen, "from", __DECONST(void *, "tmpfs"), -1);
+	build_iovec(&iov, &iovlen, "fspath", mnt, -1);
+	build_iovec(&iov, &iovlen, "ro", NULL, -1);
+	build_iovec(&iov, &iovlen, "update", NULL, -1);
+	build_iovec(&iov, &iovlen, "check_fsid", fsid, -1);
+
+	ATF_REQUIRE_ERRNO(ENOENT, nmount(iov, iovlen, 0) < 0);
+
+	free_iovec(&iov, &iovlen);
+	free(fsid);
+}
+ATF_TC_CLEANUP(update_check_fsid_negative, tc)
+{
+	unmount("mnt", MNT_FORCE);
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+
+	ATF_TP_ADD_TC(tp, mount_check_fsid_positive);
+	ATF_TP_ADD_TC(tp, mount_check_fsid_negative);
+
+	ATF_TP_ADD_TC(tp, update_check_fsid_positive);
+	ATF_TP_ADD_TC(tp, update_check_fsid_negative);
+
+	return (atf_no_error());
+}