git: 9a6bccc57c68 - releng/14.4 - unlinkat: Pass flags down to the work functions

From: Mark Johnston <markj_at_FreeBSD.org>
Date: Tue, 30 Jun 2026 17:20:59 UTC
The branch releng/14.4 has been updated by markj:

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

commit 9a6bccc57c68b7bd6fed50720d7a4aa1a4ef9d6c
Author:     Mark Johnston <markj@FreeBSD.org>
AuthorDate: 2026-06-23 20:43:53 +0000
Commit:     Mark Johnston <markj@FreeBSD.org>
CommitDate: 2026-06-29 19:17:30 +0000

    unlinkat: Pass flags down to the work functions
    
    The unlinkat(2) and funlinkat(2) system calls were ignoring
    AT_RESOLVE_BENEATH.
    
    Also pass pathseg through instead of assuming it's UIO_USERSPACE.
    
    Add some tests which make sure that AT_RESOLVE_BENEATH is handled
    properly by various system calls.
    
    Approved by:    so
    Security:       FreeBSD-SA-26:42.unlinkat
    Security:       CVE-2026-49421
    Reported by:    Yuxiang Yang, Yizhou Zhao, Ao Wang, Xuewei Feng, Qi Li, and Ke Xu from Tsinghua University using GLM-5.1 from Z.ai
    Reviewed by:    kib
    Sponsored by:   The FreeBSD Foundation
    Differential Revision:  https://reviews.freebsd.org/D57790
---
 sys/kern/vfs_syscalls.c               |  11 +-
 tests/sys/kern/Makefile               |   1 +
 tests/sys/kern/resolve_beneath_test.c | 220 ++++++++++++++++++++++++++++++++++
 3 files changed, 227 insertions(+), 5 deletions(-)

diff --git a/sys/kern/vfs_syscalls.c b/sys/kern/vfs_syscalls.c
index 5c59d4dda2fd..de2261256875 100644
--- a/sys/kern/vfs_syscalls.c
+++ b/sys/kern/vfs_syscalls.c
@@ -1889,16 +1889,17 @@ sys_unlink(struct thread *td, struct unlink_args *uap)
 
 static int
 kern_funlinkat_ex(struct thread *td, int dfd, const char *path, int fd,
-    int flag, enum uio_seg pathseg, ino_t oldinum)
+    int flags, enum uio_seg pathseg, ino_t oldinum)
 {
 
-	if ((flag & ~(AT_REMOVEDIR | AT_RESOLVE_BENEATH)) != 0)
+	if ((flags & ~(AT_REMOVEDIR | AT_RESOLVE_BENEATH)) != 0)
 		return (EINVAL);
 
-	if ((flag & AT_REMOVEDIR) != 0)
-		return (kern_frmdirat(td, dfd, path, fd, UIO_USERSPACE, 0));
+	if ((flags & AT_REMOVEDIR) != 0)
+		return (kern_frmdirat(td, dfd, path, fd, pathseg,
+		    flags & ~AT_REMOVEDIR));
 
-	return (kern_funlinkat(td, dfd, path, fd, UIO_USERSPACE, 0, 0));
+	return (kern_funlinkat(td, dfd, path, fd, pathseg, flags, 0));
 }
 
 #ifndef _SYS_SYSPROTO_H_
diff --git a/tests/sys/kern/Makefile b/tests/sys/kern/Makefile
index 67cdf19f6d6e..1c41c62b43a5 100644
--- a/tests/sys/kern/Makefile
+++ b/tests/sys/kern/Makefile
@@ -32,6 +32,7 @@ ATF_TESTS_C+=	procdesc
 ATF_TESTS_C+=	ptrace_test
 TEST_METADATA.ptrace_test+=		timeout="15"
 ATF_TESTS_C+=	reaper
+ATF_TESTS_C+=	resolve_beneath_test
 ATF_TESTS_C+=	sched_affinity
 ATF_TESTS_C+=	sigaltstack
 ATF_TESTS_C+=	sigwait
