aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/sys/acl/run2
-rw-r--r--tests/sys/acl/tools-posix.test14
-rw-r--r--tests/sys/capsicum/capmode.cc12
-rw-r--r--tests/sys/fs/fusefs/xattr.cc73
-rw-r--r--tests/sys/kern/Makefile3
-rw-r--r--tests/sys/kern/jaildesc.c201
-rw-r--r--tests/sys/kern/procdesc.c119
-rw-r--r--tests/sys/kern/ptrace_test.c93
-rw-r--r--tests/sys/mac/Makefile1
-rw-r--r--tests/sys/mac/do/Makefile13
-rw-r--r--tests/sys/mac/do/common.sh176
-rw-r--r--tests/sys/mac/do/consistency.sh211
-rw-r--r--tests/sys/mac/do/invalid_configs.sh100
-rw-r--r--tests/sys/mac/do/valid_configs.sh135
-rw-r--r--tests/sys/net/Makefile9
-rw-r--r--tests/sys/net/routing/Makefile3
-rwxr-xr-xtests/sys/net/routing/test_routing.sh231
-rw-r--r--tests/sys/netinet/Makefile2
-rwxr-xr-xtests/sys/netinet6/ndp.sh15
-rw-r--r--tests/sys/netipsec/tunnel/Makefile4
-rw-r--r--tests/sys/netpfil/common/nat.sh26
21 files changed, 1386 insertions, 57 deletions
diff --git a/tests/sys/acl/run b/tests/sys/acl/run
index f8e9c8d87f71..42dbc7373f7f 100644
--- a/tests/sys/acl/run
+++ b/tests/sys/acl/run
@@ -105,7 +105,7 @@ if (isatty(fileno(STDOUT))) {
}
}
print $status, "\n";
-exit $failed ? 1 : 0;
+exit($failed ? 1 : 0);
sub process_test($$$$) {
diff --git a/tests/sys/acl/tools-posix.test b/tests/sys/acl/tools-posix.test
index 2b2a27d24a0d..aa92911761a6 100644
--- a/tests/sys/acl/tools-posix.test
+++ b/tests/sys/acl/tools-posix.test
@@ -80,7 +80,7 @@ $ getfacl -qh lll
> group::r-x
> other::r-x
-$ getfacl -q lll
+$ getfacl -nq lll
> user::rw-
> user:42:r--
> group::r--
@@ -89,7 +89,7 @@ $ getfacl -q lll
> other::r--
$ setfacl -hm u:44:x,g:45:w lll
-$ getfacl -h lll
+$ getfacl -hn lll
> # file: lll
> # owner: root
> # group: wheel
@@ -111,7 +111,7 @@ $ rm lll
# Test removing entries.
$ setfacl -x user:42: xxx
-$ getfacl xxx
+$ getfacl -n xxx
> # file: xxx
> # owner: root
> # group: wheel
@@ -369,7 +369,7 @@ $ rm ddd/xxx
$ setfacl -dm u::rwx,g::rx,o::rx,mask::rwx ddd
$ setfacl -dm g:42:rwx,u:43:r ddd
-$ getfacl -dq ddd
+$ getfacl -dnq ddd
> user::rwx
> user:43:r--
> group::r-x
@@ -378,7 +378,7 @@ $ getfacl -dq ddd
> other::r-x
$ touch ddd/xxx
-$ getfacl -q ddd/xxx
+$ getfacl -nq ddd/xxx
> user::rw-
> user:43:r--
> group::r-x # effective: r--
@@ -387,7 +387,7 @@ $ getfacl -q ddd/xxx
> other::r--
$ mkdir ddd/ddd
-$ getfacl -q ddd/ddd
+$ getfacl -nq ddd/ddd
> user::rwx
> user:43:r--
> group::r-x
@@ -405,7 +405,7 @@ $ ls -l fff | cut -d' ' -f1
> prw-r--r--
$ setfacl -m u:42:r,g:43:w fff
-$ getfacl fff
+$ getfacl -n fff
> # file: fff
> # owner: root
> # group: wheel
diff --git a/tests/sys/capsicum/capmode.cc b/tests/sys/capsicum/capmode.cc
index c6eef19b350f..fdc572f11b5b 100644
--- a/tests/sys/capsicum/capmode.cc
+++ b/tests/sys/capsicum/capmode.cc
@@ -703,8 +703,8 @@ FORK_TEST(Capmode, NewThread) {
close(thread_pipe[1]);
}
-static volatile sig_atomic_t had_signal = 0;
-static void handle_signal(int) { had_signal = 1; }
+static volatile sig_atomic_t signal_cnt = 0;
+static void handle_signal(int) { signal_cnt++; }
FORK_TEST(Capmode, SelfKill) {
pid_t me = getpid();
@@ -722,7 +722,13 @@ FORK_TEST(Capmode, SelfKill) {
// Can only kill(2) to own pid.
EXPECT_CAPMODE(kill(child, SIGUSR1));
EXPECT_OK(kill(me, SIGUSR1));
- EXPECT_EQ(1, had_signal);
+ EXPECT_EQ(1, signal_cnt);
+
+ union sigval sv;
+ sv.sival_int = 0x1234;
+ EXPECT_CAPMODE(sigqueue(child, SIGUSR1, sv));
+ EXPECT_OK(sigqueue(me, SIGUSR1, sv));
+ EXPECT_EQ(2, signal_cnt);
signal(SIGUSR1, original);
}
diff --git a/tests/sys/fs/fusefs/xattr.cc b/tests/sys/fs/fusefs/xattr.cc
index afeacd4a249e..6dfda55079eb 100644
--- a/tests/sys/fs/fusefs/xattr.cc
+++ b/tests/sys/fs/fusefs/xattr.cc
@@ -493,6 +493,79 @@ TEST_F(ListxattrSig, erange_forever)
}
/*
+ * A buggy or malicious server returns a list that isn't nul-terminated. The
+ * kernel should handle it gracefully.
+ */
+TEST_F(Listxattr, not_nul_terminated)
+{
+ uint64_t ino = 42;
+ int ns = EXTATTR_NAMESPACE_USER;
+ char *data;
+ const char expected[4] = {3, 'f', 'o', 'o'};
+ const char first[255] = "user.foo\0system.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
+ const uint8_t badlist[9] = {'u', 's', 'e', 'r', '.', 'f', 'o', 'o', 'd'};
+ Sequence seq;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillRepeatedly(Invoke(
+ ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = S_IFREG | 0644;
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr.nlink = 1;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = UINT64_MAX;
+ })));
+
+ /*
+ * On the first LISTXATTRS call, return a big attribute just to fill
+ * the heap with non-NUL data.
+ */
+ expect_listxattr(ino, 0,
+ ReturnImmediate([&](auto in __unused, auto& out) {
+ out.body.listxattr.size = sizeof(first);
+ SET_OUT_HEADER_LEN(out, listxattr);
+ }), &seq
+ );
+ expect_listxattr(ino, sizeof(first),
+ ReturnImmediate([&](auto in __unused, auto& out) {
+ memcpy((void*)out.body.bytes, first, sizeof(first));
+ out.header.len = sizeof(fuse_out_header) + sizeof(first);
+ }), &seq
+ );
+ /*
+ * On the second LISTXATTRS call, return a malformed list with no NUL
+ * termination. The heap might still be full of the data from the
+ * first call.
+ */
+ expect_listxattr(ino, 0,
+ ReturnImmediate([&](auto in __unused, auto& out) {
+ out.body.listxattr.size = sizeof(badlist);
+ SET_OUT_HEADER_LEN(out, listxattr);
+ }), &seq
+ );
+ expect_listxattr(ino, sizeof(badlist),
+ ReturnImmediate([&](auto in __unused, auto& out) {
+ memset((void*)out.body.bytes, 'x', sizeof(first));
+ memcpy((void*)out.body.bytes, badlist, sizeof(badlist));
+ out.header.len = sizeof(fuse_out_header) + sizeof(badlist);
+ }), &seq
+ );
+
+ data = new char[1024];
+
+ ASSERT_EQ(static_cast<ssize_t>(sizeof(expected)),
+ extattr_list_file(FULLPATH, ns, data, sizeof(data)))
+ << strerror(errno);
+ /*
+ * Receiving this malformed list, the kernel should log it to dmesg and
+ * report an IO error to the caller.
+ */
+ ASSERT_EQ(-1, extattr_list_file(FULLPATH, ns, data, sizeof(data)));
+ EXPECT_EQ(EIO, errno);
+}
+
+/*
* Get the size of the list that it would take to list no extended attributes
*/
TEST_F(Listxattr, size_only_empty)
diff --git a/tests/sys/kern/Makefile b/tests/sys/kern/Makefile
index a06e8702f16d..dcaeb8d2f1fa 100644
--- a/tests/sys/kern/Makefile
+++ b/tests/sys/kern/Makefile
@@ -22,6 +22,7 @@ ATF_TESTS_C+= exterr_test
ATF_TESTS_C+= fdgrowtable_test
ATF_TESTS_C+= getdirentries_test
ATF_TESTS_C+= jail_lookup_root
+ATF_TESTS_C+= jaildesc
ATF_TESTS_C+= inotify_test
ATF_TESTS_C+= kill_zombie
.if ${MK_OPENSSL} != "no"
@@ -92,6 +93,7 @@ PROGS+= sendfile_helper
LIBADD.copy_file_range+= md
LIBADD.jail_lookup_root+= jail util
+LIBADD.jaildesc+= pthread
LIBADD.ssl_sendfile+= pthread crypto ssl
CFLAGS.sys_getrandom+= -I${SRCTOP}/sys/contrib/zstd/lib
LIBADD.sys_getrandom+= zstd
@@ -104,6 +106,7 @@ LIBADD.kcov+= pthread
CFLAGS.ktls_test+= -DOPENSSL_API_COMPAT=0x10100000L
LIBADD.ktls_test+= crypto util
LIBADD.listener_wakeup+= pthread
+LIBADD.procdesc+= kvm pthread
LIBADD.shutdown_dgram+= pthread
LIBADD.socket_msg_waitall+= pthread
LIBADD.socket_splice+= pthread
diff --git a/tests/sys/kern/jaildesc.c b/tests/sys/kern/jaildesc.c
new file mode 100644
index 000000000000..11d751554887
--- /dev/null
+++ b/tests/sys/kern/jaildesc.c
@@ -0,0 +1,201 @@
+/*
+ * Copyright (c) 2026 Mark Johnston <markj@FreeBSD.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <sys/param.h>
+#include <sys/jail.h>
+#include <sys/uio.h>
+
+#include <atf-c.h>
+#include <errno.h>
+#include <poll.h>
+#include <pthread.h>
+#include <pwd.h>
+#include <string.h>
+#include <unistd.h>
+
+/*
+ * Create a persistent jail and return an owning descriptor for it.
+ * The jail is removed when the returned descriptor is closed.
+ */
+static int
+create_jail(const char *name)
+{
+ struct iovec iov[8];
+ int desc, jid, n;
+
+ desc = -1;
+ n = 0;
+ iov[n].iov_base = __DECONST(void *, "name");
+ iov[n++].iov_len = strlen("name") + 1;
+ iov[n].iov_base = __DECONST(void *, name);
+ iov[n++].iov_len = strlen(name) + 1;
+ iov[n].iov_base = __DECONST(void *, "path");
+ iov[n++].iov_len = strlen("path") + 1;
+ iov[n].iov_base = __DECONST(void *, "/");
+ iov[n++].iov_len = strlen("/") + 1;
+ iov[n].iov_base = __DECONST(void *, "persist");
+ iov[n++].iov_len = strlen("persist") + 1;
+ iov[n].iov_base = NULL;
+ iov[n++].iov_len = 0;
+ iov[n].iov_base = __DECONST(void *, "desc");
+ iov[n++].iov_len = strlen("desc") + 1;
+ iov[n].iov_base = &desc;
+ iov[n++].iov_len = sizeof(desc);
+ jid = jail_set(iov, n, JAIL_CREATE | JAIL_OWN_DESC);
+ ATF_REQUIRE_MSG(jid >= 0, "jail_set: %s", strerror(errno));
+ return (desc);
+}
+
+static void *
+poll_jaildesc(void *arg)
+{
+ struct pollfd pfd;
+
+ pfd.fd = *(int *)arg;
+ pfd.events = POLLHUP;
+ (void)poll(&pfd, 1, 5000);
+ return ((void *)(uintptr_t)pfd.revents);
+}
+
+/*
+ * Regression test for the case where a jail descriptor is closed while a
+ * thread is blocking in poll(2) on it.
+ */
+ATF_TC(poll_close_race);
+ATF_TC_HEAD(poll_close_race, tc)
+{
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+ATF_TC_BODY(poll_close_race, tc)
+{
+ pthread_t thr;
+ uintptr_t revents;
+ int error, jd;
+
+ jd = create_jail("jaildesc_poll_close_race");
+
+ error = pthread_create(&thr, NULL, poll_jaildesc, &jd);
+ ATF_REQUIRE_MSG(error == 0, "pthread_create: %s", strerror(error));
+
+ /* Wait for the thread to block in poll(2). */
+ usleep(250000);
+
+ ATF_REQUIRE_MSG(close(jd) == 0, "close: %s", strerror(errno));
+
+ error = pthread_join(thr, (void *)&revents);
+ ATF_REQUIRE_MSG(error == 0, "pthread_join: %s", strerror(error));
+ ATF_REQUIRE_EQ(revents, POLLNVAL);
+}
+
+/*
+ * Verify that poll(2) of a jail descriptor returns POLLHUP when the jail
+ * is removed.
+ */
+ATF_TC(poll_remove_wakeup);
+ATF_TC_HEAD(poll_remove_wakeup, tc)
+{
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+ATF_TC_BODY(poll_remove_wakeup, tc)
+{
+ pthread_t thr;
+ uintptr_t revents;
+ int error, jd;
+
+ jd = create_jail("jaildesc_poll_remove_wakeup");
+
+ error = pthread_create(&thr, NULL, poll_jaildesc, &jd);
+ ATF_REQUIRE_MSG(error == 0, "pthread_create: %s", strerror(error));
+
+ /* Wait for the thread to block in poll(2). */
+ usleep(250000);
+
+ ATF_REQUIRE_MSG(jail_remove_jd(jd) == 0,
+ "jail_remove_jd: %s", strerror(errno));
+
+ error = pthread_join(thr, (void *)&revents);
+ ATF_REQUIRE_MSG(error == 0, "pthread_join: %s", strerror(error));
+ ATF_REQUIRE_EQ(revents, POLLHUP);
+
+ ATF_REQUIRE_MSG(close(jd) == 0, "close: %s", strerror(errno));
+}
+
+static int
+get_jaildesc(const char *name)
+{
+ struct iovec iov[4];
+ char namebuf[MAXHOSTNAMELEN];
+ int desc, jid, n;
+
+ strlcpy(namebuf, name, sizeof(namebuf));
+ desc = -1;
+ n = 0;
+ iov[n].iov_base = __DECONST(void *, "name");
+ iov[n++].iov_len = strlen("name") + 1;
+ iov[n].iov_base = namebuf;
+ iov[n++].iov_len = sizeof(namebuf);
+ iov[n].iov_base = __DECONST(void *, "desc");
+ iov[n++].iov_len = strlen("desc") + 1;
+ iov[n].iov_base = &desc;
+ iov[n++].iov_len = sizeof(desc);
+ jid = jail_get(iov, n, JAIL_GET_DESC);
+ ATF_REQUIRE_MSG(jid >= 0, "jail_get: %s", strerror(errno));
+ return (desc);
+}
+
+/*
+ * Regression test for the same use-after-free as poll_close_race, but with a
+ * non-owning JAIL_GET_DESC descriptor obtained without root privileges.
+ */
+ATF_TC(poll_close_race_get_desc);
+ATF_TC_HEAD(poll_close_race_get_desc, tc)
+{
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+ATF_TC_BODY(poll_close_race_get_desc, tc)
+{
+ struct passwd *pw;
+ pthread_t thr;
+ uintptr_t revents;
+ int error, jd, owning_jd;
+
+ /* Create the jail as root; keep the owning descriptor for cleanup. */
+ owning_jd = create_jail("jaildesc_poll_close_get_desc");
+
+ /*
+ * Drop root privileges. jail_get(2) with JAIL_GET_DESC does not
+ * require PRIV_JAIL_REMOVE, so a non-root process in the host prison
+ * can obtain a read-only descriptor for any visible jail.
+ */
+ pw = getpwnam("nobody");
+ ATF_REQUIRE_MSG(pw != NULL, "getpwnam: %s", strerror(errno));
+ ATF_REQUIRE_MSG(setuid(pw->pw_uid) == 0, "setuid: %s", strerror(errno));
+
+ jd = get_jaildesc("jaildesc_poll_close_get_desc");
+
+ error = pthread_create(&thr, NULL, poll_jaildesc, &jd);
+ ATF_REQUIRE_MSG(error == 0, "pthread_create: %s", strerror(error));
+
+ /* Wait for the thread to block in poll(2). */
+ usleep(250000);
+
+ ATF_REQUIRE_MSG(close(jd) == 0, "close: %s", strerror(errno));
+
+ error = pthread_join(thr, (void *)&revents);
+ ATF_REQUIRE_MSG(error == 0, "pthread_join: %s", strerror(error));
+ ATF_REQUIRE_EQ(revents, POLLNVAL);
+
+ ATF_REQUIRE_MSG(close(owning_jd) == 0, "close: %s", strerror(errno));
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+ ATF_TP_ADD_TC(tp, poll_close_race);
+ ATF_TP_ADD_TC(tp, poll_remove_wakeup);
+ ATF_TP_ADD_TC(tp, poll_close_race_get_desc);
+
+ return (atf_no_error());
+}
diff --git a/tests/sys/kern/procdesc.c b/tests/sys/kern/procdesc.c
index 3334ee404518..2fe50be45bf7 100644
--- a/tests/sys/kern/procdesc.c
+++ b/tests/sys/kern/procdesc.c
@@ -2,6 +2,7 @@
* SPDX-License-Identifier: BSD-2-Clause
*
* Copyright (c) 2026 ConnectWise
+ * Copyright (c) 2026 Mark Johnston <markj@FreeBSD.org>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
@@ -32,12 +33,51 @@
#include <sys/sysctl.h>
#include <sys/wait.h>
-#include <atf-c.h>
+#include <fcntl.h>
+#include <poll.h>
+#include <pthread.h>
#include <stdio.h>
+#include <unistd.h>
+
+#include <atf-c.h>
+#include <kvm.h>
/* Tests for procdesc(4) that aren't specific to any one syscall */
/*
+ * Block until a thread in the specified process is sleeping in the specified
+ * wait message.
+ */
+static void
+wait_for_naptime(pid_t pid, const char *wmesg)
+{
+ kvm_t *kd;
+ int count;
+
+ kd = kvm_openfiles(NULL, "/dev/null", NULL, O_RDONLY, NULL);
+ ATF_REQUIRE(kd != NULL);
+ for (;;) {
+ struct kinfo_proc *kip;
+ int i;
+
+ usleep(1000);
+ kip = kvm_getprocs(kd, KERN_PROC_PID | KERN_PROC_INC_THREAD,
+ pid, &count);
+ ATF_REQUIRE(kip != NULL);
+ for (i = 0; i < count; i++) {
+ ATF_REQUIRE(kip[i].ki_stat != SZOMB);
+ if (kip[i].ki_stat == SSLEEP &&
+ strcmp(kip[i].ki_wmesg, wmesg) == 0)
+ break;
+ }
+ if (i < count)
+ break;
+ }
+
+ kvm_close(kd);
+}
+
+/*
* Even after waiting on a process descriptor with waitpid(2), the kernel will
* not recycle the pid until after the process descriptor is closed. That is
* important to prevent users from trying to wait() twice, the second time
@@ -90,9 +130,86 @@ ATF_TC_BODY(pid_recycle, tc)
close(pd);
}
+static void *
+poll_procdesc(void *arg)
+{
+ struct pollfd pfd;
+
+ pfd.fd = *(int *)arg;
+ pfd.events = POLLHUP;
+ (void)poll(&pfd, 1, 5000);
+ return ((void *)(uintptr_t)pfd.revents);
+}
+
+/*
+ * Regression test to exercise the case where a procdesc is closed while a
+ * thread is poll()ing it.
+ */
+ATF_TC_WITHOUT_HEAD(poll_close_race);
+ATF_TC_BODY(poll_close_race, tc)
+{
+ pthread_t thr;
+ pid_t pid;
+ uintptr_t revents;
+ int error, pd;
+
+ pid = pdfork(&pd, PD_DAEMON);
+ ATF_REQUIRE_MSG(pid >= 0, "pdfork: %s", strerror(errno));
+ if (pid == 0) {
+ pause();
+ _exit(0);
+ }
+
+ error = pthread_create(&thr, NULL, poll_procdesc, &pd);
+ ATF_REQUIRE_MSG(error == 0, "pthread_create: %s", strerror(error));
+
+ wait_for_naptime(getpid(), "select");
+
+ ATF_REQUIRE_MSG(close(pd) == 0, "close: %s", strerror(errno));
+
+ error = pthread_join(thr, (void *)&revents);
+ ATF_REQUIRE_MSG(error == 0, "pthread_join: %s", strerror(error));
+ ATF_REQUIRE_EQ(revents, POLLNVAL);
+}
+
+/*
+ * Verify that poll(2) of a procdesc returns POLLHUP when the process exits.
+ */
+ATF_TC_WITHOUT_HEAD(poll_exit_wakeup);
+ATF_TC_BODY(poll_exit_wakeup, tc)
+{
+ pthread_t thr;
+ uintptr_t revents;
+ pid_t pid;
+ int error, pd;
+
+ pid = pdfork(&pd, PD_DAEMON);
+ ATF_REQUIRE_MSG(pid >= 0, "pdfork: %s", strerror(errno));
+ if (pid == 0) {
+ pause();
+ _exit(0);
+ }
+
+ error = pthread_create(&thr, NULL, poll_procdesc, &pd);
+ ATF_REQUIRE_MSG(error == 0, "pthread_create: %s", strerror(error));
+
+ wait_for_naptime(getpid(), "select");
+
+ ATF_REQUIRE_MSG(pdkill(pd, SIGKILL) == 0,
+ "pdkill: %s", strerror(errno));
+
+ error = pthread_join(thr, (void *)&revents);
+ ATF_REQUIRE_MSG(error == 0, "pthread_join: %s", strerror(error));
+ ATF_REQUIRE_EQ(revents, POLLHUP);
+
+ ATF_REQUIRE_MSG(close(pd) == 0, "close: %s", strerror(errno));
+}
+
ATF_TP_ADD_TCS(tp)
{
ATF_TP_ADD_TC(tp, pid_recycle);
+ ATF_TP_ADD_TC(tp, poll_close_race);
+ ATF_TP_ADD_TC(tp, poll_exit_wakeup);
return (atf_no_error());
}
diff --git a/tests/sys/kern/ptrace_test.c b/tests/sys/kern/ptrace_test.c
index fee0bd2ffa38..3a55a6f48033 100644
--- a/tests/sys/kern/ptrace_test.c
+++ b/tests/sys/kern/ptrace_test.c
@@ -3614,6 +3614,10 @@ ATF_TC_BODY(ptrace__PT_STEP_with_signal, tc)
ATF_REQUIRE(pl.pl_flags & PL_FLAG_SI);
REQUIRE_EQ(pl.pl_siginfo.si_signo, SIGABRT);
+#if defined(__riscv)
+ atf_tc_expect_fail("PT_STEP not implemented on riscv, see sys/riscv/riscv/ptrace_machdep.c");
+#endif
+
/* Step the child process inserting SIGUSR1. */
REQUIRE_EQ(ptrace(PT_STEP, fpid, (caddr_t)1, SIGUSR1), 0);
@@ -3731,6 +3735,10 @@ ATF_TC_BODY(ptrace__step_siginfo, tc)
ATF_REQUIRE(WIFSTOPPED(status));
REQUIRE_EQ(WSTOPSIG(status), SIGSTOP);
+#if defined(__riscv)
+ atf_tc_expect_fail("PT_STEP not implemented on riscv, see sys/riscv/riscv/ptrace_machdep.c");
+#endif
+
/* Step the child ignoring the SIGSTOP. */
REQUIRE_EQ(ptrace(PT_STEP, fpid, (caddr_t)1, 0), 0);
@@ -4362,6 +4370,25 @@ ATF_TC_BODY(ptrace__procdesc_reparent_wait_child, tc)
REQUIRE_EQ(close(pd), 0);
}
+static void
+pt_sc_remote(pid_t pid, struct ptrace_sc_remote *pscr, int error,
+ syscallarg_t ret)
+{
+ pid_t wpid;
+ int status;
+
+ ATF_REQUIRE(ptrace(PT_SC_REMOTE, pid, (caddr_t)pscr, sizeof(*pscr)) !=
+ -1);
+ ATF_REQUIRE_EQ(pscr->pscr_ret.sr_error, error);
+ if (error == 0)
+ ATF_REQUIRE_EQ(pscr->pscr_ret.sr_retval[0], ret);
+
+ wpid = waitpid(pid, &status, 0);
+ REQUIRE_EQ(wpid, pid);
+ ATF_REQUIRE(WIFSTOPPED(status));
+ REQUIRE_EQ(WSTOPSIG(status), SIGSTOP);
+}
+
/*
* Try using PT_SC_REMOTE to get the PID of a traced child process.
*/
@@ -4386,35 +4413,62 @@ ATF_TC_BODY(ptrace__PT_SC_REMOTE_getpid, tc)
pscr.pscr_syscall = SYS_getpid;
pscr.pscr_nargs = 0;
pscr.pscr_args = NULL;
- ATF_REQUIRE(ptrace(PT_SC_REMOTE, fpid, (caddr_t)&pscr, sizeof(pscr)) !=
- -1);
- ATF_REQUIRE_MSG(pscr.pscr_ret.sr_error == 0,
- "remote getpid failed with error %d", pscr.pscr_ret.sr_error);
- ATF_REQUIRE_MSG(pscr.pscr_ret.sr_retval[0] == fpid,
- "unexpected return value %jd instead of %d",
- (intmax_t)pscr.pscr_ret.sr_retval[0], fpid);
-
- wpid = waitpid(fpid, &status, 0);
- REQUIRE_EQ(wpid, fpid);
- ATF_REQUIRE(WIFSTOPPED(status));
- REQUIRE_EQ(WSTOPSIG(status), SIGSTOP);
+ pt_sc_remote(fpid, &pscr, 0, fpid);
pscr.pscr_syscall = SYS_getppid;
pscr.pscr_nargs = 0;
pscr.pscr_args = NULL;
- ATF_REQUIRE(ptrace(PT_SC_REMOTE, fpid, (caddr_t)&pscr, sizeof(pscr)) !=
- -1);
- ATF_REQUIRE_MSG(pscr.pscr_ret.sr_error == 0,
- "remote getppid failed with error %d", pscr.pscr_ret.sr_error);
- ATF_REQUIRE_MSG(pscr.pscr_ret.sr_retval[0] == getpid(),
- "unexpected return value %jd instead of %d",
- (intmax_t)pscr.pscr_ret.sr_retval[0], fpid);
+ pt_sc_remote(fpid, &pscr, 0, getpid());
+
+ ATF_REQUIRE(ptrace(PT_DETACH, fpid, (caddr_t)1, 0) != -1);
+}
+
+ATF_TC_WITHOUT_HEAD(ptrace__PT_SC_REMOTE_syscall_validation);
+ATF_TC_BODY(ptrace__PT_SC_REMOTE_syscall_validation, tc)
+{
+ struct ptrace_sc_remote pscr;
+ quad_t code;
+ int status;
+ pid_t fpid, wpid;
+
+ code = SYS_MAXSYSCALL;
+
+ ATF_REQUIRE((fpid = fork()) != -1);
+ if (fpid == 0) {
+ trace_me();
+ exit(0);
+ }
wpid = waitpid(fpid, &status, 0);
REQUIRE_EQ(wpid, fpid);
ATF_REQUIRE(WIFSTOPPED(status));
REQUIRE_EQ(WSTOPSIG(status), SIGSTOP);
+ pscr.pscr_syscall = SYS_MAXSYSCALL;
+ pscr.pscr_nargs = 0;
+ pscr.pscr_args = NULL;
+ pt_sc_remote(fpid, &pscr, ENOSYS, 0);
+
+ pscr.pscr_syscall = SYS_syscall;
+ pscr.pscr_nargs = 0;
+ pscr.pscr_args = NULL;
+ pt_sc_remote(fpid, &pscr, EINVAL, 0);
+
+ pscr.pscr_syscall = SYS_syscall;
+ pscr.pscr_nargs = 1;
+ pscr.pscr_args = (syscallarg_t *)&code;
+ pt_sc_remote(fpid, &pscr, ENOSYS, 0);
+
+ pscr.pscr_syscall = SYS___syscall;
+ pscr.pscr_nargs = 0;
+ pscr.pscr_args = NULL;
+ pt_sc_remote(fpid, &pscr, EINVAL, 0);
+
+ pscr.pscr_syscall = SYS___syscall;
+ pscr.pscr_nargs = 1;
+ pscr.pscr_args = (syscallarg_t *)&code;
+ pt_sc_remote(fpid, &pscr, ENOSYS, 0);
+
ATF_REQUIRE(ptrace(PT_DETACH, fpid, (caddr_t)1, 0) != -1);
}
@@ -4657,6 +4711,7 @@ ATF_TP_ADD_TCS(tp)
ATF_TP_ADD_TC(tp, ptrace__procdesc_wait_child);
ATF_TP_ADD_TC(tp, ptrace__procdesc_reparent_wait_child);
ATF_TP_ADD_TC(tp, ptrace__PT_SC_REMOTE_getpid);
+ ATF_TP_ADD_TC(tp, ptrace__PT_SC_REMOTE_syscall_validation);
ATF_TP_ADD_TC(tp, ptrace__reap_kill_stopped);
ATF_TP_ADD_TC(tp, ptrace__PT_ATTACH_no_EINTR);
ATF_TP_ADD_TC(tp, ptrace__PT_DETACH_continued);
diff --git a/tests/sys/mac/Makefile b/tests/sys/mac/Makefile
index 3447d00122f5..9858b09b5f1d 100644
--- a/tests/sys/mac/Makefile
+++ b/tests/sys/mac/Makefile
@@ -1,6 +1,7 @@
TESTSDIR= ${TESTSBASE}/sys/mac
TESTS_SUBDIRS+= bsdextended
+TESTS_SUBDIRS+= do
TESTS_SUBDIRS+= ipacl
TESTS_SUBDIRS+= portacl
diff --git a/tests/sys/mac/do/Makefile b/tests/sys/mac/do/Makefile
new file mode 100644
index 000000000000..0c40f65b65f6
--- /dev/null
+++ b/tests/sys/mac/do/Makefile
@@ -0,0 +1,13 @@
+PACKAGE= tests
+
+TESTSDIR= ${TESTSBASE}/sys/mac/do
+
+ATF_TESTS_SH+= valid_configs invalid_configs consistency
+
+${PACKAGE}FILES+= common.sh
+
+TEST_METADATA+= execenv="jail"
+TEST_METADATA+= required_kmods="mac_do"
+TEST_METADATA+= required_user="root"
+
+.include <bsd.test.mk>
diff --git a/tests/sys/mac/do/common.sh b/tests/sys/mac/do/common.sh
new file mode 100644
index 000000000000..4f0e838bbf5f
--- /dev/null
+++ b/tests/sys/mac/do/common.sh
@@ -0,0 +1,176 @@
+# Copyright (c) 2026 The FreeBSD Foundation
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# This software was developed by Olivier Certner <olce@FreeBSD.org> at
+# Kumacom SARL under sponsorship from the FreeBSD Foundation.
+
+rules_parameter()
+{
+ echo "$1".rules
+}
+
+exec_paths_parameter()
+{
+ echo "$1".exec_paths
+}
+
+: ${MDO:=/usr/bin/mdo}
+
+ROOT_KNOB=security.mac.do
+RULES_KNOB=$(rules_parameter ${ROOT_KNOB})
+EXEC_PATHS_KNOB=$(exec_paths_parameter ${ROOT_KNOB})
+PPE_KNOB=${ROOT_KNOB}.print_parse_error
+
+ROOT_JAIL_PARAM=mac.do
+RULES_JAIL_PARAM=$(rules_parameter ${ROOT_JAIL_PARAM})
+EXEC_PATHS_JAIL_PARAM=$(exec_paths_parameter ${ROOT_JAIL_PARAM})
+
+# To be overridden to execute commands in a sub-jail
+JEXEC=
+
+# Exit status: 0 iff disabled
+mac_do_disabled()
+{
+ [ -z "$($JEXEC sysctl -n ${RULES_KNOB})" ] ||
+ [ -z "$($JEXEC sysctl -n ${EXEC_PATHS_KNOB})" ]
+}
+
+mac_do_check_disabled()
+{
+ mac_do_disabled || atf_fail "mac_do(4) expected disabled but is not."
+}
+
+mac_do_ensure_disabled()
+{
+ mac_do_disabled || $JEXEC sysctl ${RULES_KNOB}=""
+}
+
+sysctl_rules()
+{
+ $JEXEC sysctl -n ${RULES_KNOB}
+}
+
+sysctl_exec_paths()
+{
+ $JEXEC sysctl -n ${EXEC_PATHS_KNOB}
+}
+
+# $1 = sysctl func, $2 = expected value
+sysctl_check()
+{
+ local func value
+
+ func=$1
+ value=$2
+ atf_check [ "$($func)" = "$value" ]
+}
+
+# $1 = value
+sysctl_check_rules()
+{
+ local value
+
+ value=$1
+ sysctl_check sysctl_rules $value
+}
+
+# $1 = value
+sysctl_check_exec_paths()
+{
+ local value
+
+ value=$1
+ sysctl_check sysctl_exec_paths $value
+}
+
+# $1 = knob name, $2 = value
+sysctl_set_and_check()
+{
+ local knob value
+
+ knob=$1
+ value=$2
+ atf_check -o ignore $JEXEC sysctl "$knob"="$value"
+ atf_check -o inline:"$value\n" $JEXEC sysctl -n "$knob"
+}
+
+# $1 = knob name, $2 = value
+sysctl_set_and_check_fails()
+{
+ local knob value orig_value
+
+ knob=$1
+ value=$2
+ orig_value=$(sysctl -n "$knob")
+ atf_check -s not-exit:0 -o ignore -e ignore $JEXEC sysctl "$knob"="$value"
+ atf_check -o inline:"${orig_value}\n" $JEXEC sysctl -n "$knob"
+}
+
+# $1 = sysctl function, $2 = value
+sysctl_set_and_check_rules_common()
+{
+ local func value
+
+ func=$1
+ value=$2
+ # Use older in-rule separator (':') first to have final value as specified
+ "$func" ${RULES_KNOB} "$(echo "$value" | sed 's%>%:%')"
+ "$func" ${RULES_KNOB} "$value"
+}
+
+# $1 = value
+sysctl_set_and_check_rules()
+{
+ local value
+
+ value=$1
+ sysctl_set_and_check_rules_common sysctl_set_and_check "$value"
+}
+
+# $1 = value
+sysctl_set_and_check_fails_rules()
+{
+ local value
+
+ value=$1
+ sysctl_set_and_check_rules_common sysctl_set_and_check_fails "$value"
+}
+
+# $1 = sysctl function, $2 = value
+sysctl_set_and_check_exec_paths_common()
+{
+ local func value
+
+ func=$1
+ value=$2
+ # Use older in-rule separator (':') first to have final value as specified
+ "$func" ${EXEC_PATHS_KNOB} "$(echo "$value" | sed 's%>%:%')"
+ "$func" ${EXEC_PATHS_KNOB} "$value"
+}
+
+# $1 = value
+sysctl_set_and_check_exec_paths()
+{
+ local value
+
+ value=$1
+ sysctl_set_and_check_exec_paths_common sysctl_set_and_check "$value"
+}
+
+# Create a persistent subjail. Echoes its JID.
+launch_subjail()
+{
+ (
+ set -o pipefail
+ $JEXEC jail -c -J /dev/stdout persist=true |
+ sed -nE 's%^.*jid=([0-9]+).*$%\1%p'
+ ) || atf_fail "Cannot create a subjail (check children limits?)"
+}
+
+atf_require_prog sysctl
+atf_require_prog jail
+atf_require_prog sed
+
+# Do not pollute kernel logs with parse errors
+sysctl $PPE_KNOB=0 >/dev/null 2>&1
diff --git a/tests/sys/mac/do/consistency.sh b/tests/sys/mac/do/consistency.sh
new file mode 100644
index 000000000000..6a64917edb6d
--- /dev/null
+++ b/tests/sys/mac/do/consistency.sh
@@ -0,0 +1,211 @@
+# Copyright (c) 2026 The FreeBSD Foundation
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# This software was developed by Olivier Certner <olce@FreeBSD.org> at
+# Kumacom SARL under sponsorship from the FreeBSD Foundation.
+
+SJ_JID_FILE=sj.jid
+
+atf_test_case concurrent_rules_exec_paths_changes
+concurrent_rules_exec_paths_changes_head()
+{
+ atf_set descr "Consistency of rules and exec paths changes on same jail"
+}
+concurrent_rules_exec_paths_changes_body()
+{
+ local rules exec_paths rules_es exec_paths_es
+
+ for I in $(jot - 1 1000); do
+ sysctl_set_and_check_rules "uid=$I>uid=1001"
+ done &
+ rules=$!
+
+ for I in $(jot - 1 1000); do
+ sysctl_set_and_check_exec_paths /nowhere/nonexistent$I
+ done &
+ exec_paths=$!
+
+ wait $rules
+ rules_es=$?
+
+ wait $exec_paths
+ exec_paths_es=$?
+
+ # atf_check called in the asynchronous AND-OR lists above causes exit of the
+ # subshells and also a write to the ATF result file. These writes are
+ # concurrent and may cause the result file to be malformed. Consequently,
+ # it is important that, once execution becomes sequential again, atf_fail() is
+ # called again (and not just exit()).
+ if [ $rules_es -ne 0 ] || [ $exec_paths_es -ne 0 ]; then
+ atf_fail "Rules exit status: $rules_es, \
+exec paths exit status: $exec_paths_es"
+ fi
+}
+
+atf_test_case inheritance cleanup
+inheritance_head()
+{
+ atf_set descr "Simple inheritance test (values propagated to child jail)"
+}
+inheritance_body()
+{
+ local sj rules exec_paths
+
+ # For the sake of not running the test under Kyua
+ mac_do_ensure_disabled
+
+ sj=$(launch_subjail)
+ echo $sj > "${SJ_JID_FILE}"
+
+ jail -m jid=$sj ${ROOT_JAIL_PARAM}=inherit
+ JEXEC="jexec $sj"
+ mac_do_check_disabled
+ JEXEC=
+
+ rules="uid=1001>uid=0"
+ sysctl_set_and_check_rules $rules
+ JEXEC="jexec $sj"
+ sysctl_check_rules $rules
+ JEXEC=
+
+ rules="gid=1001>uid=0"
+ sysctl_set_and_check_rules $rules
+ JEXEC="jexec $sj"
+ sysctl_check_rules $rules
+ JEXEC=
+
+ # Not really necessary, just to keep mac_do(4) disabled
+ sysctl_set_and_check_rules ""
+
+ exec_paths="/nowhere/nonexistent"
+ sysctl_set_and_check_exec_paths $exec_paths
+ JEXEC="jexec $sj"
+ sysctl_check_exec_paths $exec_paths
+ JEXEC=
+
+ exec_paths="$MDO"
+ sysctl_set_and_check_exec_paths $exec_paths
+ JEXEC="jexec $sj"
+ sysctl_check_exec_paths $exec_paths
+ JEXEC=
+}
+inheritance_cleanup()
+{
+ # We clean up our subjail manually just for the sake of launching this test
+ # with atf-sh. Kyua is informed that these tests should run in a jail, and
+ # kills it automatically after the test, which kills all subjails. It is
+ # annoying that atf-sh does not offer a more practical way to pass
+ # information from the body to the cleanup part than a file.
+ jail -r $(cat "${SJ_JID_FILE}")
+ rm -f "${SJ_JID_FILE}"
+}
+
+atf_test_case inheritance_relax_parent_jail cleanup
+inheritance_relax_parent_jail_head()
+{
+ atf_set descr \
+ "Test sequential consistency in a \"relax parent rules\" scenario"
+}
+inheritance_relax_parent_jail_body()
+{
+ local sj rules exec_paths subproc
+
+ sj=$(launch_subjail)
+ echo $sj > "${SJ_JID_FILE}"
+
+ jail -m jid=$sj ${ROOT_JAIL_PARAM}=inherit
+ rules="uid=1001>uid=0"
+ sysctl_set_and_check_rules $rules
+ # Additional inheritance sanity check
+ JEXEC="jexec $sj"
+ sysctl_check_rules $rules
+ JEXEC=
+ exec_paths="$MDO"
+ sysctl_set_and_check_exec_paths $exec_paths
+ # Additional inheritance sanity check
+ JEXEC="jexec $sj"
+ sysctl_check_exec_paths $exec_paths
+ JEXEC=
+
+ # Launch a process that tries to become 'root' from user 1002, and verify
+ # that this always fails.
+ { for I in $(jot - 1 1000); do
+ jexec $sj "$MDO" -u 1002 -g 1002 -G 1002 "$MDO" -i true 2>/dev/null &&
+ exit 1
+ done; true; } &
+ subproc=$!
+
+ # Decouple the subjail from the parent jail, copying its parameters
+ jail -m jid=$sj ${ROOT_JAIL_PARAM}=new
+ # Allow user 1002 to become 'root' on the parent jail
+ sysctl_set_and_check_rules "$rules;uid=1002>uid=0"
+ JEXEC="jexec $sj"
+ # Additional sanity check (that rules of the subjail are now independent)
+ [ "$(sysctl_rules)" == $rules ] || atf_fail "Rules not copied"
+ [ "$(sysctl_exec_paths)" == $exec_paths ] ||
+ atf_fail "Exec paths not copied"
+ JEXEC=
+
+ wait $subproc || atf_fail "A transition wrongly succeeded in the subjail!"
+}
+inheritance_relax_parent_jail_cleanup()
+{
+ # See inheritance_cleanup() for explanations
+ jail -r $(cat "${SJ_JID_FILE}")
+ rm -f "${SJ_JID_FILE}"
+}
+
+atf_test_case same_knob_and_jail_parameter cleanup
+same_knob_and_jail_parameter_head()
+{
+ atf_set descr \
+ "Corresponding sysctl knobs and jail parameters have same value"
+}
+same_knob_and_jail_parameter_body()
+{
+ local sj rules exec_paths subproc
+
+ sj=$(launch_subjail)
+ echo $sj > "${SJ_JID_FILE}"
+
+ # Set sysctl knobs, observe parameters
+ rules="uid=19999>uid=21700"
+ exec_paths="/improbable/path/he"
+ JEXEC="jexec $sj"
+ sysctl_set_and_check_rules $rules
+ sysctl_set_and_check_exec_paths $exec_paths
+ JEXEC=
+ atf_check -o inline:"$rules\n" jls -j $sj ${RULES_JAIL_PARAM}
+ atf_check -o inline:"${exec_paths}\n" jls -j $sj ${EXEC_PATHS_JAIL_PARAM}
+
+ # Set parameters, observe knobs
+ rules="uid=128000>uid=-1"
+ exec_paths="/hello/i_ve/changed"
+ jail -m jid=$sj ${RULES_JAIL_PARAM}=$rules \
+ ${EXEC_PATHS_JAIL_PARAM}=${exec_paths}
+ JEXEC="jexec $sj"
+ sysctl_check_rules $rules
+ sysctl_check_exec_paths $exec_paths
+ JEXEC=
+}
+same_knob_and_jail_parameter_cleanup()
+{
+ # See inheritance_cleanup() for explanations
+ jail -r $(cat "${SJ_JID_FILE}")
+ rm -f "${SJ_JID_FILE}"
+}
+
+
+atf_init_test_cases()
+{
+ . $(atf_get_srcdir)/common.sh
+ atf_require_prog jot
+ # Needs an absolute path for mdo(1), to set it in exec_paths
+ atf_require_prog "$MDO"
+
+ atf_add_test_case concurrent_rules_exec_paths_changes
+ atf_add_test_case inheritance
+ atf_add_test_case inheritance_relax_parent_jail
+ atf_add_test_case same_knob_and_jail_parameter
+}
diff --git a/tests/sys/mac/do/invalid_configs.sh b/tests/sys/mac/do/invalid_configs.sh
new file mode 100644
index 000000000000..91e38a0055c0
--- /dev/null
+++ b/tests/sys/mac/do/invalid_configs.sh
@@ -0,0 +1,100 @@
+# Copyright (c) 2026 The FreeBSD Foundation
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# This software was developed by Olivier Certner <olce@FreeBSD.org> at
+# Kumacom SARL under sponsorship from the FreeBSD Foundation.
+
+atf_test_case rule_no_target_part
+rule_no_target_part_head()
+{
+ atf_set descr "Missing target part in a rule"
+}
+rule_no_target_part_body()
+{
+ sysctl_set_and_check_fails_rules "uid=0>"
+ sysctl_set_and_check_fails_rules "gid=0>"
+ sysctl_set_and_check_fails_rules "uid=0"
+ sysctl_set_and_check_fails_rules "gid=0"
+}
+
+atf_test_case rule_no_match_part
+rule_no_match_part_head()
+{
+ atf_set descr "Missing match part in a rule"
+}
+rule_no_match_part_body()
+{
+ sysctl_set_and_check_fails_rules ">uid=0"
+ sysctl_set_and_check_fails_rules ">gid=0"
+}
+
+atf_test_case rule_space_between_flag_and_gid_fail
+rule_space_between_flag_and_gid_fail_head()
+{
+ atf_set descr "No space allowed between flag and GID"
+}
+rule_space_between_flag_and_gid_fail_body()
+{
+ sysctl_set_and_check_fails_rules "uid=1001>uid=0,gid=0,+ gid=0"
+}
+
+atf_test_case rule_user_names_fail
+rule_user_names_fail_head()
+{
+ atf_set descr "Reject user names (only numerical IDs supported)"
+}
+rule_user_names_fail_body()
+{
+ sysctl_set_and_check_fails_rules "uid=user>uid=0"
+ sysctl_set_and_check_fails_rules "uid=1001>uid=root"
+}
+
+atf_test_case rule_group_names_fail
+rule_group_names_fail_head()
+{
+ atf_set descr "Reject group names (only numerical IDs supported)"
+}
+rule_group_names_fail_body()
+{
+ sysctl_set_and_check_fails_rules "gid=group>gid=0"
+ sysctl_set_and_check_fails_rules "gid=1001>gid=root"
+ sysctl_set_and_check_fails_rules "gid=1001>gid=0,+gid=operator"
+}
+
+atf_test_case rules_wrong_separator
+rules_wrong_separator_head()
+{
+ atf_set descr "Wrong rules separator"
+}
+rules_wrong_separator_body()
+{
+ sysctl_set_and_check_fails_rules "uid=1001>gid=0:gid=1001>gid=5"
+}
+
+# Added after observing a panic() in this situation because of a double-free
+# after introduction of "exec_paths".
+atf_test_case non_first_rule_unparseable
+non_first_rule_unparseable_head()
+{
+ atf_set descr "Non-first rule wrong"
+}
+
+non_first_rule_unparseable_body()
+{
+ sysctl_set_and_check_fails_rules "gid=1001>uid=0;hello"
+}
+
+
+atf_init_test_cases()
+{
+ . "$(atf_get_srcdir)"/common.sh
+
+ atf_add_test_case rule_no_target_part
+ atf_add_test_case rule_no_match_part
+ atf_add_test_case rule_space_between_flag_and_gid_fail
+ atf_add_test_case rule_user_names_fail
+ atf_add_test_case rule_group_names_fail
+ atf_add_test_case rules_wrong_separator
+ atf_add_test_case non_first_rule_unparseable
+}
diff --git a/tests/sys/mac/do/valid_configs.sh b/tests/sys/mac/do/valid_configs.sh
new file mode 100644
index 000000000000..fc1c9a370854
--- /dev/null
+++ b/tests/sys/mac/do/valid_configs.sh
@@ -0,0 +1,135 @@
+# Copyright (c) 2026 The FreeBSD Foundation
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# This software was developed by Olivier Certner <olce@FreeBSD.org> at
+# Kumacom SARL under sponsorship from the FreeBSD Foundation.
+
+atf_test_case rule_uid_to_any
+rule_uid_to_any_head()
+{
+ atf_set descr "Single \"to any\" rule"
+}
+rule_uid_to_any_body()
+{
+ sysctl_set_and_check_rules "uid=1001>any"
+ sysctl_set_and_check_rules "gid=1001>any"
+}
+
+atf_test_case rule_uid_to_uid
+rule_uid_to_uid_head()
+{
+ atf_set descr "Single \"to UID\" rule"
+}
+rule_uid_to_uid_body()
+{
+ sysctl_set_and_check_rules "uid=1001>uid=0"
+ sysctl_set_and_check_rules "gid=1001>uid=0"
+}
+
+atf_test_case rule_uid_to_uid_any
+rule_uid_to_uid_any_head()
+{
+ atf_set descr "Single \"to UID any\" rule"
+}
+rule_uid_to_uid_any_body()
+{
+ sysctl_set_and_check_rules "uid=1001>uid=any"
+ sysctl_set_and_check_rules "gid=1001>uid=any"
+}
+
+atf_test_case rule_uid_to_uid_star
+rule_uid_to_uid_star_head()
+{
+ atf_set descr "Single \"to any (with '*')\" rule"
+}
+rule_uid_to_uid_star_body()
+{
+ sysctl_set_and_check_rules "uid=1001>uid=*"
+ sysctl_set_and_check_rules "gid=1001>uid=*"
+}
+
+atf_test_case rule_uid_to_uid_gid
+rule_uid_to_uid_gid_head()
+{
+ atf_set descr "Single \"to UID and GID\" rule"
+}
+rule_uid_to_uid_gid_body()
+{
+ sysctl_set_and_check_rules "uid=1001>uid=0,gid=0"
+ sysctl_set_and_check_rules "gid=1001>uid=0,gid=0"
+}
+
+atf_test_case rule_uid_to_uid_gid_optional_sgid
+rule_uid_to_uid_gid_optional_sgid_head()
+{
+ atf_set descr "Single \"to UID, GID and \
+optional supplementary group rule\" rule"
+}
+rule_uid_to_uid_gid_optional_sgid_body()
+{
+ sysctl_set_and_check_rules "uid=1001>uid=0,gid=0,+gid=0"
+ sysctl_set_and_check_rules "gid=1001>uid=0,gid=0,+gid=0"
+}
+
+atf_test_case rule_uid_to_uid_gid_mandatory_sgid
+rule_uid_to_uid_gid_mandatory_sgid_head()
+{
+ atf_set descr "Single \"to UID, GID and \
+mandatory supplementary group\" rule"
+}
+rule_uid_to_uid_gid_mandatory_sgid_body()
+{
+ sysctl_set_and_check_rules "uid=1001>uid=0,gid=0,!gid=0"
+ sysctl_set_and_check_rules "gid=1001>uid=0,gid=0,!gid=0"
+}
+
+atf_test_case rule_uid_to_uid_gid_excluded_sgid
+rule_uid_to_uid_gid_excluded_sgid_head()
+{
+ atf_set descr "Single \"to UID, GID and excluded supplementary group\" rule"
+}
+rule_uid_to_uid_gid_excluded_sgid_body()
+{
+ sysctl_set_and_check_rules "uid=1001>uid=0,gid=0,-gid=0"
+ sysctl_set_and_check_rules "gid=1001>uid=0,gid=0,-gid=0"
+}
+
+atf_test_case rules_uid_to_uid
+rules_uid_to_uid_head()
+{
+ atf_set descr "Multiple \"to UID\" rules"
+}
+rules_uid_to_uid_body() {
+ sysctl_set_and_check_rules \
+ "uid=1001>uid=0;uid=1001>uid=0,gid=0,!gid=0,+gid=5;gid=1001>gid=5"
+}
+
+atf_test_case rules_uid_to_uid_with_spaces
+rules_uid_to_uid_with_spaces_head()
+{
+ atf_set descr "Multiple \"to UID\" rules with extra spaces"
+}
+rules_uid_to_uid_with_spaces_body()
+{
+ sysctl_set_and_check_rules \
+ "uid=1001 > uid=0; uid=1001>uid=0, gid = 0, !gid =0,+gid =5; \
+gid= 1001 >gid =5"
+}
+
+
+atf_init_test_cases()
+{
+ . "$(atf_get_srcdir)"/common.sh
+
+ atf_add_test_case rule_uid_to_any
+ atf_add_test_case rule_uid_to_uid
+ atf_add_test_case rule_uid_to_uid_any
+ atf_add_test_case rule_uid_to_uid_star
+ atf_add_test_case rule_uid_to_uid_gid
+ atf_add_test_case rule_uid_to_uid_gid_optional_sgid
+ atf_add_test_case rule_uid_to_uid_gid_mandatory_sgid
+ atf_add_test_case rule_uid_to_uid_gid_excluded_sgid
+ atf_add_test_case rules_uid_to_uid
+ atf_add_test_case rules_uid_to_uid_with_spaces
+}
diff --git a/tests/sys/net/Makefile b/tests/sys/net/Makefile
index 6dcc23b49b67..b1bd2c06f729 100644
--- a/tests/sys/net/Makefile
+++ b/tests/sys/net/Makefile
@@ -6,8 +6,6 @@ BINDIR= ${TESTSDIR}
ATF_TESTS_C+= if_epair
ATF_TESTS_SH+= if_epair_test
ATF_TESTS_SH+= if_bridge_test
-TEST_METADATA.if_bridge_test+= execenv="jail"
-TEST_METADATA.if_bridge_test+= execenv_jail_params="vnet allow.raw_sockets"
ATF_TESTS_SH+= if_clone_test
ATF_TESTS_SH+= if_gif
ATF_TESTS_SH+= if_lagg_test
@@ -16,6 +14,8 @@ ATF_TESTS_SH+= if_tun_test
ATF_TESTS_SH+= if_vlan
ATF_TESTS_SH+= if_wg
ATF_TESTS_SH+= if_geneve
+TEST_METADATA+= execenv="jail"
+TEST_METADATA+= execenv_jail_params="vnet allow.raw_sockets"
TESTS_SUBDIRS+= bpf
TESTS_SUBDIRS+= if_ovpn
@@ -26,11 +26,6 @@ TESTS_SUBDIRS+= routing
PROGS+= bridge
LIBADD.bridge+= netmap
-# The tests are written to be run in parallel, but doing so leads to random
-# panics. I think it's because the kernel's list of interfaces isn't properly
-# locked.
-TEST_METADATA+= is_exclusive=true
-
${PACKAGE}FILES+= \
dhclient_pcp.conf \
pcp.py \
diff --git a/tests/sys/net/routing/Makefile b/tests/sys/net/routing/Makefile
index c725d23f15d1..cbb00a236871 100644
--- a/tests/sys/net/routing/Makefile
+++ b/tests/sys/net/routing/Makefile
@@ -8,6 +8,7 @@ ATF_TESTS_C += test_rtsock_lladdr
ATF_TESTS_C += test_rtsock_ops
ATF_TESTS_PYTEST += test_routing_l3.py
ATF_TESTS_PYTEST += test_rtsock_multipath.py
+ATF_TESTS_SH+= test_routing
${PACKAGE}FILES+= generic_cleanup.sh
${PACKAGE}FILESMODE_generic_cleanup.sh=0555
@@ -15,6 +16,8 @@ ${PACKAGE}FILESMODE_generic_cleanup.sh=0555
# Most of the tests operates on a common IPv4/IPv6 prefix,
# so running them in parallel will lead to weird results.
TEST_METADATA+= is_exclusive=true
+TEST_METADATA.test_routing+= execenv="jail" \
+ execenv_jail_params="vnet"
CFLAGS+= -I${.CURDIR:H:H:H}
diff --git a/tests/sys/net/routing/test_routing.sh b/tests/sys/net/routing/test_routing.sh
new file mode 100755
index 000000000000..296bd7ebaffd
--- /dev/null
+++ b/tests/sys/net/routing/test_routing.sh
@@ -0,0 +1,231 @@
+#
+# Copyright (c) 2026 Pouria Mousavizadeh Tehrani <pouria@FreeBSD.org>
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+
+. $(atf_get_srcdir)/../../common/vnet.subr
+
+jq_rtentry()
+{
+ local route="$1"
+
+ jq -r '.statistics."route-information"."route-table"."rt-family".[]."rt-entry".[] |
+ select(.destination == "'${route}'")'
+}
+
+jq_nhop_filter()
+{
+ local nhop="$1"
+ local weight="$2"
+ local metric="$3"
+
+ jq -r 'select(.gateway == "'${nhop}'") |
+ select(.weight == '${weight}') |
+ select(.metric == '${metric}') |
+ .gateway'
+}
+
+
+atf_test_case "add_lowest_metric" "cleanup"
+add_lowest_metric_head()
+{
+ atf_set descr 'Create 4 routes to same dst and verify the lowest metric wins'
+ atf_set require.user root
+ atf_set require.progs jq
+}
+
+add_lowest_metric_body()
+{
+ local epair laddr route nhop1 nhop2 nhop3
+
+ laddr="3fff::1"
+ route="3fff:a::"
+ nhop1="3fff::1"
+ nhop2="3fff::2"
+ nhop3="3fff::3"
+
+ vnet_init
+ epair=$(vnet_mkepair)
+
+ atf_check -o ignore \
+ ifconfig ${epair}a inet6 ${laddr} up
+
+ # Create an ECMP route with metric 2
+ atf_check -o ignore \
+ route -6 add -net ${route}/64 -gateway ${nhop2} -weight 10 -metric 2
+ atf_check -o ignore \
+ route -6 add -net ${route}/64 -gateway ${nhop3} -weight 10 -metric 2
+
+ # Validate routes
+ atf_check -o save:netstat \
+ netstat -rn6 --libxo json
+ output=$(cat netstat | jq_rtentry ${route}/64 | jq_nhop_filter ${nhop2} 10 2)
+ atf_check_equal "$output" "$nhop2"
+ output=$(cat netstat | jq_rtentry ${route}/64 | jq_nhop_filter ${nhop3} 10 2)
+ atf_check_equal "$output" "$nhop3"
+
+ # Create a route with metric 3
+ atf_check -o ignore \
+ route -6 add -net ${route}/64 -gateway ${nhop1} -metric 3
+ # Verify that nhop1 is not the best route
+ atf_check -o not-match:".*gateway: ${nhop1}.*" \
+ route -n6 get -net ${route}/64
+
+ # Create a route to the same nhop with same metric 3 and verify it fails
+ atf_check -s exit:1 -o ignore -e match:".*exists.*" \
+ route -6 add -net ${route}/64 -gateway ${nhop1} -metric 3
+
+ # Create a route to an existing nhop with lower metric
+ atf_check -o ignore \
+ route -6 add -net ${route}/64 -gateway ${nhop1} -metric 1
+ # Verify that nhop1 is now the best route
+ atf_check -o match:".*gateway: ${nhop1}.*" \
+ route -n6 get -net ${route}/64
+}
+
+add_lowest_metric_cleanup()
+{
+ vnet_cleanup
+}
+
+atf_test_case "add_default_metric" "cleanup"
+add_default_metric_head()
+{
+ atf_set descr 'Create a route and verify the default metric is set'
+ atf_set require.user root
+ atf_set require.progs jq
+}
+
+add_default_metric_body()
+{
+ local epair laddr route nhop1
+
+ laddr="3fff::1"
+ route="3fff:a::"
+ nhop1="3fff::1"
+
+ vnet_init
+ epair=$(vnet_mkepair)
+
+ atf_check -o ignore \
+ ifconfig ${epair}a inet6 ${laddr} up
+
+ # Create a route without specifying its metric
+ atf_check -o ignore \
+ route -6 add -net ${route}/64 -gateway ${nhop1}
+
+ # Verify the route has the default metric of 1
+ atf_check -o save:netstat \
+ netstat -rn6 --libxo json
+ output=$(cat netstat | jq_rtentry ${route}/64 | jq_nhop_filter ${nhop1} 1 1)
+ atf_check_equal "$output" "$nhop1"
+}
+
+add_default_metric_cleanup()
+{
+ vnet_cleanup
+}
+
+atf_test_case "delete_route_with_metric" "cleanup"
+delete_route_with_metric_head()
+{
+ atf_set descr 'Create multiple routes to same dst and delete routes with specific metric'
+ atf_set require.user root
+ atf_set require.progs jq
+}
+
+delete_route_with_metric_body()
+{
+ local epair laddr route nhop1 nhop2
+
+ laddr="3fff::1"
+ route="3fff:a::"
+ nhop1="3fff::1"
+ nhop2="3fff::2"
+
+ vnet_init
+ epair=$(vnet_mkepair)
+
+ atf_check -o ignore \
+ ifconfig ${epair}a inet6 ${laddr} up
+
+ # Create two groups of ECMP routes with metric 2 and 3, and
+ # another route with metric 4.
+ atf_check -o ignore \
+ route -6 add -net ${route}/64 -gateway ${nhop1} -metric 3
+ atf_check -o ignore \
+ route -6 add -net ${route}/64 -gateway ${nhop1} -weight 10 -metric 2
+ atf_check -o ignore \
+ route -6 add -net ${route}/64 -gateway ${nhop2} -weight 10 -metric 2
+ atf_check -o ignore \
+ route -6 add -net ${route}/64 -gateway ${nhop2} -metric 3
+ atf_check -o ignore \
+ route -6 add -net ${route}/64 -gateway ${nhop2} -metric 4
+
+ # Validate we have 5 routes
+ atf_check -o save:netstat \
+ netstat -rn6 --libxo json
+ output=$(cat netstat | jq_rtentry ${route}/64 | jq_nhop_filter ${nhop1} 1 3)
+ atf_check_equal "$output" "$nhop1"
+ output=$(cat netstat | jq_rtentry ${route}/64 | jq_nhop_filter ${nhop1} 10 2)
+ atf_check_equal "$output" "$nhop1"
+ output=$(cat netstat | jq_rtentry ${route}/64 | jq_nhop_filter ${nhop2} 10 2)
+ atf_check_equal "$output" "$nhop2"
+ output=$(cat netstat | jq_rtentry ${route}/64 | jq_nhop_filter ${nhop2} 1 3)
+ atf_check_equal "$output" "$nhop2"
+ output=$(cat netstat | jq_rtentry ${route}/64 | jq_nhop_filter ${nhop2} 1 4)
+ atf_check_equal "$output" "$nhop2"
+
+ # Delete one of the nexthops of them best ECMP route
+ # Test that deleting a route by specifying gateway + metric works.
+ atf_check -o ignore \
+ route -n6 delete -net ${route}/64 -gateway ${nhop2} -metric 2
+
+ # Verify that nhop1 is the best route now
+ atf_check -o match:".*gateway: ${nhop1}.*" \
+ route -n6 get -net ${route}/64
+
+ # But other route with nhops2 should exists.
+ atf_check -o save:netstat \
+ netstat -rn6 --libxo json
+ output=$(cat netstat | jq_rtentry ${route}/64 | jq_nhop_filter ${nhop2} 1 3)
+ atf_check_equal "$output" "$nhop2"
+ output=$(cat netstat | jq_rtentry ${route}/64 | jq_nhop_filter ${nhop2} 1 4)
+ atf_check_equal "$output" "$nhop2"
+
+ # Delete routes with nhop1 as nexthop without specifying metric.
+ # Test that deleting a route by gateway removes all routes with
+ # that gateway, regardless of metric value.
+ atf_check -o ignore \
+ route -n6 delete -net ${route}/64 -gateway ${nhop1}
+
+ # Verify that nhop2 is the best route now
+ atf_check -o match:".*gateway: ${nhop2}.*" \
+ route -n6 get -net ${route}/64
+
+ # Delete routes with metric 3 without specifying their gateway.
+ # Test that deleting a route by metric removes all routes with
+ # that metric, regardless of gateway value.
+ atf_check -o ignore \
+ route -n6 delete -net ${route}/64 -metric 3
+
+ # Verify that nhop2 is still the best route with metric of 4
+ atf_check -o match:".*gateway: ${nhop2}.*" \
+ route -n6 get -net ${route}/64
+ output=$(cat netstat | jq_rtentry ${route}/64 | jq_nhop_filter ${nhop2} 1 4)
+ atf_check_equal "$output" "$nhop2"
+}
+
+delete_route_with_metric_cleanup()
+{
+ vnet_cleanup
+}
+
+
+atf_init_test_cases()
+{
+ atf_add_test_case "add_lowest_metric"
+ atf_add_test_case "add_default_metric"
+ atf_add_test_case "delete_route_with_metric"
+}
diff --git a/tests/sys/netinet/Makefile b/tests/sys/netinet/Makefile
index a13b0b42e2bc..d5bfdad0a812 100644
--- a/tests/sys/netinet/Makefile
+++ b/tests/sys/netinet/Makefile
@@ -40,6 +40,8 @@ LIBADD.udp_bindings= pthread
# Some of the arp tests look for log messages in the dmesg buffer, so run them
# serially to avoid problems with interleaved output.
TEST_METADATA.arp+= is_exclusive="true"
+TEST_METADATA.carp+= execenv="jail" \
+ execenv_jail_params="vnet allow.raw_sockets"
TEST_METADATA.divert+= required_programs="python" \
execenv="jail" \
execenv_jail_params="vnet allow.raw_sockets"
diff --git a/tests/sys/netinet6/ndp.sh b/tests/sys/netinet6/ndp.sh
index 35ea6655d922..636a5558b7a8 100755
--- a/tests/sys/netinet6/ndp.sh
+++ b/tests/sys/netinet6/ndp.sh
@@ -834,11 +834,16 @@ ndp_routeinfo_option_body() {
done
# Make sure routes from rti option are being installed
- atf_check -s exit:0 \
- -o match:"^${route1}/32[[:space:]]+${lladdr}.*1800" \
- -o match:"^${route2}/48[[:space:]]+${lladdr}.*600" \
- -o match:"^default[[:space:]]+${lladdr}" \
- jexec ${jname} netstat -rn6
+ atf_check -s exit:0 -o save:netstat_out netstat -j ${jname} -rn6
+ atf_check -s exit:0 -o match:"^default[[:space:]]+${lladdr}" \
+ cat netstat_out
+
+ # Ensure that route1's and route2's expiration times are correct
+ # respectively and do not get swapped
+ expire1="$(grep "^${route1}/32[[:space:]]*${lladdr}" netstat_out | cut -wf5)"
+ atf_check -s exit:0 test 601 -le ${expire1} -a ${expire1} -le 1800
+ expire2="$(grep "^${route2}/48[[:space:]]*${lladdr}" netstat_out | cut -wf5)"
+ atf_check -s exit:0 test ${expire2} -le 600
# Verify the default route lifetime and its preference is overwrited
atf_check -s exit:0 \
diff --git a/tests/sys/netipsec/tunnel/Makefile b/tests/sys/netipsec/tunnel/Makefile
index c6060a790cc3..49fddc403005 100644
--- a/tests/sys/netipsec/tunnel/Makefile
+++ b/tests/sys/netipsec/tunnel/Makefile
@@ -13,8 +13,8 @@ ATF_TESTS_SH+= empty \
aesni_aes_gcm_256 \
chacha20_poly1305
-# Each test uses the same names for its jails, so they must be run serially.
-TEST_METADATA+= is_exclusive=true
+TEST_METADATA+= execenv="jail" \
+ execenv_jail_params="vnet allow.raw_sockets"
${PACKAGE}FILES+= utils.subr
diff --git a/tests/sys/netpfil/common/nat.sh b/tests/sys/netpfil/common/nat.sh
index 023b0742ec6b..2b828dc03fdc 100644
--- a/tests/sys/netpfil/common/nat.sh
+++ b/tests/sys/netpfil/common/nat.sh
@@ -26,6 +26,8 @@
#
#
+set -e
+
. $(atf_get_srcdir)/utils.subr
. $(atf_get_srcdir)/runner.subr
@@ -178,13 +180,13 @@ common_cgn() {
atf_check -s exit:2 -o ignore jexec client1 ping -t 1 -c 1 198.51.100.2
atf_check -s exit:2 -o ignore jexec client2 ping -t 1 -c 1 198.51.100.2
- if [[ $portalias ]]; then
+ if [ ${portalias} = "true" ]; then
firewall_config nat $firewall \
"ipfw" \
- "ipfw -q nat 123 config if ${epair_host_nat}b unreg_cgn port_alias 2000-2999" \
- "ipfw -q nat 456 config if ${epair_host_nat}b unreg_cgn port_alias 3000-3999" \
- "ipfw -q add 1000 nat 123 all from any to 198.51.100.2 2000-2999 in via ${epair_host_nat}b" \
- "ipfw -q add 2000 nat 456 all from any to 198.51.100.2 3000-3999 in via ${epair_host_nat}b" \
+ "ipfw -q nat 123 config if ${epair_host_nat}b unreg_cgn port_range 2000-2999" \
+ "ipfw -q nat 456 config if ${epair_host_nat}b unreg_cgn port_range 3000-3999" \
+ "ipfw -q add 1000 nat 123 all from any to 198.51.100.0/24 2000-2999 in via ${epair_host_nat}b" \
+ "ipfw -q add 2000 nat 456 all from any to 198.51.100.0/24 3000-3999 in via ${epair_host_nat}b" \
"ipfw -q add 3000 nat 123 all from 100.64.0.2 to any out via ${epair_host_nat}b" \
"ipfw -q add 4000 nat 456 all from 100.64.1.2 to any out via ${epair_host_nat}b"
else
@@ -194,16 +196,16 @@ common_cgn() {
"ipfw -q add 1000 nat 123 all from any to any"
fi
- # ping is successful now
- atf_check -s exit:0 -o ignore jexec client1 ping -t 1 -c 1 198.51.100.2
- atf_check -s exit:0 -o ignore jexec client2 ping -t 1 -c 1 198.51.100.2
-
# if portalias, test a tcp server/client with nc
- if [[ $portalias ]]; then
+ if [ ${portalias} = "true" ]; then
for inst in 1 2; do
- daemon nc -p 198.51.100.2 7
- atf_check -s exit:0 -o ignore jexec client$inst sh -c "echo | nc -N 198.51.100.2 7"
+ daemon nc -l 198.51.100.2 7
+ atf_check -s exit:0 -o ignore -e ignore jexec client$inst nc -z 198.51.100.2 7
done
+ else
+ # ping is successful now
+ atf_check -s exit:0 -o ignore jexec client1 ping -t 1 -c 1 198.51.100.2
+ atf_check -s exit:0 -o ignore jexec client2 ping -t 1 -c 1 198.51.100.2
fi
}