Page MenuHomeFreeBSD

D59930.diff
No OneTemporary

D59930.diff

diff --git a/lib/libsys/mount.2 b/lib/libsys/mount.2
--- a/lib/libsys/mount.2
+++ b/lib/libsys/mount.2
@@ -90,6 +90,26 @@
.Li fspath Ta mount point pathname (e.g., Dq Li /proc )
.El
.Pp
+The following options may be specified when mounting any file system:
+.Bl -column onto_fsid -offset indent
+.It
+.Li onto_fsid Ta fsid of a vnode being covered by the mount in the Dq Li FSID:%d:%d
+format
+.El
+.Pp
+The
+.Dq Li onto_fsid
+option can be used to avoid a TOCTOU (time-of-check to time-of-use) attacks
+when the caller wants to perform some checks on a directory being mounted into.
+There is an unavoidable time gap between checking and calling
+.Fn nmount ,
+which can be used by the attacker to change the directory via symlinks.
+To avoid that the caller can obtain a FSID value of the directory being covered
+by the mount and pass it to kernel.
+The kernel will deny mounting if the passed FSID value does not match the FSID
+of the mount point where the covered vnode lives.
+When doing a mount update, the caller should pass a FSID of the mounted FS.
+.Pp
Depending on the file system type, other options may be
recognized or required;
for example, most disk-based file systems require a
diff --git a/sys/kern/vfs_mount.c b/sys/kern/vfs_mount.c
--- a/sys/kern/vfs_mount.c
+++ b/sys/kern/vfs_mount.c
@@ -79,7 +79,7 @@
static int vfs_domount(struct thread *td, const char *fstype, char *fspath,
uint64_t fsflags, bool only_export, bool jail_export,
- struct vfsoptlist **optlist);
+ fsid_t *onto_fsid, struct vfsoptlist **optlist);
static void free_mntarg(struct mntarg *ma);
static void pnfsd_waitreplenish(struct mount *mp);
@@ -809,14 +809,17 @@
{
struct vfsoptlist *optlist;
struct vfsopt *opt, *tmp_opt;
- char *fstype, *fspath, *errmsg;
+ char *fstype, *fspath, *errmsg, *onto_fsid_str;
int error, fstypelen, fspathlen, errmsg_len, errmsg_pos;
bool autoro, has_nonexport, only_export, jail_export;
+ fsid_t onto_fsid;
+ fsid_t *onto_fsid_p;
- errmsg = fspath = NULL;
+ errmsg = fspath = onto_fsid_str = NULL;
errmsg_len = fspathlen = 0;
errmsg_pos = -1;
autoro = default_autoro;
+ onto_fsid_p = NULL;
error = vfs_buildopts(fsoptions, &optlist);
if (error)
@@ -977,6 +980,9 @@
} else if (strcmp(opt->name, "noemptydir") == 0) {
fsflags &= ~MNT_EMPTYDIR;
do_freeopt = 1;
+ } else if (strcmp(opt->name, "onto_fsid") == 0) {
+ onto_fsid_str = strndup(opt->value, opt->len, M_MOUNT);
+ do_freeopt = 1;
}
if (do_freeopt)
vfs_freeopt(optlist, opt);
@@ -992,6 +998,21 @@
goto bail;
}
+ if (onto_fsid_str) {
+ error = sscanf(onto_fsid_str, "FSID:%d:%d",
+ &onto_fsid.val[0], &onto_fsid.val[1]) != 2;
+ free(onto_fsid_str, M_MOUNT);
+ if (error) {
+ error = ENOENT;
+ if (errmsg != NULL) {
+ strncpy(errmsg, "Invalid or wrong fsid",
+ errmsg_len);
+ }
+ goto bail;
+ }
+ onto_fsid_p = &onto_fsid;
+ }
+
/*
* only_export is set to true only if exports are being
* updated and nothing else is being updated.
@@ -1008,12 +1029,17 @@
jail_export = true;
error = vfs_domount(td, fstype, fspath, fsflags, only_export,
- jail_export, &optlist);
+ jail_export, onto_fsid_p, &optlist);
if (error == ENODEV) {
error = EINVAL;
if (errmsg != NULL)
strncpy(errmsg, "Invalid fstype", errmsg_len);
goto bail;
+ } else if (error == ENOENT) {
+ if (errmsg != NULL)
+ strncpy(errmsg, "vnode's FSID does not match onto_fsid",
+ errmsg_len);
+ goto bail;
}
/*
@@ -1027,7 +1053,7 @@
" trying R/O mount\n", __func__);
fsflags |= MNT_RDONLY;
error = vfs_domount(td, fstype, fspath, fsflags, only_export,
- jail_export, &optlist);
+ jail_export, onto_fsid_p, &optlist);
}
bail:
/* copyout the errmsg */
@@ -1627,6 +1653,9 @@
uint64_t fsflags, /* Flags common to all filesystems. */
bool only_export, /* Got export option. */
bool jail_export, /* Got export option in vnet prison. */
+ fsid_t *onto_fsid, /* fsid of an inode we're expecting
+ to mount over.
+ */
struct vfsoptlist **optlist /* Options local to the filesystem. */
)
{
@@ -1696,6 +1725,19 @@
if (error != 0)
return (error);
vp = nd.ni_vp;
+ if (onto_fsid != NULL) {
+ struct mount *mp;
+
+ mp = vfs_getvfs(onto_fsid);
+ if (mp != vp->v_mount) {
+ vput(vp);
+ error = ENOENT;
+ }
+ 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
--- a/tests/sys/vfs/Makefile
+++ b/tests/sys/vfs/Makefile
@@ -5,6 +5,11 @@
ATF_TESTS_C+= lookup_cap_dotdot
CFLAGS.lookup_cap_dotdot.c+= -I${SRCTOP}/tests
+ATF_TESTS_C+= nmount_onto_fsid
+CFLAGS.nmount_onto_fsid.c+= -I${SRCTOP}/tests
+
+LDFLAGS= -lutil
+
#ATF_TESTS_SH+= lookup_test
TAP_TESTS_SH+= trailing_slash
diff --git a/tests/sys/vfs/nmount_onto_fsid.c b/tests/sys/vfs/nmount_onto_fsid.c
new file mode 100644
--- /dev/null
+++ b/tests/sys/vfs/nmount_onto_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_onto_fsid_positive);
+ATF_TC_HEAD(mount_onto_fsid_positive, tc)
+{
+ atf_tc_set_md_var(tc, "descr", "Positive testcase for nmount() with onto_fsid");
+}
+ATF_TC_BODY(mount_onto_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, "onto_fsid", fsid, -1);
+
+ ATF_REQUIRE_INTEQ(0, nmount(iov, iovlen, 0));
+
+ free_iovec(&iov, &iovlen);
+ free(fsid);
+}
+ATF_TC_CLEANUP(mount_onto_fsid_positive, tc)
+{
+ unmount("mnt", MNT_FORCE);
+}
+
+ATF_TC_WITH_CLEANUP(mount_onto_fsid_negative);
+ATF_TC_HEAD(mount_onto_fsid_negative, tc)
+{
+ atf_tc_set_md_var(tc, "descr", "Negative testcase for nmount() with onto_fsid");
+}
+ATF_TC_BODY(mount_onto_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, "onto_fsid", fsid, -1);
+
+ ATF_REQUIRE_ERRNO(ENOENT, nmount(iov, iovlen, 0) < 0);
+
+ free_iovec(&iov, &iovlen);
+ free(fsid);
+}
+ATF_TC_CLEANUP(mount_onto_fsid_negative, tc)
+{
+ unmount("mnt", MNT_FORCE);
+}
+
+ATF_TC_WITH_CLEANUP(update_onto_fsid_positive);
+ATF_TC_HEAD(update_onto_fsid_positive, tc)
+{
+ atf_tc_set_md_var(tc, "descr", "Positive testcase for updating a mount with onto_fsid");
+}
+ATF_TC_BODY(update_onto_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, "onto_fsid", fsid, -1);
+
+ ATF_REQUIRE_INTEQ(0, nmount(iov, iovlen, 0));
+
+ free_iovec(&iov, &iovlen);
+ free(fsid);
+}
+ATF_TC_CLEANUP(update_onto_fsid_positive, tc)
+{
+ unmount("mnt", MNT_FORCE);
+}
+
+ATF_TC_WITH_CLEANUP(update_onto_fsid_negative);
+ATF_TC_HEAD(update_onto_fsid_negative, tc)
+{
+ atf_tc_set_md_var(tc, "descr", "Negative testcase for updating a mount with onto_fsid");
+}
+ATF_TC_BODY(update_onto_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, "onto_fsid", fsid, -1);
+
+ ATF_REQUIRE_ERRNO(ENOENT, nmount(iov, iovlen, 0) < 0);
+
+ free_iovec(&iov, &iovlen);
+ free(fsid);
+}
+ATF_TC_CLEANUP(update_onto_fsid_negative, tc)
+{
+ unmount("mnt", MNT_FORCE);
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+
+ ATF_TP_ADD_TC(tp, mount_onto_fsid_positive);
+ ATF_TP_ADD_TC(tp, mount_onto_fsid_negative);
+
+ ATF_TP_ADD_TC(tp, update_onto_fsid_positive);
+ ATF_TP_ADD_TC(tp, update_onto_fsid_negative);
+
+ return (atf_no_error());
+}

File Metadata

Mime Type
text/plain
Expires
Thu, Sep 24, 9:02 PM (2 h, 21 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
39550709
Default Alt Text
D59930.diff (11 KB)

Event Timeline