diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/sys/acl/run | 2 | ||||
| -rw-r--r-- | tests/sys/acl/tools-posix.test | 14 | ||||
| -rw-r--r-- | tests/sys/capsicum/capmode.cc | 12 | ||||
| -rw-r--r-- | tests/sys/fs/fusefs/xattr.cc | 73 | ||||
| -rw-r--r-- | tests/sys/kern/Makefile | 3 | ||||
| -rw-r--r-- | tests/sys/kern/jaildesc.c | 201 | ||||
| -rw-r--r-- | tests/sys/kern/procdesc.c | 119 | ||||
| -rw-r--r-- | tests/sys/kern/ptrace_test.c | 93 | ||||
| -rw-r--r-- | tests/sys/mac/Makefile | 1 | ||||
| -rw-r--r-- | tests/sys/mac/do/Makefile | 13 | ||||
| -rw-r--r-- | tests/sys/mac/do/common.sh | 176 | ||||
| -rw-r--r-- | tests/sys/mac/do/consistency.sh | 211 | ||||
| -rw-r--r-- | tests/sys/mac/do/invalid_configs.sh | 100 | ||||
| -rw-r--r-- | tests/sys/mac/do/valid_configs.sh | 135 | ||||
| -rw-r--r-- | tests/sys/net/Makefile | 9 | ||||
| -rw-r--r-- | tests/sys/net/routing/Makefile | 3 | ||||
| -rwxr-xr-x | tests/sys/net/routing/test_routing.sh | 231 | ||||
| -rw-r--r-- | tests/sys/netinet/Makefile | 2 | ||||
| -rwxr-xr-x | tests/sys/netinet6/ndp.sh | 15 | ||||
| -rw-r--r-- | tests/sys/netipsec/tunnel/Makefile | 4 | ||||
| -rw-r--r-- | tests/sys/netpfil/common/nat.sh | 26 |
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 } |
