NetBSD Problem Report #58901
From www@netbsd.org Fri Dec 13 21:52:27 2024
Return-Path: <www@netbsd.org>
Received: from mail.netbsd.org (mail.netbsd.org [199.233.217.200])
(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)
key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256
client-signature RSA-PSS (2048 bits) client-digest SHA256)
(Client CN "mail.NetBSD.org", Issuer "mail.NetBSD.org CA" (not verified))
by mollari.NetBSD.org (Postfix) with ESMTPS id 6AD361A923C
for <gnats-bugs@gnats.NetBSD.org>; Fri, 13 Dec 2024 21:52:27 +0000 (UTC)
Message-Id: <20241213215225.EA1551A923D@mollari.NetBSD.org>
Date: Fri, 13 Dec 2024 21:52:25 +0000 (UTC)
From: campbell+netbsd@mumble.net
Reply-To: campbell+netbsd@mumble.net
To: gnats-bugs@NetBSD.org
Subject: nfs server should allow specifying fsid in exports(5)
X-Send-Pr-Version: www-1.0
>Number: 58901
>Category: kern
>Synopsis: nfs server should allow specifying fsid in exports(5)
>Confidential: no
>Severity: serious
>Priority: medium
>Responsible: kern-bug-people
>State: open
>Class: sw-bug
>Submitter-Id: net
>Arrival-Date: Fri Dec 13 21:55:00 +0000 2024
>Last-Modified: Wed Aug 26 04:00:02 +0000 2026
>Originator: Taylor R Campbell
>Release: current, 10, 9, ...
>Organization:
The Nfsid Foundinode
>Environment:
>Description:
When exporting a local file system, the NFS server uses the fsid of the local file system. This fsid is often assembled by boot-dependent quantities like the ordering of bus scanning or file system mounting:
1523 mp->mnt_stat.f_fsidx.__fsid_val[0] = (long)dev;
1524 mp->mnt_stat.f_fsidx.__fsid_val[1] = makefstype(MOUNT_FFS);
1525 mp->mnt_stat.f_fsid = mp->mnt_stat.f_fsidx.__fsid_val[0];
https://nxr.netbsd.org/xref/src/sys/ufs/ffs/ffs_vfsops.c?r=1.382#1523
232 mutex_enter(&mntid_lock);
233 if (xxxfs_mntid == 0)
234 ++xxxfs_mntid;
235 mtype = makefstype(mp->mnt_op->vfs_name);
236 tfsid.__fsid_val[0] = makedev(mtype & 0xff, xxxfs_mntid);
237 tfsid.__fsid_val[1] = mtype;
238 /* Always increment to not return the same fsid to parallel mounts. */
239 xxxfs_mntid++;
240
241 /*
242 * Directly walk mountlist to prevent deadlock through
243 * mountlist_iterator_next() -> vfs_busy().
244 */
245 mutex_enter(&mountlist_lock);
246 for (me = TAILQ_FIRST(&mountlist); me != TAILQ_END(&mountlist); ) {
247 if (me->me_type == ME_MOUNT &&
248 me->me_mount->mnt_stat.f_fsidx.__fsid_val[0] ==
249 tfsid.__fsid_val[0] &&
250 me->me_mount->mnt_stat.f_fsidx.__fsid_val[1] ==
251 tfsid.__fsid_val[1]) {
252 tfsid.__fsid_val[0]++;
253 xxxfs_mntid++;
254 me = TAILQ_FIRST(&mountlist);
255 } else {
256 me = TAILQ_NEXT(me, me_list);
257 }
258 }
259 mutex_exit(&mountlist_lock);
260
261 mp->mnt_stat.f_fsidx.__fsid_val[0] = tfsid.__fsid_val[0];
262 mp->mnt_stat.f_fsidx.__fsid_val[1] = tfsid.__fsid_val[1];
263 mp->mnt_stat.f_fsid = mp->mnt_stat.f_fsidx.__fsid_val[0];
https://nxr.netbsd.org/xref/src/sys/kern/vfs_mount.c#221
If anything changes this -- you move disks around or change partitions so the dkN numbers change, you edit /etc/fstab to mount file systems in a different order, or whatever -- all NFS file handles for what should be the `same' local file system will change.
Worse, if /export/A is mounted from /dev/dk3 and /export/B from /dev/dk4 at first, and then they are exchanged so /export/A is mounted from /dev/dk4 and /export/B is mounted from /dev/dk3, file handles could collide, causing the wrong data to be read or written by clients with cached file handles.
zfs avoids this by randomly generating its fsids and storing them in the file system, so they're independent of the device enumeration at boot and the number of times other file systems have been mounted. But file systems that haven't reserved space in their metadata for this can't take advantage of the same approach.
>How-To-Repeat:
1. Mount a file system on /dev/dkN via named wedge and export it.
2. Mount a file system on /dev/dkM via named wedge and export it (M =/= N).
3. Start some clients caching some file handles.
4. Reboot and rejigger the hardware or partitioning so dkN and dkM are swapped but the same file systems are mounted at the same paths as before.
5. ???
6. Profit!
>Fix:
1. Create new syntax (or search other NFS server software for syntax) in /etc/exports for explicitly specifying the fsid of each exported file system.
2. Decide how to detect and address the entirely new problem of accidentally exporting an _actually different_ file system at the same path with the same fsid recorded in /etc/exports.
>Audit-Trail:
From: Taylor R Campbell <riastradh@NetBSD.org>
To: gnats-bugs@NetBSD.org, netbsd-bugs@NetBSD.org
Cc: yamt@NetBSD.org
Subject: Re: kern/58901: nfs server should allow specifying fsid in exports(5)
Date: Wed, 26 Aug 2026 03:51:26 +0000
The attached patch aims to implement a new exports(5) option
-fsid=0x12345678.0x0abcdef9
to set the exported fsid to {0x12345678,0x0abcdef9}, or
-fsid=0x01020304.0x05060708:0x12345678.0x0abcdef9
to verify that the local fsid is {0x01020304,0x05060708}, and if it
is, export it as {0x12345678,0x0abcdef9}. (If not, mountd(8) will
fail to export the mount point at all, so you can't accidentally
export a file system that has the wrong local fsid internally.)
I initially used `,' instead of `.' as the separator but that fell
afoul of the mountd(8) parser for exports(5) options somehow.
I have only verified that /usr/tests/fs/nfs and /usr/tests/fs/vfs
pass, and (by inserting panics into it) that it's using the new code
paths, and (by examining debug messages) that it parses the option
sensibly; I haven't verified that it actually exports the fsid as
requested. (Perhaps showmount(8) should sprout an option to dump the
raw fhandles of the mount points to make this easier.)
This should relieve some of the pressure we have to keep fsids
consistent, so we can perhaps, e.g., start using the struct fs::fs_id
member of the ffs superblock we've been initializing but not using for
decades (PR kern/58902: ffs ignores fs_id), and/or get rid of that
awful hash function in makefstype, and/or enforce fsid uniqueness
locally (PR kern/60135: fsid may not be unique).
From: Taylor R Campbell <riastradh@NetBSD.org>
To: gnats-bugs@NetBSD.org, netbsd-bugs@NetBSD.org
Cc: yamt@NetBSD.org
Subject: Re: kern/58901: nfs server should allow specifying fsid in exports(5)
Date: Wed, 26 Aug 2026 03:56:27 +0000
This is a multi-part message in MIME format.
--=_doTlMKKkYbLBtme6fab/51tYZGWhbizH
> Date: Wed, 26 Aug 2026 03:51:26 +0000
> From: Taylor R Campbell <riastradh@NetBSD.org>
>
> The attached patch aims to implement a new exports(5) option
> [...]
Coulda sworn I confirmed my mailer said it was attached before I hit
send!
--=_doTlMKKkYbLBtme6fab/51tYZGWhbizH
Content-Type: text/plain; charset="ISO-8859-1"; name="pr58901-nfsexportfsid"
Content-Transfer-Encoding: quoted-printable
Content-Disposition: attachment; filename="pr58901-nfsexportfsid.patch"
# HG changeset patch
# User Taylor R Campbell <riastradh@NetBSD.org>
# Date 1787601051 0
# Mon Aug 24 19:50:51 2026 +0000
# Branch trunk
# Node ID 3c615e360a5e8c96d95f1de403834c27fb2799bb
# Parent 2db9820dc602fe20366ec34e15e1cface0847cc8
# EXP-Topic riastradh-pr58901-nfsexportfsid
WIP: exports(5): New -fsid option to verify and control exported fsids.
This extends nfssvc(2) to support specifying the expected fsid and
the fsid to be published. Rather than just use up more flag bits
(which has always been a little weird for what is mostly a set of
mutually exclusive operations), I took an existing unused combination
of flag bits, NFSSVC_SETEXPORTSLIST | NFSSVC_REPLACEEXPORTSLIST, to
make a new extensible operation, NFSSVC_REPLACEEXPORTSEXT.
NFSSVC_REPLACEEXPORTSEXT passes a buffer of export commands for a
single mount point to the kernel, with two commands defined now --
MOUNTD_EXPORT_ARGS for the export args as previously passed to
NFSSVC_REPLACEEXPORTSLIST, and MOUNTD_EXPORT_SET_FSID to verify the
internal fsid and set the external one fsid -- and ample room for
future changes later.
The interface uses all fixed-size types so there is no need for new
compat32 logic. MOUNTD_EXPORT_ARGS commands are processed just like
NFSSVC_REPLACEEXPORTSLIST, so both paths still exercise the same
logic after the arguments are copied in. This keeps the amount of
hard-to-exercise compat code down.
PR kern/58901: nfs server should allow specifying fsid in exports(5)
diff -r 2db9820dc602 -r 3c615e360a5e sys/nfs/nfs.h
--- a/sys/nfs/nfs.h Wed Aug 26 03:28:28 2026 +0000
+++ b/sys/nfs/nfs.h Mon Aug 24 19:50:51 2026 +0000
@@ -44,6 +44,9 @@
#include <sys/rbtree.h>
#endif
=20
+#include <sys/stdalign.h>
+#include <sys/stddef.h>
+
#include <nfs/rpcv2.h>
=20
/*
@@ -230,6 +233,40 @@ struct mountd_exports_list {
struct export_args *mel_exports;
};
=20
+struct mountd_exports_ext {
+ uint64_t mext_pathptr;
+ uint64_t mext_entriesptr;
+ uint16_t mext_entriesbytes;
+ uint8_t mext_pad[6];
+};
+
+struct mountd_export_entry {
+ uint8_t ment_cmd;
+ uint8_t ment_len;
+ uint8_t ment_pad[6];
+ uint8_t ment_data[/*ment_len*/];
+} __aligned(8);
+
+#define MOUNTD_EXPORT_ENTRYSIZE(size) \
+ roundup(offsetof(struct mountd_export_entry, ment_data[(size)]), \
+ alignof(struct mountd_export_entry))
+#define MOUNTD_EXPORT_ENTRYNEXT(ment) \
+ ((struct mountd_export_entry *)&(ment)->ment_data[(ment)->ment_len])
+
+#define MOUNTD_EXPORT_ARGS 0
+#define MOUNTD_EXPORT_SET_FSID 1
+
+struct mountd_export_entry_args {
+ uint64_t mea_uptr;
+ uint32_t mea_nexports;
+ uint32_t mea_pad;
+};
+
+struct mountd_export_entry_set_fsid {
+ fsid_t mesf_int_fsid;
+ fsid_t mesf_ext_fsid;
+};
+
/*
* try to keep nfsstats, which is exposed to userland via sysctl,
* compatible after NQNFS removal.
@@ -288,6 +325,7 @@ struct nfsstats {
#define NFSSVC_MNTD 0x100
#define NFSSVC_SETEXPORTSLIST 0x200
#define NFSSVC_REPLACEEXPORTSLIST 0x400
+#define NFSSVC_REPLACEEXPORTSEXT 0x600
=20
/*
* fs.nfs sysctl(3) identifiers
diff -r 2db9820dc602 -r 3c615e360a5e sys/nfs/nfs_export.c
--- a/sys/nfs/nfs_export.c Wed Aug 26 03:28:28 2026 +0000
+++ b/sys/nfs/nfs_export.c Mon Aug 24 19:50:51 2026 +0000
@@ -121,6 +121,7 @@ struct netexport {
TAILQ_ENTRY(netexport) ne_list;
unsigned ne_nexports;
struct mount *ne_mount;
+ fsid_t ne_fsid;
struct netcred ne_defexported; /* Default export */
struct radix_node_head *ne_rtable[AF_MAX+1]; /* Individual exports */
};
@@ -240,6 +241,25 @@ int
mountd_set_exports_list(const struct mountd_exports_list *mel, struct lwp =
*l,
struct mount *nmp, int cmd)
{
+
+ return mountd_set_exports_ext(mel->mel_path, l, nmp, mel->mel_exports,
+ mel->mel_nexports, /*int_fsidp*/NULL, /*ext_fsidp*/NULL, cmd);
+}
+
+/*
+ * mountd_set_exports_ext(upath, l, mp, exports, nexports, int_fsid, ext_f=
sid,
+ * cmd)
+ *
+ * Set up the exports list of a mount point.
+ *
+ * This is NOT part of the user ABI or kernel module ABI, so it
+ * can freely be extended.
+ */
+int
+mountd_set_exports_ext(const char *upath, struct lwp *l, struct mount *nmp,
+ const struct export_args *exports, size_t nexports,
+ const fsid_t *int_fsidp, const fsid_t *ext_fsidp, int cmd)
+{
int error;
size_t i;
struct mount *mp;
@@ -254,7 +274,7 @@ mountd_set_exports_list(const struct mou
return EPERM;
=20
/* Look up the file system path. */
- error =3D pathbuf_copyin(mel->mel_path, &pb);
+ error =3D pathbuf_copyin(upath, &pb);
if (error) {
return error;
}
@@ -270,6 +290,19 @@ mountd_set_exports_list(const struct mou
pathbuf_destroy(pb);
=20
/*
+ * If the caller specified an expected internal fsid, check it.
+ */
+ if (int_fsidp) {
+ if ((int_fsidp->__fsid_val[0] !=3D
+ mp->mnt_stat.f_fsidx.__fsid_val[0]) ||
+ (int_fsidp->__fsid_val[0] !=3D
+ mp->mnt_stat.f_fsidx.__fsid_val[1])) {
+ vput(vp);
+ return EXDEV; /* XXX which errno? */
+ }
+ }
+
+ /*
* Make sure the file system can do vptofh. If the file system
* knows the handle's size, just trust it's able to do the
* actual translation also (otherwise we should check fhtovp
@@ -301,10 +334,10 @@ mountd_set_exports_list(const struct mou
KASSERT(ne->ne_mount =3D=3D mp);
=20
if (cmd =3D=3D NFSSVC_SETEXPORTSLIST) {
- if (mel->mel_nexports =3D=3D 0)
+ if (nexports =3D=3D 0)
netexport_clear(ne);
- else if (mel->mel_nexports =3D=3D 1)
- error =3D export(ne, &mel->mel_exports[0]);
+ else if (nexports =3D=3D 1)
+ error =3D export(ne, &exports[0]);
else {
printf("%s: Cannot set more than one "
"entry at once (unimplemented)\n", __func__);
@@ -312,12 +345,19 @@ mountd_set_exports_list(const struct mou
}
} else if (cmd =3D=3D NFSSVC_REPLACEEXPORTSLIST) {
netexport_clear(ne);
- for (i =3D 0; error =3D=3D 0 && i < mel->mel_nexports; i++)
- error =3D export(ne, &mel->mel_exports[i]);
+ for (i =3D 0; error =3D=3D 0 && i < nexports; i++)
+ error =3D export(ne, &exports[i]);
} else {
printf("%s: Command %#x not implemented\n", __func__, cmd);
error =3D EOPNOTSUPP;
}
+ if (error)
+ goto out;
+
+ /*
+ * All good. Set the external fsid for the export.
+ */
+ ne->ne_fsid =3D (ext_fsidp ? *ext_fsidp : mp->mnt_stat.f_fsidx);
=20
out:
netexport_wrunlock();
@@ -362,10 +402,8 @@ netexport_lookup_byfsid(const fsid_t *fs
struct netexport *ne;
=20
TAILQ_FOREACH(ne, &netexport_list, ne_list) {
- const struct mount *mp =3D ne->ne_mount;
-
- if (mp->mnt_stat.f_fsidx.__fsid_val[0] =3D=3D fsid->__fsid_val[0] &&
- mp->mnt_stat.f_fsidx.__fsid_val[1] =3D=3D fsid->__fsid_val[1]) {
+ if (ne->ne_fsid.__fsid_val[0] =3D=3D fsid->__fsid_val[0] &&
+ ne->ne_fsid.__fsid_val[1] =3D=3D fsid->__fsid_val[1]) {
goto done;
}
}
@@ -435,6 +473,7 @@ nfs_export_update_30(struct mount *mp, c
/* Request to delete exports. The mask above holds the
* value that used to be in MNT_DELEXPORT. */
mel.mel_nexports =3D 0;
+ mel.mel_exports =3D NULL;
} else {
/*
* The following code assumes export_args has not
diff -r 2db9820dc602 -r 3c615e360a5e sys/nfs/nfs_serv.c
--- a/sys/nfs/nfs_serv.c Wed Aug 26 03:28:28 2026 +0000
+++ b/sys/nfs/nfs_serv.c Mon Aug 24 19:50:51 2026 +0000
@@ -180,7 +180,7 @@ nfsrv3_access(struct nfsrv_descript *nfs
=20
nfsm_srvmtofh(&nsfh);
nfsm_dissect(tl, u_int32_t *, NFSX_UNSIGNED);
- error =3D nfsrv_fhtovp(&nsfh, 1, &vp, cred, slp, nam, &rdonly,
+ error =3D nfsrv_fhtovp(&nsfh, 1, &vp, NULL, cred, slp, nam, &rdonly,
(nfsd->nd_flag & ND_KERBAUTH), false);
if (error) {
nfsm_reply(NFSX_UNSIGNED);
@@ -242,7 +242,7 @@ nfsrv_getattr(struct nfsrv_descript *nfs
u_quad_t frev;
=20
nfsm_srvmtofh(&nsfh);
- error =3D nfsrv_fhtovp(&nsfh, 1, &vp, cred, slp, nam, &rdonly,
+ error =3D nfsrv_fhtovp(&nsfh, 1, &vp, NULL, cred, slp, nam, &rdonly,
(nfsd->nd_flag & ND_KERBAUTH), false);
if (error) {
nfsm_reply(0);
@@ -330,7 +330,7 @@ nfsrv_setattr(struct nfsrv_descript *nfs
/*
* Now that we have all the fields, lets do it.
*/
- error =3D nfsrv_fhtovp(&nsfh, 1, &vp, cred, slp, nam, &rdonly,
+ error =3D nfsrv_fhtovp(&nsfh, 1, &vp, NULL, cred, slp, nam, &rdonly,
(nfsd->nd_flag & ND_KERBAUTH), false);
if (error) {
nfsm_reply(2 * NFSX_UNSIGNED);
@@ -401,6 +401,7 @@ nfsrv_lookup(struct nfsrv_descript *nfsd
struct pathbuf *ipb =3D NULL;
struct vnode *vp, *dirp;
nfsrvfh_t nsfh;
+ fsid_t fsid;
char *cp;
u_int32_t *tl;
int32_t t1;
@@ -421,7 +422,7 @@ nfsrv_lookup(struct nfsrv_descript *nfsd
nd.ni_cnd.cn_cred =3D cred;
nd.ni_cnd.cn_nameiop =3D LOOKUP;
nd.ni_cnd.cn_flags =3D LOCKLEAF;
- error =3D nfs_namei(&nd, &nsfh, len, slp, nam, &md, &dpos,
+ error =3D nfs_namei(&nd, &nsfh, &fsid, len, slp, nam, &md, &dpos,
&dirp, NULL, NULL,
lwp, (nfsd->nd_flag & ND_KERBAUTH), pubflag);
=20
@@ -488,7 +489,7 @@ nfsrv_lookup(struct nfsrv_descript *nfsd
pathbuf_destroy(ipb);
}
vp =3D ndp->ni_vp;
- error =3D nfsrv_composefh(vp, &nsfh, v3);
+ error =3D nfsrv_composefh(vp, &nsfh, &fsid, v3);
if (!error)
error =3D VOP_GETATTR(vp, &va, cred);
vput(vp);
@@ -574,7 +575,7 @@ nfsrv_readlink(struct nfsrv_descript *nf
uiop->uio_resid =3D len;
uiop->uio_rw =3D UIO_READ;
UIO_SETUP_SYSSPACE(uiop);
- error =3D nfsrv_fhtovp(&nsfh, 1, &vp, cred, slp, nam,
+ error =3D nfsrv_fhtovp(&nsfh, 1, &vp, NULL, cred, slp, nam,
&rdonly, (nfsd->nd_flag & ND_KERBAUTH), false);
if (error) {
m_freem(mp3);
@@ -655,7 +656,7 @@ nfsrv_read(struct nfsrv_descript *nfsd,=20
nfsm_dissect(tl, uint32_t *, NFSX_UNSIGNED);
reqlen =3D fxdr_unsigned(uint32_t, *tl);
reqlen =3D MIN(reqlen, NFS_SRVMAXDATA(nfsd));
- error =3D nfsrv_fhtovp(&nsfh, 1, &vp, cred, slp, nam,
+ error =3D nfsrv_fhtovp(&nsfh, 1, &vp, NULL, cred, slp, nam,
&rdonly, (nfsd->nd_flag & ND_KERBAUTH), false);
if (error) {
nfsm_reply(2 * NFSX_UNSIGNED);
@@ -924,7 +925,7 @@ nfsrv_write(struct nfsrv_descript *nfsd,
nfsm_srvwcc_data(forat_ret, &forat, aftat_ret, &va);
return (0);
}
- error =3D nfsrv_fhtovp(&nsfh, 1, &vp, cred, slp, nam,
+ error =3D nfsrv_fhtovp(&nsfh, 1, &vp, NULL, cred, slp, nam,
&rdonly, (nfsd->nd_flag & ND_KERBAUTH), false);
if (error) {
nfsm_reply(2 * NFSX_UNSIGNED);
@@ -1223,7 +1224,7 @@ loop1:
cred =3D nfsd->nd_cr;
v3 =3D (nfsd->nd_flag & ND_NFSV3);
forat_ret =3D aftat_ret =3D 1;
- error =3D nfsrv_fhtovp(&nfsd->nd_fh, 1, &vp, cred, slp,
+ error =3D nfsrv_fhtovp(&nfsd->nd_fh, 1, &vp, NULL, cred, slp,
nfsd->nd_nam, &rdonly, (nfsd->nd_flag & ND_KERBAUTH),
false);
if (!error) {
@@ -1438,6 +1439,7 @@ nfsrv_create(struct nfsrv_descript *nfsd
struct mbuf *mb, *mreq __unused;
struct vnode *vp =3D NULL, *dirp =3D NULL;
nfsrvfh_t nsfh;
+ fsid_t fsid;
u_quad_t frev, tempsize;
u_char cverf[NFSX_V3CREATEVERF];
=20
@@ -1447,7 +1449,7 @@ nfsrv_create(struct nfsrv_descript *nfsd
nd.ni_cnd.cn_cred =3D cred;
nd.ni_cnd.cn_nameiop =3D CREATE;
nd.ni_cnd.cn_flags =3D LOCKPARENT | LOCKLEAF;
- error =3D nfs_namei(&nd, &nsfh, len, slp, nam, &md, &dpos,
+ error =3D nfs_namei(&nd, &nsfh, &fsid, len, slp, nam, &md, &dpos,
&dirp, (v3 ? &dirfor_ret : NULL), &dirfor,
lwp, (nfsd->nd_flag & ND_KERBAUTH), false);
if (error) {
@@ -1589,7 +1591,7 @@ nfsrv_create(struct nfsrv_descript *nfsd
vput(vp);
}
if (!error) {
- error =3D nfsrv_composefh(vp, &nsfh, v3);
+ error =3D nfsrv_composefh(vp, &nsfh, &fsid, v3);
if (!error)
error =3D VOP_GETATTR(vp, &va, cred);
vput(vp);
@@ -1679,6 +1681,7 @@ nfsrv_mknod(struct nfsrv_descript *nfsd,
struct mbuf *mb, *mreq __unused;
struct vnode *vp, *dirp =3D (struct vnode *)0;
nfsrvfh_t nsfh;
+ fsid_t fsid;
u_quad_t frev;
=20
nd.ni_cnd.cn_nameiop =3D 0;
@@ -1687,7 +1690,7 @@ nfsrv_mknod(struct nfsrv_descript *nfsd,
nd.ni_cnd.cn_cred =3D cred;
nd.ni_cnd.cn_nameiop =3D CREATE;
nd.ni_cnd.cn_flags =3D LOCKPARENT | LOCKLEAF;
- error =3D nfs_namei(&nd, &nsfh, len, slp, nam, &md, &dpos,
+ error =3D nfs_namei(&nd, &nsfh, &fsid, len, slp, nam, &md, &dpos,
&dirp, &dirfor_ret, &dirfor,
lwp, (nfsd->nd_flag & ND_KERBAUTH), false);
if (error) {
@@ -1763,7 +1766,7 @@ abort:
out:
vp =3D nd.ni_vp;
if (!error) {
- error =3D nfsrv_composefh(vp, &nsfh, true);
+ error =3D nfsrv_composefh(vp, &nsfh, &fsid, true);
if (!error)
error =3D VOP_GETATTR(vp, &va, cred);
vput(vp);
@@ -1842,7 +1845,7 @@ nfsrv_remove(struct nfsrv_descript *nfsd
nd.ni_cnd.cn_cred =3D cred;
nd.ni_cnd.cn_nameiop =3D DELETE;
nd.ni_cnd.cn_flags =3D LOCKPARENT | LOCKLEAF;
- error =3D nfs_namei(&nd, &nsfh, len, slp, nam, &md, &dpos,
+ error =3D nfs_namei(&nd, &nsfh, NULL, len, slp, nam, &md, &dpos,
&dirp, (v3 ? &dirfor_ret : NULL), &dirfor,
lwp, (nfsd->nd_flag & ND_KERBAUTH), false);
if (!error) {
@@ -1935,7 +1938,7 @@ nfsrv_rename(struct nfsrv_descript *nfsd
fromnd.ni_cnd.cn_cred =3D cred;
fromnd.ni_cnd.cn_nameiop =3D DELETE;
fromnd.ni_cnd.cn_flags =3D LOCKPARENT;
- error =3D nfs_namei(&fromnd, &fnsfh, len, slp, nam, &md,
+ error =3D nfs_namei(&fromnd, &fnsfh, NULL, len, slp, nam, &md,
&dpos, &fdirp, (v3 ? &fdirfor_ret : NULL), &fdirfor,
lwp, (nfsd->nd_flag & ND_KERBAUTH), false);
if (error) {
@@ -2006,7 +2009,7 @@ nfsrv_rename(struct nfsrv_descript *nfsd
tond.ni_cnd.cn_cred =3D cred;
tond.ni_cnd.cn_nameiop =3D RENAME;
tond.ni_cnd.cn_flags =3D LOCKPARENT | LOCKLEAF | NOCACHE;
- error =3D nfs_namei(&tond, &tnsfh, len2, slp, nam, &md,
+ error =3D nfs_namei(&tond, &tnsfh, NULL, len2, slp, nam, &md,
&dpos, &tdirp, (v3 ? &tdirfor_ret : NULL), &tdirfor,
lwp, (nfsd->nd_flag & ND_KERBAUTH), false);
if (error) {
@@ -2186,7 +2189,7 @@ nfsrv_link(struct nfsrv_descript *nfsd,=20
nfsm_srvmtofh(&nsfh);
nfsm_srvmtofh(&dnsfh);
nfsm_srvnamesiz(len);
- error =3D nfsrv_fhtovp(&nsfh, false, &vp, cred, slp, nam,
+ error =3D nfsrv_fhtovp(&nsfh, false, &vp, NULL, cred, slp, nam,
&rdonly, (nfsd->nd_flag & ND_KERBAUTH), false);
if (error) {
nfsm_reply(NFSX_POSTOPATTR(v3) + NFSX_WCCDATA(v3));
@@ -2201,7 +2204,7 @@ nfsrv_link(struct nfsrv_descript *nfsd,=20
nd.ni_cnd.cn_cred =3D cred;
nd.ni_cnd.cn_nameiop =3D CREATE;
nd.ni_cnd.cn_flags =3D LOCKPARENT;
- error =3D nfs_namei(&nd, &dnsfh, len, slp, nam, &md, &dpos,
+ error =3D nfs_namei(&nd, &dnsfh, NULL, len, slp, nam, &md, &dpos,
&dirp, (v3 ? &dirfor_ret : NULL), &dirfor,
lwp, (nfsd->nd_flag & ND_KERBAUTH), false);
if (error)
@@ -2283,6 +2286,7 @@ nfsrv_symlink(struct nfsrv_descript *nfs
struct mbuf *mb, *mreq __unused;
struct vnode *dirp =3D (struct vnode *)0;
nfsrvfh_t nsfh;
+ fsid_t fsid;
u_quad_t frev;
=20
nd.ni_cnd.cn_nameiop =3D 0;
@@ -2291,7 +2295,7 @@ nfsrv_symlink(struct nfsrv_descript *nfs
nd.ni_cnd.cn_cred =3D cred;
nd.ni_cnd.cn_nameiop =3D CREATE;
nd.ni_cnd.cn_flags =3D LOCKPARENT;
- error =3D nfs_namei(&nd, &nsfh, len, slp, nam, &md, &dpos,
+ error =3D nfs_namei(&nd, &nsfh, &fsid, len, slp, nam, &md, &dpos,
&dirp, (v3 ? &dirfor_ret : NULL), &dirfor,
lwp, (nfsd->nd_flag & ND_KERBAUTH), false);
if (error)
@@ -2346,7 +2350,7 @@ abortop:
if (!error) {
if (v3) {
vn_lock(nd.ni_vp, LK_SHARED | LK_RETRY);
- error =3D nfsrv_composefh(nd.ni_vp, &nsfh, v3);
+ error =3D nfsrv_composefh(nd.ni_vp, &nsfh, &fsid, v3);
if (!error)
error =3D VOP_GETATTR(nd.ni_vp, &va, cred);
vput(nd.ni_vp);
@@ -2427,6 +2431,7 @@ nfsrv_mkdir(struct nfsrv_descript *nfsd,
struct mbuf *mb, *mreq __unused;
struct vnode *vp, *dirp =3D (struct vnode *)0;
nfsrvfh_t nsfh;
+ fsid_t fsid;
u_quad_t frev;
=20
nfsm_srvmtofh(&nsfh);
@@ -2434,7 +2439,7 @@ nfsrv_mkdir(struct nfsrv_descript *nfsd,
nd.ni_cnd.cn_cred =3D cred;
nd.ni_cnd.cn_nameiop =3D CREATE;
nd.ni_cnd.cn_flags =3D LOCKPARENT;
- error =3D nfs_namei(&nd, &nsfh, len, slp, nam, &md, &dpos,
+ error =3D nfs_namei(&nd, &nsfh, &fsid, len, slp, nam, &md, &dpos,
&dirp, (v3 ? &dirfor_ret : NULL), &dirfor,
lwp, (nfsd->nd_flag & ND_KERBAUTH), false);
if (error) {
@@ -2474,7 +2479,7 @@ nfsrv_mkdir(struct nfsrv_descript *nfsd,
if (!error) {
vp =3D nd.ni_vp;
vn_lock(vp, LK_SHARED | LK_RETRY);
- error =3D nfsrv_composefh(vp, &nsfh, v3);
+ error =3D nfsrv_composefh(vp, &nsfh, &fsid, v3);
if (!error)
error =3D VOP_GETATTR(vp, &va, cred);
vput(vp);
@@ -2556,7 +2561,7 @@ nfsrv_rmdir(struct nfsrv_descript *nfsd,
nd.ni_cnd.cn_cred =3D cred;
nd.ni_cnd.cn_nameiop =3D DELETE;
nd.ni_cnd.cn_flags =3D LOCKPARENT | LOCKLEAF;
- error =3D nfs_namei(&nd, &nsfh, len, slp, nam, &md, &dpos,
+ error =3D nfs_namei(&nd, &nsfh, NULL, len, slp, nam, &md, &dpos,
&dirp, (v3 ? &dirfor_ret : NULL), &dirfor,
lwp, (nfsd->nd_flag & ND_KERBAUTH), false);
if (error) {
@@ -2717,7 +2722,7 @@ nfsrv_readdir(struct nfsrv_descript *nfs
if (siz > xfer)
siz =3D xfer;
fullsiz =3D siz;
- error =3D nfsrv_fhtovp(&nsfh, 1, &vp, cred, slp, nam,
+ error =3D nfsrv_fhtovp(&nsfh, 1, &vp, NULL, cred, slp, nam,
&rdonly, (nfsd->nd_flag & ND_KERBAUTH), false);
if (!error && vp->v_type !=3D VDIR) {
error =3D ENOTDIR;
@@ -2948,6 +2953,7 @@ nfsrv_readdirplus(struct nfsrv_descript=20
struct vnode *vp, *nvp;
struct flrep fl;
nfsrvfh_t nsfh;
+ fsid_t fsid;
struct uio io;
struct iovec iv;
struct vattr va, at, *vap =3D &va;
@@ -2980,7 +2986,7 @@ nfsrv_readdirplus(struct nfsrv_descript=20
if (siz > xfer)
siz =3D xfer;
fullsiz =3D siz;
- error =3D nfsrv_fhtovp(&nsfh, 1, &vp, cred, slp, nam,
+ error =3D nfsrv_fhtovp(&nsfh, 1, &vp, &fsid, cred, slp, nam,
&rdonly, (nfsd->nd_flag & ND_KERBAUTH), false);
if (!error && vp->v_type !=3D VDIR) {
error =3D ENOTDIR;
@@ -3132,7 +3138,8 @@ again:
if (VFS_VGET(vp->v_mount, dp->d_fileno, LK_EXCLUSIVE,
&nvp))
goto invalid;
- if (nfsrv_composefh(nvp, &nnsfh, true)) {
+ KASSERT(nvp->v_mount =3D=3D vp->v_mount);
+ if (nfsrv_composefh(nvp, &nnsfh, &fsid, true)) {
vput(nvp);
goto invalid;
}
@@ -3289,7 +3296,7 @@ nfsrv_commit(struct nfsrv_descript *nfsd
off =3D fxdr_hyper(tl);
tl +=3D 2;
cnt =3D fxdr_unsigned(uint32_t, *tl);
- error =3D nfsrv_fhtovp(&nsfh, 1, &vp, cred, slp, nam,
+ error =3D nfsrv_fhtovp(&nsfh, 1, &vp, NULL, cred, slp, nam,
&rdonly, (nfsd->nd_flag & ND_KERBAUTH), false);
if (error) {
nfsm_reply(2 * NFSX_UNSIGNED);
@@ -3344,7 +3351,7 @@ nfsrv_statfs(struct nfsrv_descript *nfsd
u_quad_t frev, tval;
=20
nfsm_srvmtofh(&nsfh);
- error =3D nfsrv_fhtovp(&nsfh, 1, &vp, cred, slp, nam,
+ error =3D nfsrv_fhtovp(&nsfh, 1, &vp, NULL, cred, slp, nam,
&rdonly, (nfsd->nd_flag & ND_KERBAUTH), false);
if (error) {
nfsm_reply(NFSX_UNSIGNED);
@@ -3414,7 +3421,7 @@ nfsrv_fsinfo(struct nfsrv_descript *nfsd
struct statvfs *sb;
=20
nfsm_srvmtofh(&nsfh);
- error =3D nfsrv_fhtovp(&nsfh, 1, &vp, cred, slp, nam,
+ error =3D nfsrv_fhtovp(&nsfh, 1, &vp, NULL, cred, slp, nam,
&rdonly, (nfsd->nd_flag & ND_KERBAUTH), false);
if (error) {
nfsm_reply(NFSX_UNSIGNED);
@@ -3483,7 +3490,7 @@ nfsrv_pathconf(struct nfsrv_descript *nf
u_quad_t frev;
=20
nfsm_srvmtofh(&nsfh);
- error =3D nfsrv_fhtovp(&nsfh, 1, &vp, cred, slp, nam,
+ error =3D nfsrv_fhtovp(&nsfh, 1, &vp, NULL, cred, slp, nam,
&rdonly, (nfsd->nd_flag & ND_KERBAUTH), false);
if (error) {
nfsm_reply(NFSX_UNSIGNED);
diff -r 2db9820dc602 -r 3c615e360a5e sys/nfs/nfs_srvsubs.c
--- a/sys/nfs/nfs_srvsubs.c Wed Aug 26 03:28:28 2026 +0000
+++ b/sys/nfs/nfs_srvsubs.c Mon Aug 24 19:50:51 2026 +0000
@@ -116,7 +116,7 @@
* it is not.
*/
int
-nfs_namei(struct nameidata *ndp, nfsrvfh_t *nsfh, uint32_t len, struct nfs=
svc_sock *slp, struct mbuf *nam, struct mbuf **mdp, char **dposp, struct vn=
ode **retdirp, int *dirattr_retp, struct vattr *dirattrp, struct lwp *l, in=
t kerbflag, int pubflag)
+nfs_namei(struct nameidata *ndp, nfsrvfh_t *nsfh, fsid_t *fsidp, uint32_t =
len, struct nfssvc_sock *slp, struct mbuf *nam, struct mbuf **mdp, char **d=
posp, struct vnode **retdirp, int *dirattr_retp, struct vattr *dirattrp, st=
ruct lwp *l, int kerbflag, int pubflag)
{
int i, rem;
struct mbuf *md;
@@ -174,7 +174,7 @@ nfs_namei(struct nameidata *ndp, nfsrvfh
/*
* Extract and set starting directory.
*/
- error =3D nfsrv_fhtovp(nsfh, false, &dp, ndp->ni_cnd.cn_cred, slp,
+ error =3D nfsrv_fhtovp(nsfh, false, &dp, fsidp, ndp->ni_cnd.cn_cred, slp,
nam, &rdonly, kerbflag, pubflag);
if (error)
goto out;
@@ -292,7 +292,7 @@ out:
* - if not lockflag unlock it with VOP_UNLOCK()
*/
int
-nfsrv_fhtovp(nfsrvfh_t *nsfh, int lockflag, struct vnode **vpp,
+nfsrv_fhtovp(nfsrvfh_t *nsfh, int lockflag, struct vnode **vpp, fsid_t *fs=
idp,
kauth_cred_t cred, struct nfssvc_sock *slp, struct mbuf *nam, int *rdo=
nlyp,
int kerbflag, int pubflag)
{
@@ -363,6 +363,8 @@ nfsrv_fhtovp(nfsrvfh_t *nsfh, int lockfl
*rdonlyp =3D 0;
if (!lockflag)
VOP_UNLOCK(*vpp);
+ if (fsidp)
+ *fsidp =3D fhp->fh_fsid;
return (0);
}
=20
@@ -389,7 +391,8 @@ nfs_ispublicfh(const nfsrvfh_t *nsfh)
}
=20
int
-nfsrv_composefh(struct vnode *vp, nfsrvfh_t *nsfh, bool v3)
+nfsrv_composefh(struct vnode *vp, nfsrvfh_t *nsfh,
+ const fsid_t fsidp[static 1], bool v3)
{
int error;
size_t fhsize;
@@ -402,6 +405,7 @@ nfsrv_composefh(struct vnode *vp, nfsrvf
if (error !=3D 0) {
return error;
}
+ NFSRVFH_FHANDLE(nsfh)->fh_fsid =3D *fsidp;
if (!v3 && fhsize < NFSX_V2FH) {
memset((char *)NFSRVFH_DATA(nsfh) + fhsize, 0,
NFSX_V2FH - fhsize);
diff -r 2db9820dc602 -r 3c615e360a5e sys/nfs/nfs_syscalls.c
--- a/sys/nfs/nfs_syscalls.c Wed Aug 26 03:28:28 2026 +0000
+++ b/sys/nfs/nfs_syscalls.c Mon Aug 24 19:50:51 2026 +0000
@@ -119,6 +119,9 @@ static void nfsd_rt(int, struct nfsrv_de
static int nfssvc_nfsd(struct nfssvc_copy_ops *, struct nfsd_srvargs *, vo=
id *,
struct lwp *);
=20
+static int nfssvc_replaceexportsext(const struct nfssvc_copy_ops *,
+ struct lwp *, const struct mountd_exports_ext *);
+
static int nfsd_compare_nodes(void *, const void *, const void *);
static int nfsd_compare_key(void *, const void *, const void *);
=20
@@ -353,6 +356,14 @@ do_nfssvc(struct nfssvc_copy_ops *ops, s
}
error =3D nfssvc_addsock(fp, nam);
fd_putfile(nfsdarg.sock);
+ } else if ((flag & (NFSSVC_SETEXPORTSLIST | NFSSVC_REPLACEEXPORTSLIST))
+ =3D=3D NFSSVC_REPLACEEXPORTSEXT) {
+ struct mountd_exports_ext mext;
+
+ error =3D copyin(argp, &mext, sizeof(mext));
+ if (error !=3D 0)
+ return error;
+ return nfssvc_replaceexportsext(ops, l, &mext);
} else if (flag & (NFSSVC_SETEXPORTSLIST | NFSSVC_REPLACEEXPORTSLIST)) {
struct export_args *args;
struct mountd_exports_list mel;
@@ -479,6 +490,191 @@ do_nfssvc(struct nfssvc_copy_ops *ops, s
return (error);
}
=20
+static int
+nfssvc_replaceexportsext(const struct nfssvc_copy_ops *ops, struct lwp *l,
+ const struct mountd_exports_ext *mext)
+{
+ const struct mountd_export_entry *user_entries;
+ const char *user_path;
+ size_t nbytes;
+ struct mountd_export_entry *ment0 =3D NULL, *ment;
+ struct export_args *args =3D NULL;
+ size_t nexports =3D 0;
+ const fsid_t *int_fsidp =3D NULL;
+ const fsid_t *ext_fsidp =3D NULL;
+ int error;
+
+ /*
+ * The buffer of export command entries should be the same for
+ * all platforms (hence no need for compat32 logic) and small
+ * enough to be reasonable to copy into a kernel buffer,
+ * guaranteed by the sizes verified at compile-time.
+ */
+ CTASSERT(sizeof(*mext) =3D=3D 24); /* 32/64-bit compatible */
+ CTASSERT(__type_max(typeof(mext->mext_entriesbytes)) <=3D 65535);
+
+ /*
+ * If the user's buffer isn't large enough to have a single
+ * entry in it, fail with EINVAL before we even bother copying
+ * in the entries.
+ */
+ nbytes =3D mext->mext_entriesbytes;
+ if (nbytes < sizeof(ment[0])) {
+ error =3D EINVAL;
+ goto out;
+ }
+
+ /*
+ * If the user's pointers are out-of-range, fail with EFAULT
+ * before we even bother copying in the entries.
+ *
+ * This isn't necessary for LP64 platforms, but for LP32 ones,
+ * it forestalls questions about integer truncation in
+ * conversion from uint64_t to void * later on -- if the high
+ * bits aren't all zero, we won't even reach the conversion.
+ *
+ * (We could check vm_map_max(&l->l_proc->p_vmspace->vm_map),
+ * but there's no additional benefit to that -- this just
+ * pacifies concerns by the compiler or other readers about
+ * integer->pointer truncation.)
+ */
+ if (mext->mext_entriesptr >=3D VM_MAXUSER_ADDRESS ||
+ mext->mext_pathptr >=3D VM_MAXUSER_ADDRESS) {
+ error =3D EFAULT;
+ goto out;
+ }
+ user_entries =3D (const void *)(uintptr_t)mext->mext_entriesptr;
+ user_path =3D (const void *)(uintptr_t)mext->mext_pathptr;
+
+ /*
+ * Allocate a temporary kernel buffer and copy the user's
+ * buffer into it.
+ *
+ * The size is CTASSERTed above to be small enough, <=3D64K, to
+ * be safe to allocate a kernel buffer.
+ */
+ ment0 =3D malloc(nbytes, M_TEMP, M_WAITOK);
+ error =3D copyin(user_entries, ment0, nbytes);
+ if (error)
+ goto out;
+
+ /*
+ * Iterate over the entries. Entries are length-encoded, so we
+ * have to be careful to make sure we don't overrun the buffer.
+ */
+ for (ment =3D ment0;
+ (char *)ment - (char *)ment0 < nbytes - sizeof(ment[0]);
+ ment =3D MOUNTD_EXPORT_ENTRYNEXT(ment)) {
+
+ /*
+ * If the encoded length goes beyond the end of our
+ * buffer, reject the input with EINVAL.
+ */
+ if (ment->ment_len > nbytes -
+ ((char *)ment->ment_data - (char *)ment0)) {
+ error =3D EINVAL;
+ goto out;
+ }
+ KASSERT((char *)&ment->ment_data[ment->ment_len] <=3D
+ (char *)ment0 + nbytes);
+
+ /*
+ * Dispatch on the export command.
+ */
+ switch (ment->ment_cmd) {
+ case MOUNTD_EXPORT_ARGS: {
+ struct mountd_export_entry_args *entry;
+ const struct export_args *user_exports;
+
+ CTASSERT(sizeof(*entry) =3D=3D 16);
+ if (ment->ment_len !=3D sizeof(*entry)) {
+ error =3D EINVAL;
+ goto out;
+ }
+ entry =3D (void *)ment->ment_data;
+
+ /*
+ * Make sure the number of exports per mount is
+ * small enough we can safely copy it into a
+ * kernel buffer.
+ */
+ if (entry->mea_nexports >
+ atomic_load_relaxed(&nfsd_maxexportspermount)) {
+ error =3D E2BIG;
+ goto out;
+ }
+
+ /*
+ * Verify the user's address is reasonable for
+ * this platform before we potentially truncate
+ * it in integer to pointer conversion.
+ */
+ if (entry->mea_uptr >=3D VM_MAXUSER_ADDRESS) {
+ error =3D EFAULT;
+ goto out;
+ }
+ user_exports =3D
+ (const void *)(uintptr_t)entry->mea_uptr;
+ nexports =3D entry->mea_nexports;
+
+ /*
+ * Allocate a temporary kernel buffer for the
+ * user's exports and copy them in.
+ */
+ args =3D malloc(nexports * sizeof(args[0]),
+ M_TEMP, M_WAITOK);
+ error =3D ops->exp_in(args, user_exports, nexports);
+ if (error)
+ goto out;
+ break;
+ }
+ case MOUNTD_EXPORT_SET_FSID: {
+ struct mountd_export_entry_set_fsid *entry;
+
+ CTASSERT(sizeof(*entry) =3D=3D 16);
+ if (ment->ment_len !=3D sizeof(*entry)) {
+ error =3D EINVAL;
+ goto out;
+ }
+ entry =3D (struct mountd_export_entry_set_fsid *)
+ ment->ment_data;
+
+ /*
+ * Refuse multiple fsid commands with EINVAL.
+ */
+ if (int_fsidp !=3D NULL || ext_fsidp !=3D NULL) {
+ error =3D EINVAL;
+ goto out;
+ }
+
+ /*
+ * Take the specified internal (expected local)
+ * and external (exported) fsids directly from
+ * the buffer, no copyin needed.
+ */
+ int_fsidp =3D &entry->mesf_int_fsid;
+ ext_fsidp =3D &entry->mesf_ext_fsid;
+ break;
+ }
+ default:
+ error =3D EOPNOTSUPP;
+ goto out;
+ }
+ }
+
+ /*
+ * We have copied in everything except for the user's path.
+ * Configure the mount point's exports.
+ */
+ error =3D mountd_set_exports_ext(user_path, l, /*nmp*/NULL, args,
+ nexports, int_fsidp, ext_fsidp, NFSSVC_REPLACEEXPORTSLIST);
+
+out: if (args)
+ free(args, M_TEMP);
+ free(ment0, M_TEMP);
+ return error;
+}
+
static struct nfssvc_sock *
nfsrv_sockalloc(void)
{
diff -r 2db9820dc602 -r 3c615e360a5e sys/nfs/nfs_var.h
--- a/sys/nfs/nfs_var.h Wed Aug 26 03:28:28 2026 +0000
+++ b/sys/nfs/nfs_var.h Mon Aug 24 19:50:51 2026 +0000
@@ -273,16 +273,16 @@ int nfs_getattrcache(struct vnode *, str
void nfs_delayedtruncate(struct vnode *);
int nfs_check_wccdata(struct nfsnode *, const struct timespec *,
struct timespec *, bool);
-int nfs_namei(struct nameidata *, nfsrvfh_t *, uint32_t, struct nfssvc_soc=
k *,
- struct mbuf *, struct mbuf **, char **, struct vnode **,
- int *, struct vattr *, struct lwp *, int, int);
+int nfs_namei(struct nameidata *, nfsrvfh_t *, fsid_t *, uint32_t,
+ struct nfssvc_sock *, struct mbuf *, struct mbuf **, char **,
+ struct vnode **, int *, struct vattr *, struct lwp *, int, int);
void nfs_zeropad(struct mbuf *, int, int);
void nfsm_srvwcc(struct nfsrv_descript *, int, struct vattr *, int,
struct vattr *, struct mbuf **, char **);
void nfsm_srvpostopattr(struct nfsrv_descript *, int, struct vattr *,
struct mbuf **, char **);
void nfsm_srvfattr(struct nfsrv_descript *, struct vattr *, struct nfs_fat=
tr *);
-int nfsrv_fhtovp(nfsrvfh_t *, int, struct vnode **, kauth_cred_t,
+int nfsrv_fhtovp(nfsrvfh_t *, int, struct vnode **, fsid_t *, kauth_cred_t,
struct nfssvc_sock *, struct mbuf *, int *, int, int);
int nfs_ispublicfh(const nfsrvfh_t *);
int netaddr_match(int, union nethostaddr *, struct mbuf *);
@@ -306,7 +306,7 @@ void nfs_cookieheuristic(struct vnode *,
u_int32_t nfs_getxid(void);
void nfs_renewxid(struct nfsreq *);
=20
-int nfsrv_composefh(struct vnode *, nfsrvfh_t *, bool);
+int nfsrv_composefh(struct vnode *, nfsrvfh_t *, const fsid_t[static 1], b=
ool);
int nfsrv_comparefh(const nfsrvfh_t *, const nfsrvfh_t *);
void nfsrv_copyfh(nfsrvfh_t *, const nfsrvfh_t *);
=20
@@ -361,6 +361,8 @@ int do_nfssvc(struct nfssvc_copy_ops *,=20
extern struct nfs_public nfs_pub;
int mountd_set_exports_list(const struct mountd_exports_list *, struct lwp=
*,
struct mount *, int);
+int mountd_set_exports_ext(const char *, struct lwp *, struct mount *,
+ const struct export_args *, size_t, const fsid_t *, const fsid_t *, in=
t);
int netexport_check(const fsid_t *, struct mbuf *, struct mount **, int *,
kauth_cred_t *);
void netexport_rdlock(void);
diff -r 2db9820dc602 -r 3c615e360a5e usr.sbin/mountd/exports.5
--- a/usr.sbin/mountd/exports.5 Wed Aug 26 03:28:28 2026 +0000
+++ b/usr.sbin/mountd/exports.5 Mon Aug 24 19:50:51 2026 +0000
@@ -293,7 +293,8 @@ WebNFS enables any client to get filehan
Using IP spoofing, a client could then pretend to be a host to which
the same filesystem was exported read/write, and use the handle to
gain access to that filesystem.
-.Bl -tag -width Fl
+.Pp
+.Bl -tag -width Fl -compact
.Sm off
.It Fl network Li =3D Ar netname Op Li / Ar prefixlength
.Sm on
@@ -326,6 +327,7 @@ is used to specify
on
.Ql ne2
interface.
+.Pp
.Sm off
.It Fl mask No =3D Ar netmask
.Sm on
@@ -334,6 +336,41 @@ Netmask for
.Fl network
options with no
.Ar prefixlength .
+.Pp
+.Sm off
+.It Fl fsid No =3D 0x Ar extfsid0 No . 0x Ar extfsid1
+.It Fl fsid No =3D 0x Ar intfsid0 No . 0x Ar intfsid1 No : 0x Ar extfsid0 =
No . 0x Ar extfsid1
+.Sm on
+Set the external fsid in file handles exposed over the network.
+The fsid components \(em
+.Ar intfsid0 , Ar intfsid1 , Ar extfsid0 ,
+and
+.Ar extfsid1
+\(em are 32-bit unsigned hexadecimal integers, and correspond to the
+elements of the
+.Vt fsid_t
+structure
+.Pq Xr statvfs 5 .
+.Pp
+In the first form, simply use
+.Ar extfsid0 No / Ar extfsid1
+as the fsid in file handles exposed over the network.
+.Pp
+In the second form,
+.Xr mountd 8
+will verify that the internal file system's fsid matches
+.Ar intfsid0 No / Ar intfsid1 ,
+and only if it matches, export the file system with
+.Ar extfsid0 No / Ar extfsid1 .
+If it does not match,
+.Xr mountd 8
+will fail noisily.
+This way, you can ensure that the wrong file system is not
+inadvertently exported.
+.Pp
+If the same mount point is exported multiple times, or multiple
+.Fl fsid
+options are specified, they must all match for the mount point.
.El
.Sh FILES
.Bl -tag -width Pa -compact
diff -r 2db9820dc602 -r 3c615e360a5e usr.sbin/mountd/mountd.c
--- a/usr.sbin/mountd/mountd.c Wed Aug 26 03:28:28 2026 +0000
+++ b/usr.sbin/mountd/mountd.c Mon Aug 24 19:50:51 2026 +0000
@@ -81,6 +81,7 @@ static char sccsid[] =3D "@(#)mountd.c
#include <pwd.h>
#include <netgroup.h>
#include <signal.h>
+#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -137,11 +138,13 @@ struct exportlist {
struct dirlist *ex_defdir;
int ex_flag;
fsid_t ex_fs;
+ fsid_t ex_ext_fs;
char *ex_fsdir;
char *ex_indexfile;
};
/* ex_flag bits */
#define EX_LINKED 0x1
+#define EX_SET_FSID 0x2
=20
union grouptypes {
struct addrinfo *gt_addrinfo;
@@ -210,6 +213,8 @@ static void hang_dirp(struct dirlist *,=20
static void mntsrv(struct svc_req *, SVCXPRT *);
static void nextfield(char **, char **);
static void parsecred(char *, struct uucred *);
+static int parse_fsid(const char *, fsid_t[static 1], fsid_t[static 1],
+ bool[static 1]);
static int put_exlist(struct dirlist *, XDR *, struct dirlist *, int *);
static int scan_tree(struct dirlist *, struct sockaddr *);
__dead static void send_umntall(int);
@@ -235,7 +240,16 @@ static struct uucred def_anon =3D {
0,
{ 0 }
};
-static struct mountd_exports_list *mel_tab;
+
+struct mountd_exports {
+ const char *mel_path;
+ size_t mel_nexports;
+ struct export_args *mel_exports;
+ fsid_t mel_int_fsid;
+ fsid_t *mel_ext_fsid;
+};
+
+static struct mountd_exports *mel_tab;
static int mel_tab_len;
=20
int opt_flags;
@@ -1214,8 +1228,8 @@ nextline:
static int
mel_compare(const void *a, const void *b)
{
- const struct mountd_exports_list *mela =3D a;
- const struct mountd_exports_list *melb =3D b;
+ const struct mountd_exports *mela =3D a;
+ const struct mountd_exports *melb =3D b;
=20
return strcmp(mela->mel_path, melb->mel_path);
}
@@ -1258,11 +1272,17 @@ get_exportlist(int n)
* file systems.
*/
mel_tab_len =3D getmntinfo(&fsp, MNT_NOWAIT);
+ if (mel_tab_len =3D=3D -1) {
+ warn("getmntinfo");
+ return;
+ }
mel_tab =3D ecalloc(mel_tab_len, sizeof(*mel_tab));
for (i =3D 0; i < mel_tab_len; i++) {
mel_tab[i].mel_path =3D estrdup(fsp[i].f_mntonname);
mel_tab[i].mel_nexports =3D 0;
mel_tab[i].mel_exports =3D NULL;
+ mel_tab[i].mel_int_fsid =3D fsp->f_fsidx;
+ mel_tab[i].mel_ext_fsid =3D NULL;
}
qsort(mel_tab, mel_tab_len, sizeof(mel_tab[0]), mel_compare);
=20
@@ -1288,9 +1308,45 @@ get_exportlist(int n)
}
=20
for (i =3D 0; i < mel_tab_len; i++) {
- struct mountd_exports_list *mel =3D &mel_tab[i];
+ struct mountd_exports *mel =3D &mel_tab[i];
+ struct mountd_exports_ext mext;
+ size_t mentbytes;
+ struct mountd_export_entry_args *args_entry;
+ struct mountd_export_entry_set_fsid *set_fsid;
+ union {
+ struct mountd_export_entry ment;
+ char buf[MOUNTD_EXPORT_ENTRYSIZE(sizeof(*args_entry)) +
+ MOUNTD_EXPORT_ENTRYSIZE(sizeof(*set_fsid))];
+ } u;
+ struct mountd_export_entry *ment;
+
+ mentbytes =3D 0;
=20
- if (nfssvc(NFSSVC_REPLACEEXPORTSLIST, mel) =3D=3D -1 &&
+ ment =3D &u.ment;
+ ment->ment_cmd =3D MOUNTD_EXPORT_ARGS;
+ ment->ment_len =3D sizeof(*args_entry);
+ args_entry =3D (void *)ment->ment_data;
+ args_entry->mea_uptr =3D (uintptr_t)(void *)mel->mel_exports;
+ args_entry->mea_nexports =3D mel->mel_nexports;
+ mentbytes +=3D MOUNTD_EXPORT_ENTRYSIZE(sizeof(*args_entry));
+
+ if (mel->mel_ext_fsid) {
+ ment =3D MOUNTD_EXPORT_ENTRYNEXT(ment);
+ ment->ment_cmd =3D MOUNTD_EXPORT_SET_FSID;
+ ment->ment_len =3D sizeof(*set_fsid);
+ set_fsid =3D (void *)ment->ment_data;
+ set_fsid->mesf_int_fsid =3D mel->mel_int_fsid;
+ set_fsid->mesf_ext_fsid =3D *mel->mel_ext_fsid;
+ mentbytes +=3D
+ MOUNTD_EXPORT_ENTRYSIZE(sizeof(*set_fsid));
+ }
+
+ memset(&mext, 0, sizeof(mext));
+ mext.mext_pathptr =3D (uintptr_t)(const void *)mel->mel_path;
+ mext.mext_entriesptr =3D (uintptr_t)(const void *)&u.ment;
+ mext.mext_entriesbytes =3D mentbytes;
+
+ if (nfssvc(NFSSVC_REPLACEEXPORTSEXT, &mext) =3D=3D -1 &&
(mel->mel_nexports > 0 || errno !=3D EOPNOTSUPP))
syslog(LOG_ERR, "Can't update exports for %s (%m)",
mel_tab[i].mel_path);
@@ -1838,6 +1894,65 @@ do_opt(const char *line, size_t lineno,=20
opt_flags |=3D (OP_MAPALL | OP_NORESPORT);
} else if (cpoptarg && !strcmp(cpopt, "index")) {
ep->ex_indexfile =3D strdup(cpoptarg);
+ } else if (cpoptarg && !strcmp(cpopt, "fsid")) {
+ fsid_t int_fsid, ext_fsid;
+ bool check;
+
+ if (parse_fsid(cpoptarg, &int_fsid, &ext_fsid,
+ &check)) {
+ syslog(LOG_ERR,
+ "\"%s\", line %ld: Bad fsid opt %s",
+ line, (unsigned long)lineno, cpoptarg);
+ return (1);
+ }
+
+ if (mountd_debug) {
+ if (check) {
+ fprintf(stderr, "expect fsid"
+ " {0x%"PRIx32",0x%"PRIx32"}\n",
+ int_fsid.__fsid_val[0],
+ int_fsid.__fsid_val[1]);
+ fprintf(stderr, "actual fsid"
+ " {0x%"PRIx32",0x%"PRIx32"}\n",
+ ep->ex_fs.__fsid_val[0],
+ ep->ex_fs.__fsid_val[1]);
+ }
+ fprintf(stderr, "export with fsid"
+ " {0x%"PRIx32",0x%"PRIx32"}\n",
+ ext_fsid.__fsid_val[0],
+ ext_fsid.__fsid_val[1]);
+ }
+
+ if (check) {
+ if ((int_fsid.__fsid_val[0] !=3D
+ ep->ex_fs.__fsid_val[0]) ||
+ (int_fsid.__fsid_val[1] !=3D
+ ep->ex_fs.__fsid_val[1])) {
+ syslog(LOG_ERR, "\"%s\", line %ld:"
+ " Wrong internal fsid: internal"
+ " 0x%"PRIx32",0x%"PRIx32" !=3D"
+ " expected 0x%"PRIx32",0x%"PRIx32,
+ line, (unsigned long)lineno,
+ ep->ex_fs.__fsid_val[0],
+ ep->ex_fs.__fsid_val[1],
+ int_fsid.__fsid_val[0],
+ int_fsid.__fsid_val[1]);
+ return (1);
+ }
+ }
+ if (ep->ex_flag & EX_SET_FSID) {
+ if ((ext_fsid.__fsid_val[0] !=3D
+ ep->ex_fs.__fsid_val[0]) ||
+ (ext_fsid.__fsid_val[1] !=3D
+ ep->ex_fs.__fsid_val[1])) {
+ syslog(LOG_ERR, "\"%s\", line %ld:"
+ " Duplicate external fsid",
+ line, (unsigned long)lineno);
+ return (1);
+ }
+ }
+ ep->ex_flag |=3D EX_SET_FSID;
+ ep->ex_ext_fs =3D ext_fsid;
} else {
syslog(LOG_ERR,=20
"\"%s\", line %ld: Bad opt %s",
@@ -1953,10 +2068,10 @@ get_ht(void)
static int
add_export_arg(const char *path, int exflags, struct uucred *anoncrp,
struct sockaddr *addrp, int addrlen, struct sockaddr *maskp, int maskl=
en,
- char *indexfile)
+ struct exportlist *ep)
{
- const struct mountd_exports_list mel_key =3D { .mel_path =3D path };
- struct mountd_exports_list *mel;
+ const struct mountd_exports mel_key =3D { .mel_path =3D path };
+ struct mountd_exports *mel;
struct export_args *export;
=20
if (addrp !=3D NULL && addrp->sa_family =3D=3D AF_INET6 && have_v6 =3D=3D=
0)
@@ -1969,6 +2084,19 @@ add_export_arg(const char *path, int exf
path);
return 1;
}
+ if (ep->ex_flag & EX_SET_FSID) {
+ if (mel->mel_ext_fsid) {
+ if ((ep->ex_ext_fs.__fsid_val[0] !=3D
+ mel->mel_ext_fsid->__fsid_val[0]) ||
+ (ep->ex_ext_fs.__fsid_val[1] !=3D
+ mel->mel_ext_fsid->__fsid_val[1])) {
+ syslog(LOG_ERR, "Conflicting external fsids"
+ " for %s", path);
+ return 1;
+ }
+ }
+ mel->mel_ext_fsid =3D &ep->ex_ext_fs;
+ }
ereallocarr(&mel->mel_exports, mel->mel_nexports + 1,
sizeof(*mel->mel_exports));
export =3D &mel->mel_exports[mel->mel_nexports++];
@@ -1976,8 +2104,8 @@ add_export_arg(const char *path, int exf
=20
export->ex_flags =3D exflags;
export->ex_anon =3D *anoncrp;
- if (indexfile)
- export->ex_indexfile =3D estrdup(indexfile);
+ if (ep->ex_indexfile)
+ export->ex_indexfile =3D estrdup(ep->ex_indexfile);
if (addrlen > 0) {
export->ex_addr =3D emalloc(addrlen);
export->ex_addrlen =3D addrlen;
@@ -2010,7 +2138,7 @@ do_nfssvc(const char *line, size_t linen
addrp =3D ai->ai_addr;
addrlen =3D ai->ai_addrlen;
if (add_export_arg(fsb->f_mntonname, exflags, anoncrp,
- addrp, addrlen, NULL, 0, ep->ex_indexfile) !=3D 0)
+ addrp, addrlen, NULL, 0, ep) !=3D 0)
return 1;
}
} else if (grp->gr_type =3D=3D GT_NET) {
@@ -2026,7 +2154,7 @@ do_nfssvc(const char *line, size_t linen
}
if (add_export_arg(fsb->f_mntonname, exflags, anoncrp,
addrp, addrlen, (struct sockaddr *)&ss, ss.ss_len,
- ep->ex_indexfile) !=3D 0)
+ ep) !=3D 0)
return 1;
} else {
syslog(LOG_ERR, "\"%s\", line %ld: Bad netgroup type",
@@ -2135,6 +2263,90 @@ parsecred(char *namelist, struct uucred=20
syslog(LOG_ERR, "Too many groups");
}
=20
+/*
+ * parse_fsid(fsidstr, &int_fsid, &ext_fsid, &check)
+ *
+ * Parse an fsid setting. One of:
+ *
+ * 0x<hex32>.0x<hex32>
+ * 0x<hex32>.0x<hex32>:0x<hex32>.0x<hex32>
+ *
+ * In the first case, set check =3D false; in the second case, set
+ * check =3D true.
+ *
+ * Return zero on success, nonzero on failure.
+ */
+static int
+parse_fsid(const char *fsidstr,
+ fsid_t int_fsid[static 1], fsid_t ext_fsid[static 1],
+ bool check[static 1])
+{
+ unsigned long x;
+ fsid_t fsid0, fsid1;
+ char *endptr;
+
+ memset(&fsid0, 0, sizeof(fsid0));
+ memset(&fsid1, 0, sizeof(fsid1));
+
+ if (strncmp(fsidstr, "0x", 2) !=3D 0)
+ return 1;
+ fsidstr +=3D 2;
+
+ errno =3D 0;
+ x =3D strtoul(fsidstr, &endptr, 0x10);
+ if (endptr =3D=3D fsidstr || endptr[0] !=3D '.' || errno || x > UINT32_MA=
X)
+ return 1;
+ fsid0.__fsid_val[0] =3D (int32_t)x;
+ fsidstr =3D endptr;
+
+ if (strncmp(fsidstr, ".0x", 3) !=3D 0)
+ return 1;
+ fsidstr +=3D 3;
+
+ errno =3D 0;
+ x =3D strtoul(fsidstr, &endptr, 0x10);
+ if (endptr =3D=3D fsidstr ||
+ (endptr[0] !=3D '\0' && endptr[0] !=3D ':') ||
+ errno ||
+ x > UINT32_MAX)
+ return 1;
+ fsid0.__fsid_val[1] =3D (int32_t)x;
+ fsidstr =3D endptr;
+
+ if (fsidstr[0] =3D=3D '\0') {
+ *ext_fsid =3D fsid0;
+ *check =3D false;
+ return 0;
+ }
+
+ if (strncmp(fsidstr, ":0x", 3) !=3D 0)
+ return 1;
+ fsidstr +=3D 3;
+
+ errno =3D 0;
+ x =3D strtoul(fsidstr, &endptr, 0x10);
+ if (endptr =3D=3D fsidstr || endptr[0] !=3D '.' || errno || x > UINT32_MA=
X)
+ return 1;
+ fsid1.__fsid_val[0] =3D (int32_t)x;
+ fsidstr =3D endptr;
+
+ if (strncmp(fsidstr, ".0x", 3) !=3D 0)
+ return 1;
+ fsidstr++;
+
+ errno =3D 0;
+ x =3D strtoul(fsidstr, &endptr, 0x10);
+ if (endptr =3D=3D fsidstr || endptr[0] !=3D '\0' || errno || x > UINT32_M=
AX)
+ return 1;
+ fsid1.__fsid_val[1] =3D (int32_t)x;
+ fsidstr =3D endptr;
+
+ *int_fsid =3D fsid0;
+ *ext_fsid =3D fsid1;
+ *check =3D true;
+ return 0;
+}
+
#define STRSIZ (RPCMNT_NAMELEN+RPCMNT_PATHLEN+50)
/*
* Routines that maintain the remote mounttab
--=_doTlMKKkYbLBtme6fab/51tYZGWhbizH--
(Contact us)
$NetBSD: query-full-pr,v 1.51 2026/08/10 02:28:17 riastradh Exp $
$NetBSD: gnats_config.sh,v 1.10 2026/05/13 22:00:09 riastradh Exp $
Copyright © 1994-2026
The NetBSD Foundation, Inc. ALL RIGHTS RESERVED.