diff --git a/tests/sys/kern/resolve_beneath_test.c b/tests/sys/kern/resolve_beneath_test.c
new file mode 100644
index 000000000000..b83926038ded
--- /dev/null
+++ b/tests/sys/kern/resolve_beneath_test.c
@@ -0,0 +1,220 @@
+/*
+ * Copyright (c) 2026 The FreeBSD Foundation
+ *
+ * This software was written by Mark Johnston under sponsorship from
+ * the FreeBSD Foundation.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <sys/stat.h>
+#include <sys/mount.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include <atf-c.h>
+
+/*
+ * Verify that AT_RESOLVE_BENEATH is respected by various system calls.
+ */
+
+static int
+setup(void)
+{
+	int fd, tfd;
+
+	ATF_REQUIRE_EQ(0, mkdir("base", 0755));
+	ATF_REQUIRE_EQ(0, mkdir("outside", 0755));
+	ATF_REQUIRE((tfd = open("outside/target", O_CREAT | O_WRONLY, 0644)) >=
+	    0);
+	ATF_REQUIRE(close(tfd) == 0);
+	ATF_REQUIRE_EQ(0, mkdir("outside/dir", 0755));
+	ATF_REQUIRE((tfd = open("base/file", O_CREAT | O_WRONLY, 0644)) >= 0);
+	ATF_REQUIRE(close(tfd) == 0);
+	ATF_REQUIRE_EQ(0, mkdir("base/subdir", 0755));
+	ATF_REQUIRE((fd = open("base", O_DIRECTORY | O_RDONLY)) >= 0);
+	return (fd);
+}
+
+ATF_TC_WITHOUT_HEAD(faccessat_beneath);
+ATF_TC_BODY(faccessat_beneath, tc)
+{
+	int fd;
+
+	fd = setup();
+	ATF_REQUIRE_EQ(0,
+	    faccessat(fd, "file", F_OK, AT_RESOLVE_BENEATH));
+	ATF_REQUIRE_ERRNO(ENOTCAPABLE,
+	    faccessat(fd, "../outside/target", F_OK,
+	    AT_RESOLVE_BENEATH) == -1);
+	ATF_REQUIRE(close(fd) == 0);
+}
+
+ATF_TC_WITHOUT_HEAD(chflagsat_beneath);
+ATF_TC_BODY(chflagsat_beneath, tc)
+{
+	int fd, ret;
+
+	fd = setup();
+	ret = chflagsat(fd, "file", UF_NODUMP, AT_RESOLVE_BENEATH);
+	if (ret != 0)
+		ATF_REQUIRE_EQ(EOPNOTSUPP, errno);
+	ATF_REQUIRE_ERRNO(ENOTCAPABLE,
+	    chflagsat(fd, "../outside/target", UF_NODUMP,
+	    AT_RESOLVE_BENEATH) == -1);
+	ATF_REQUIRE(close(fd) == 0);
+}
+
+ATF_TC_WITHOUT_HEAD(fchmodat_beneath);
+ATF_TC_BODY(fchmodat_beneath, tc)
+{
+	int fd;
+
+	fd = setup();
+	ATF_REQUIRE_EQ(0,
+	    fchmodat(fd, "file", 0644, AT_RESOLVE_BENEATH));
+	ATF_REQUIRE_ERRNO(ENOTCAPABLE,
+	    fchmodat(fd, "../outside/target", 0644,
+	    AT_RESOLVE_BENEATH) == -1);
+	ATF_REQUIRE(close(fd) == 0);
+}
+
+ATF_TC_WITHOUT_HEAD(fchownat_beneath);
+ATF_TC_BODY(fchownat_beneath, tc)
+{
+	int fd;
+
+	fd = setup();
+	ATF_REQUIRE_EQ(0,
+	    fchownat(fd, "file", -1, -1, AT_RESOLVE_BENEATH));
+	ATF_REQUIRE_ERRNO(ENOTCAPABLE,
+	    fchownat(fd, "../outside/target", 0, 0,
+	    AT_RESOLVE_BENEATH) == -1);
+	ATF_REQUIRE(close(fd) == 0);
+}
+
+ATF_TC_WITHOUT_HEAD(utimensat_beneath);
+ATF_TC_BODY(utimensat_beneath, tc)
+{
+	int fd;
+
+	fd = setup();
+	ATF_REQUIRE_EQ(0,
+	    utimensat(fd, "file", NULL, AT_RESOLVE_BENEATH));
+	ATF_REQUIRE_ERRNO(ENOTCAPABLE,
+	    utimensat(fd, "../outside/target", NULL,
+	    AT_RESOLVE_BENEATH) == -1);
+	ATF_REQUIRE(close(fd) == 0);
+}
+
+ATF_TC_WITHOUT_HEAD(linkat_beneath);
+ATF_TC_BODY(linkat_beneath, tc)
+{
+	int fd;
+
+	fd = setup();
+	ATF_REQUIRE_EQ(0,
+	    linkat(fd, "file", fd, "hardlink", AT_RESOLVE_BENEATH));
+	ATF_REQUIRE_ERRNO(ENOTCAPABLE,
+	    linkat(fd, "../outside/target", fd, "hardlink2",
+	    AT_RESOLVE_BENEATH) == -1);
+	ATF_REQUIRE(close(fd) == 0);
+}
+
+ATF_TC_WITHOUT_HEAD(unlinkat_beneath);
+ATF_TC_BODY(unlinkat_beneath, tc)
+{
+	int fd;
+
+	fd = setup();
+	ATF_REQUIRE_EQ(0,
+	    unlinkat(fd, "file", AT_RESOLVE_BENEATH));
+	ATF_REQUIRE_ERRNO(ENOTCAPABLE,
+	    unlinkat(fd, "../outside/target",
+	    AT_RESOLVE_BENEATH) == -1);
+	ATF_REQUIRE(close(fd) == 0);
+}
+
+ATF_TC_WITHOUT_HEAD(unlinkat_rmdir_beneath);
+ATF_TC_BODY(unlinkat_rmdir_beneath, tc)
+{
+	int fd;
+
+	fd = setup();
+	ATF_REQUIRE_EQ(0,
+	    unlinkat(fd, "subdir",
+	    AT_REMOVEDIR | AT_RESOLVE_BENEATH));
+	ATF_REQUIRE_ERRNO(ENOTCAPABLE,
+	    unlinkat(fd, "../outside/dir",
+	    AT_REMOVEDIR | AT_RESOLVE_BENEATH) == -1);
+	ATF_REQUIRE_EQ(0, access("outside/dir", F_OK));
+	ATF_REQUIRE(close(fd) == 0);
+}
+
+ATF_TC_WITHOUT_HEAD(funlinkat_beneath);
+ATF_TC_BODY(funlinkat_beneath, tc)
+{
+	int fd;
+
+	fd = setup();
+	ATF_REQUIRE_EQ(0,
+	    funlinkat(fd, "file", FD_NONE, AT_RESOLVE_BENEATH));
+	ATF_REQUIRE_ERRNO(ENOTCAPABLE,
+	    funlinkat(fd, "../outside/target", FD_NONE,
+	    AT_RESOLVE_BENEATH) == -1);
+	ATF_REQUIRE(close(fd) == 0);
+}
+
+ATF_TC_WITHOUT_HEAD(funlinkat_rmdir_beneath);
+ATF_TC_BODY(funlinkat_rmdir_beneath, tc)
+{
+	int fd;
+
+	fd = setup();
+	ATF_REQUIRE_EQ(0, mkdir("base/subdir2", 0755));
+	ATF_REQUIRE_EQ(0,
+	    funlinkat(fd, "subdir2", FD_NONE,
+	    AT_REMOVEDIR | AT_RESOLVE_BENEATH));
+	ATF_REQUIRE_ERRNO(ENOTCAPABLE,
+	    funlinkat(fd, "../outside/dir", FD_NONE,
+	    AT_REMOVEDIR | AT_RESOLVE_BENEATH) == -1);
+	ATF_REQUIRE_EQ(0, access("outside/dir", F_OK));
+	ATF_REQUIRE(close(fd) == 0);
+}
+
+ATF_TC(getfhat_beneath);
+ATF_TC_HEAD(getfhat_beneath, tc)
+{
+	atf_tc_set_md_var(tc, "require.user", "root");
+}
+ATF_TC_BODY(getfhat_beneath, tc)
+{
+	fhandle_t fh;
+	int fd;
+
+	fd = setup();
+	ATF_REQUIRE_EQ(0,
+	    getfhat(fd, "file", &fh, AT_RESOLVE_BENEATH));
+	ATF_REQUIRE_ERRNO(ENOTCAPABLE,
+	    getfhat(fd, "../outside/target", &fh,
+	    AT_RESOLVE_BENEATH) == -1);
+	ATF_REQUIRE(close(fd) == 0);
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+	ATF_TP_ADD_TC(tp, faccessat_beneath);
+	ATF_TP_ADD_TC(tp, chflagsat_beneath);
+	ATF_TP_ADD_TC(tp, fchmodat_beneath);
+	ATF_TP_ADD_TC(tp, fchownat_beneath);
+	ATF_TP_ADD_TC(tp, utimensat_beneath);
+	ATF_TP_ADD_TC(tp, linkat_beneath);
+	ATF_TP_ADD_TC(tp, unlinkat_beneath);
+	ATF_TP_ADD_TC(tp, unlinkat_rmdir_beneath);
+	ATF_TP_ADD_TC(tp, funlinkat_beneath);
+	ATF_TP_ADD_TC(tp, funlinkat_rmdir_beneath);
+	ATF_TP_ADD_TC(tp, getfhat_beneath);
+	return (atf_no_error());
+}