git: 047562735bc9 - main - fdescfs: Pass up additional metadata during lookups
- Go to: [ bottom of page ] [ top of archives ] [ this month ]
Date: Tue, 29 Sep 2026 15:55:48 UTC
The branch main has been updated by markj:
URL: https://cgit.FreeBSD.org/src/commit/?id=047562735bc9bc4587519e2287106a3294ce6505
commit 047562735bc9bc4587519e2287106a3294ce6505
Author: Mark Johnston <markj@FreeBSD.org>
AuthorDate: 2026-09-28 14:44:04 +0000
Commit: Mark Johnston <markj@FreeBSD.org>
CommitDate: 2026-09-29 15:54:16 +0000
fdescfs: Pass up additional metadata during lookups
When an fdescfs mount has the nodup option set, fdesc_lookup(/dev/fd/n)
returns the vnode referenced by file descriptor n, rather than returning
an fdescfs vnode. This meant that fd metadata attached to fd n was not
preserved when reopening the file, which is contrary to the expected
semantics for capsicum rights and the UF_RESOLVE_BENEATH fd flag. For
regular fdescfs mounts, this metadata is copied via dupfdopen().
Fix the problem by passing up this metadata through the nameidata
structure. Thus, if one opens /dev/fd/n, the returned fd will inherit
UF_RESOLVE_BENEATH and the capability rights of fd n. Add some
regression tests as well.
Approved by: so
Security: FreeBSD-SA-26:66.jail
Security: CVE-2026-101304
Reported by: Jan Bramkamp
Reviewed by: kib
Sponsored by: The FreeBSD Foundation
Differential Revision: https://reviews.freebsd.org/D59886
---
sys/fs/fdescfs/fdesc_vnops.c | 38 ++++++++-
tests/sys/fs/fdescfs/fdescfs_test.c | 165 ++++++++++++++++++++++++++++++++++++
2 files changed, 201 insertions(+), 2 deletions(-)
diff --git a/sys/fs/fdescfs/fdesc_vnops.c b/sys/fs/fdescfs/fdesc_vnops.c
index eb32750dda55..446936b449f7 100644
--- a/sys/fs/fdescfs/fdesc_vnops.c
+++ b/sys/fs/fdescfs/fdesc_vnops.c
@@ -289,11 +289,13 @@ fdesc_lookup(struct vop_lookup_args *ap)
char *pname = cnp->cn_nameptr;
struct thread *td = curthread;
struct file *fp;
+ struct filecaps fcaps;
struct fdesc_get_ino_args arg;
+ struct vnode *fvp;
int nlen = cnp->cn_namelen;
u_int fd, fd1;
int error;
- struct vnode *fvp;
+ uint8_t fflags;
bool traverse;
if ((cnp->cn_flags & ISLASTCN) &&
@@ -335,7 +337,8 @@ fdesc_lookup(struct vop_lookup_args *ap)
/*
* No rights to check since 'fp' isn't actually used.
*/
- if ((error = fget(td, fd, &cap_no_rights, &fp)) != 0)
+ if ((error = fget_cap(td, fd, &cap_no_rights, &fflags, &fp,
+ &fcaps)) != 0)
goto bad;
/* A component below /dev/fd/N resolves in the directory N names. */
@@ -344,6 +347,7 @@ fdesc_lookup(struct vop_lookup_args *ap)
(VFSTOFDESC(dvp->v_mount)->flags & FMNT_LINRDLNKF) != 0;
if (traverse && fp->f_type != DTYPE_VNODE) {
fdrop(fp, td);
+ filecaps_free(&fcaps);
error = ENOTDIR;
goto bad;
}
@@ -381,6 +385,36 @@ fdesc_lookup(struct vop_lookup_args *ap)
error = ENOENT;
}
+ /*
+ * Make sure that a nodup mount can't be used to launder away monotonic
+ * file descriptor metadata, namely UF_RESOLVE_BENEATH and capability
+ * rights.
+ */
+ if (error == 0 && fvp->v_mount != dvp->v_mount &&
+ ((fflags & UF_RESOLVE_BENEATH) != 0 || !filecaps_full(&fcaps))) {
+ struct nameidata *ndp;
+
+ ndp = vfs_lookup_nameidata(cnp);
+ if (ndp == NULL) {
+ vput(fvp);
+ error = ENOTCAPABLE;
+ } else {
+ if ((fflags & UF_RESOLVE_BENEATH) != 0)
+ ndp->ni_resflags |= NIRES_BENEATH;
+ if (!filecaps_full(&fcaps)) {
+ if (cap_rights_is_valid(
+ &ndp->ni_filecaps.fc_rights))
+ filecaps_intersect(&fcaps,
+ &ndp->ni_filecaps);
+ else
+ filecaps_move(&fcaps,
+ &ndp->ni_filecaps);
+ ndp->ni_resflags |= NIRES_STRICTREL;
+ }
+ }
+ }
+ filecaps_free(&fcaps);
+
if (error)
goto bad;
*vpp = fvp;
diff --git a/tests/sys/fs/fdescfs/fdescfs_test.c b/tests/sys/fs/fdescfs/fdescfs_test.c
index 1bec12c8e13b..ee1f85ee866d 100644
--- a/tests/sys/fs/fdescfs/fdescfs_test.c
+++ b/tests/sys/fs/fdescfs/fdescfs_test.c
@@ -1,4 +1,6 @@
#include <sys/param.h>
+#include <sys/capsicum.h>
+#include <sys/filio.h>
#include <sys/mount.h>
#include <sys/stat.h>
@@ -13,6 +15,7 @@
#include <unistd.h>
static const char *const linrdlnk[] = { "linrdlnk", NULL };
+static const char *const nodup[] = { "nodup", NULL };
static const char *const nodup_linrdlnk[] = { "nodup", "linrdlnk", NULL };
static void
@@ -194,6 +197,74 @@ check_traverse(int flags)
ATF_REQUIRE_EQ(0, unmount("mnt", 0));
}
+static void
+check_cap_rights(const char *const *opts)
+{
+ cap_rights_t expected, actual;
+ char path[64];
+ int copy, fd;
+
+ mount_fdescfs(opts);
+ fd = open("file", O_RDONLY | O_CREAT, 0644);
+ ATF_REQUIRE(fd >= 0);
+ cap_rights_init(&expected, CAP_READ, CAP_FSTAT);
+ ATF_REQUIRE_EQ(0, cap_rights_limit(fd, &expected));
+ fdpath(path, sizeof(path), fd, "");
+ copy = open(path, O_RDONLY);
+ ATF_REQUIRE(copy >= 0);
+ ATF_REQUIRE_EQ(0, cap_rights_get(copy, &actual));
+ ATF_CHECK(cap_rights_contains(&actual, &expected));
+ ATF_CHECK(cap_rights_contains(&expected, &actual));
+ ATF_REQUIRE_EQ(0, close(copy));
+ ATF_REQUIRE_EQ(0, close(fd));
+ ATF_REQUIRE_EQ(0, unmount("mnt", 0));
+}
+
+static void
+check_resolve_beneath(const char *const *opts, int oflags)
+{
+ char path[64];
+ int copy, dirfd, fd;
+
+ mount_fdescfs(opts);
+ ATF_REQUIRE_EQ(0, mkdir("dir", 0755));
+ dirfd = open("dir", O_RDONLY | O_DIRECTORY);
+ ATF_REQUIRE(dirfd >= 0);
+ ATF_REQUIRE_EQ(0, fcntl(dirfd, F_SETFD, FD_RESOLVE_BENEATH));
+ fdpath(path, sizeof(path), dirfd, "");
+ copy = open(path, oflags);
+ ATF_REQUIRE(copy >= 0);
+ fd = openat(copy, ".", O_RDONLY | O_DIRECTORY);
+ ATF_REQUIRE(fd >= 0);
+ ATF_REQUIRE_EQ(0, close(fd));
+ ATF_CHECK_ERRNO(ENOTCAPABLE,
+ openat(copy, "..", O_RDONLY | O_DIRECTORY) == -1);
+ ATF_REQUIRE_EQ(0, close(copy));
+ ATF_REQUIRE_EQ(0, close(dirfd));
+ ATF_REQUIRE_EQ(0, unmount("mnt", 0));
+}
+
+static void
+check_ioctl_caps(const char *const *opts)
+{
+ cap_ioctl_t cmds[] = { FIOCLEX };
+ char path[64];
+ int copy, fd;
+
+ mount_fdescfs(opts);
+ fd = open("file", O_RDONLY | O_CREAT, 0644);
+ ATF_REQUIRE(fd >= 0);
+ ATF_REQUIRE_EQ(0, cap_ioctls_limit(fd, cmds, nitems(cmds)));
+ fdpath(path, sizeof(path), fd, "");
+ copy = open(path, O_RDONLY);
+ ATF_REQUIRE(copy >= 0);
+ ATF_REQUIRE_EQ(0, ioctl(copy, FIOCLEX, 0));
+ ATF_CHECK_ERRNO(ENOTCAPABLE, ioctl(copy, FIONCLEX, 0) == -1);
+ ATF_REQUIRE_EQ(0, close(copy));
+ ATF_REQUIRE_EQ(0, close(fd));
+ ATF_REQUIRE_EQ(0, unmount("mnt", 0));
+}
+
#define FDESCFS_TC(name, description) \
ATF_TC_WITH_CLEANUP(name); \
ATF_TC_HEAD(name, tc) \
@@ -357,6 +428,93 @@ ATF_TC_BODY(root_reference, tc)
ATF_REQUIRE_EQ(0, unmount("mnt", 0));
}
+FDESCFS_TC(cap_rights, "fdescfs preserves capability rights");
+ATF_TC_BODY(cap_rights, tc)
+{
+ check_cap_rights(NULL);
+}
+
+FDESCFS_TC(nodup_cap_rights, "nodup mounts preserve capability rights");
+ATF_TC_BODY(nodup_cap_rights, tc)
+{
+ check_cap_rights(nodup);
+}
+
+FDESCFS_TC(resolve_beneath, "fdescfs preserves the FD_RESOLVE_BENEATH flag");
+ATF_TC_BODY(resolve_beneath, tc)
+{
+ check_resolve_beneath(NULL, O_RDONLY);
+}
+
+FDESCFS_TC(nodup_resolve_beneath,
+ "nodup mounts preserve the FD_RESOLVE_BENEATH flag");
+ATF_TC_BODY(nodup_resolve_beneath, tc)
+{
+ check_resolve_beneath(nodup, O_RDONLY | O_DIRECTORY);
+}
+
+FDESCFS_TC(ioctl_caps, "fdescfs preserves ioctl capability restrictions");
+ATF_TC_BODY(ioctl_caps, tc)
+{
+ check_ioctl_caps(NULL);
+}
+
+FDESCFS_TC(nodup_ioctl_caps,
+ "nodup mounts preserve ioctl capability restrictions");
+ATF_TC_BODY(nodup_ioctl_caps, tc)
+{
+ check_ioctl_caps(nodup);
+}
+
+FDESCFS_TC(nodup_ioctl_cap_intersection,
+ "nodup mounts intersect ioctl caps from source fd and dirfd");
+ATF_TC_BODY(nodup_ioctl_cap_intersection, tc)
+{
+ cap_ioctl_t fd_cmds[] = { FIOCLEX };
+ cap_ioctl_t both_cmds[] = { FIOCLEX, FIONCLEX };
+ cap_ioctl_t fionclex_cmds[] = { FIONCLEX };
+ char fd_path[16];
+ int copy, fd, len, mntfd;
+
+ mount_fdescfs(nodup);
+ fd = open("file", O_RDONLY | O_CREAT, 0644);
+ ATF_REQUIRE(fd >= 0);
+ ATF_REQUIRE_EQ(0, cap_ioctls_limit(fd, fd_cmds, nitems(fd_cmds)));
+ len = snprintf(fd_path, sizeof(fd_path), "%d", fd);
+ ATF_REQUIRE(len > 0 && (size_t)len < sizeof(fd_path));
+
+ /*
+ * Non-empty intersection: loop removes FIONCLEX, leaving {FIOCLEX}.
+ */
+ mntfd = open("mnt", O_RDONLY | O_DIRECTORY);
+ ATF_REQUIRE(mntfd >= 0);
+ ATF_REQUIRE_EQ(0, cap_ioctls_limit(mntfd, both_cmds, nitems(both_cmds)));
+ copy = openat(mntfd, fd_path, O_RDONLY);
+ ATF_REQUIRE(copy >= 0);
+ ATF_REQUIRE_EQ(0, ioctl(copy, FIOCLEX, 0));
+ ATF_CHECK_ERRNO(ENOTCAPABLE, ioctl(copy, FIONCLEX, 0) == -1);
+ ATF_REQUIRE_EQ(0, close(copy));
+ ATF_REQUIRE_EQ(0, close(mntfd));
+
+ /*
+ * Empty intersection: loop removes FIONCLEX from {FIONCLEX} since fd
+ * only allows {FIOCLEX}, leaving an empty list.
+ */
+ mntfd = open("mnt", O_RDONLY | O_DIRECTORY);
+ ATF_REQUIRE(mntfd >= 0);
+ ATF_REQUIRE_EQ(0,
+ cap_ioctls_limit(mntfd, fionclex_cmds, nitems(fionclex_cmds)));
+ copy = openat(mntfd, fd_path, O_RDONLY);
+ ATF_REQUIRE(copy >= 0);
+ ATF_CHECK_ERRNO(ENOTCAPABLE, ioctl(copy, FIOCLEX, 0) == -1);
+ ATF_CHECK_ERRNO(ENOTCAPABLE, ioctl(copy, FIONCLEX, 0) == -1);
+ ATF_REQUIRE_EQ(0, close(copy));
+ ATF_REQUIRE_EQ(0, close(mntfd));
+
+ ATF_REQUIRE_EQ(0, close(fd));
+ ATF_REQUIRE_EQ(0, unmount("mnt", 0));
+}
+
ATF_TP_ADD_TCS(tp)
{
ATF_TP_ADD_TC(tp, traverse_dir);
@@ -369,5 +527,12 @@ ATF_TP_ADD_TCS(tp)
ATF_TP_ADD_TC(tp, plain_mount);
ATF_TP_ADD_TC(tp, nodup_linrdlnk_mount);
ATF_TP_ADD_TC(tp, root_reference);
+ ATF_TP_ADD_TC(tp, cap_rights);
+ ATF_TP_ADD_TC(tp, nodup_cap_rights);
+ ATF_TP_ADD_TC(tp, resolve_beneath);
+ ATF_TP_ADD_TC(tp, nodup_resolve_beneath);
+ ATF_TP_ADD_TC(tp, ioctl_caps);
+ ATF_TP_ADD_TC(tp, nodup_ioctl_caps);
+ ATF_TP_ADD_TC(tp, nodup_ioctl_cap_intersection);
return (atf_no_error());
}