diff options
Diffstat (limited to 'tests/sys/kern')
-rw-r--r-- | tests/sys/kern/Makefile | 13 | ||||
-rw-r--r-- | tests/sys/kern/copy_file_range.c | 231 | ||||
-rw-r--r-- | tests/sys/kern/exterr_test.c | 108 | ||||
-rw-r--r-- | tests/sys/kern/getdirentries_test.c | 172 | ||||
-rw-r--r-- | tests/sys/kern/inotify_test.c | 864 | ||||
-rw-r--r-- | tests/sys/kern/jail_lookup_root.c | 133 | ||||
-rw-r--r-- | tests/sys/kern/ptrace_test.c | 141 | ||||
-rw-r--r-- | tests/sys/kern/socket_splice.c | 4 | ||||
-rw-r--r-- | tests/sys/kern/tty/Makefile | 3 | ||||
-rw-r--r-- | tests/sys/kern/tty/test_sti.c | 337 | ||||
-rw-r--r-- | tests/sys/kern/unix_passfd_test.c | 205 | ||||
-rw-r--r-- | tests/sys/kern/unix_seqpacket_test.c | 36 | ||||
-rw-r--r-- | tests/sys/kern/unix_stream.c | 381 |
13 files changed, 2586 insertions, 42 deletions
diff --git a/tests/sys/kern/Makefile b/tests/sys/kern/Makefile index 900c9a5b3bbe..9044b1e7e4f2 100644 --- a/tests/sys/kern/Makefile +++ b/tests/sys/kern/Makefile @@ -8,6 +8,7 @@ TESTSRC= ${SRCTOP}/contrib/netbsd-tests/kernel TESTSDIR= ${TESTSBASE}/sys/kern ATF_TESTS_C+= basic_signal +ATF_TESTS_C+= copy_file_range .if ${MACHINE_ARCH} != "i386" && ${MACHINE_ARCH} != "powerpc" && \ ${MACHINE_ARCH} != "powerpcspe" # No support for atomic_load_64 on i386 or (32-bit) powerpc @@ -15,7 +16,13 @@ ATF_TESTS_C+= kcov .endif ATF_TESTS_C+= kern_copyin ATF_TESTS_C+= kern_descrip_test +# One test modifies the maxfiles limit, which can cause spurious test failures. +TEST_METADATA.kern_descrip_test+= is_exclusive="true" +ATF_TESTS_C+= exterr_test ATF_TESTS_C+= fdgrowtable_test +ATF_TESTS_C+= getdirentries_test +ATF_TESTS_C+= jail_lookup_root +ATF_TESTS_C+= inotify_test ATF_TESTS_C+= kill_zombie .if ${MK_OPENSSL} != "no" ATF_TESTS_C+= ktls_test @@ -75,12 +82,15 @@ PROGS+= coredump_phnum_helper PROGS+= pdeathsig_helper PROGS+= sendfile_helper +LIBADD.copy_file_range+= md +LIBADD.jail_lookup_root+= jail util CFLAGS.sys_getrandom+= -I${SRCTOP}/sys/contrib/zstd/lib LIBADD.sys_getrandom+= zstd LIBADD.sys_getrandom+= c LIBADD.sys_getrandom+= pthread LIBADD.ptrace_test+= pthread LIBADD.unix_seqpacket_test+= pthread +LIBADD.inotify_test+= util LIBADD.kcov+= pthread CFLAGS.ktls_test+= -DOPENSSL_API_COMPAT=0x10100000L LIBADD.ktls_test+= crypto util @@ -92,6 +102,9 @@ LIBADD.sendfile_helper+= pthread LIBADD.fdgrowtable_test+= util pthread kvm procstat LIBADD.sigwait+= rt LIBADD.ktrace_test+= sysdecode +LIBADD.unix_passfd_dgram+= jail +LIBADD.unix_passfd_stream+= jail +LIBADD.unix_stream+= pthread NETBSD_ATF_TESTS_C+= lockf_test NETBSD_ATF_TESTS_C+= mqueue_test diff --git a/tests/sys/kern/copy_file_range.c b/tests/sys/kern/copy_file_range.c new file mode 100644 index 000000000000..ca52eaf668e3 --- /dev/null +++ b/tests/sys/kern/copy_file_range.c @@ -0,0 +1,231 @@ +/* + * Copyright (c) 2025 Mark Johnston <markj@FreeBSD.org> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include <sys/mman.h> +#include <sys/stat.h> + +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> + +#include <atf-c.h> +#include <sha256.h> + +/* + * Create a file with random data and size between 1B and 32MB. Return a file + * descriptor for the file. + */ +static int +genfile(void) +{ + char buf[256], file[NAME_MAX]; + size_t sz; + int fd; + + sz = (random() % (32 * 1024 * 1024ul)) + 1; + + snprintf(file, sizeof(file), "testfile.XXXXXX"); + fd = mkstemp(file); + ATF_REQUIRE(fd != -1); + + while (sz > 0) { + ssize_t n; + int error; + + error = getentropy(buf, sizeof(buf)); + ATF_REQUIRE(error == 0); + n = write(fd, buf, sizeof(buf) < sz ? sizeof(buf) : sz); + ATF_REQUIRE(n > 0); + + sz -= n; + } + + ATF_REQUIRE(lseek(fd, 0, SEEK_SET) == 0); + return (fd); +} + +/* + * Return true if the file data in the two file descriptors is the same, + * false otherwise. + */ +static bool +cmpfile(int fd1, int fd2) +{ + struct stat st1, st2; + void *addr1, *addr2; + size_t sz; + int res; + + ATF_REQUIRE(fstat(fd1, &st1) == 0); + ATF_REQUIRE(fstat(fd2, &st2) == 0); + if (st1.st_size != st2.st_size) + return (false); + + sz = st1.st_size; + addr1 = mmap(NULL, sz, PROT_READ, MAP_PRIVATE, fd1, 0); + ATF_REQUIRE(addr1 != MAP_FAILED); + addr2 = mmap(NULL, sz, PROT_READ, MAP_PRIVATE, fd2, 0); + ATF_REQUIRE(addr2 != MAP_FAILED); + + res = memcmp(addr1, addr2, sz); + + ATF_REQUIRE(munmap(addr1, sz) == 0); + ATF_REQUIRE(munmap(addr2, sz) == 0); + + return (res == 0); +} + +/* + * Exercise a few error paths in the copy_file_range() syscall. + */ +ATF_TC_WITHOUT_HEAD(copy_file_range_invalid); +ATF_TC_BODY(copy_file_range_invalid, tc) +{ + off_t off1, off2; + int fd1, fd2; + + fd1 = genfile(); + fd2 = genfile(); + + /* Can't copy a file to itself without explicit offsets. */ + ATF_REQUIRE_ERRNO(EINVAL, + copy_file_range(fd1, NULL, fd1, NULL, SSIZE_MAX, 0) == -1); + + /* When copying a file to itself, ranges cannot overlap. */ + off1 = off2 = 0; + ATF_REQUIRE_ERRNO(EINVAL, + copy_file_range(fd1, &off1, fd1, &off2, 1, 0) == -1); + + /* Negative offsets are not allowed. */ + off1 = -1; + off2 = 0; + ATF_REQUIRE_ERRNO(EINVAL, + copy_file_range(fd1, &off1, fd2, &off2, 42, 0) == -1); + ATF_REQUIRE_ERRNO(EINVAL, + copy_file_range(fd2, &off2, fd1, &off1, 42, 0) == -1); +} + +/* + * Make sure that copy_file_range() updates the file offsets passed to it. + */ +ATF_TC_WITHOUT_HEAD(copy_file_range_offset); +ATF_TC_BODY(copy_file_range_offset, tc) +{ + struct stat sb; + off_t off1, off2; + ssize_t n; + int fd1, fd2; + + off1 = off2 = 0; + + fd1 = genfile(); + fd2 = open("copy", O_RDWR | O_CREAT, 0644); + ATF_REQUIRE(fd2 != -1); + + ATF_REQUIRE(fstat(fd1, &sb) == 0); + + ATF_REQUIRE(lseek(fd1, 0, SEEK_CUR) == 0); + ATF_REQUIRE(lseek(fd2, 0, SEEK_CUR) == 0); + + do { + off_t ooff1, ooff2; + + ooff1 = off1; + ooff2 = off2; + n = copy_file_range(fd1, &off1, fd2, &off2, sb.st_size, 0); + ATF_REQUIRE(n >= 0); + ATF_REQUIRE_EQ(off1, ooff1 + n); + ATF_REQUIRE_EQ(off2, ooff2 + n); + } while (n != 0); + + /* Offsets should have been adjusted by copy_file_range(). */ + ATF_REQUIRE_EQ(off1, sb.st_size); + ATF_REQUIRE_EQ(off2, sb.st_size); + /* Seek offsets should have been left alone. */ + ATF_REQUIRE(lseek(fd1, 0, SEEK_CUR) == 0); + ATF_REQUIRE(lseek(fd2, 0, SEEK_CUR) == 0); + /* Make sure the file contents are the same. */ + ATF_REQUIRE_MSG(cmpfile(fd1, fd2), "file contents differ"); + + ATF_REQUIRE(close(fd1) == 0); + ATF_REQUIRE(close(fd2) == 0); +} + +/* + * Make sure that copying to a larger file doesn't cause it to be truncated. + */ +ATF_TC_WITHOUT_HEAD(copy_file_range_truncate); +ATF_TC_BODY(copy_file_range_truncate, tc) +{ + struct stat sb, sb1, sb2; + char digest1[65], digest2[65]; + off_t off; + ssize_t n; + int fd1, fd2; + + fd1 = genfile(); + fd2 = genfile(); + + ATF_REQUIRE(fstat(fd1, &sb1) == 0); + ATF_REQUIRE(fstat(fd2, &sb2) == 0); + + /* fd1 refers to the smaller file. */ + if (sb1.st_size > sb2.st_size) { + int tmp; + + tmp = fd1; + fd1 = fd2; + fd2 = tmp; + ATF_REQUIRE(fstat(fd1, &sb1) == 0); + ATF_REQUIRE(fstat(fd2, &sb2) == 0); + } + + /* + * Compute a hash of the bytes in the larger file which lie beyond the + * length of the smaller file. + */ + SHA256_FdChunk(fd2, digest1, sb1.st_size, sb2.st_size - sb1.st_size); + ATF_REQUIRE(lseek(fd2, 0, SEEK_SET) == 0); + + do { + n = copy_file_range(fd1, NULL, fd2, NULL, SSIZE_MAX, 0); + ATF_REQUIRE(n >= 0); + } while (n != 0); + + /* Validate file offsets after the copy. */ + off = lseek(fd1, 0, SEEK_CUR); + ATF_REQUIRE(off == sb1.st_size); + off = lseek(fd2, 0, SEEK_CUR); + ATF_REQUIRE(off == sb1.st_size); + + /* The larger file's size should remain the same. */ + ATF_REQUIRE(fstat(fd2, &sb) == 0); + ATF_REQUIRE(sb.st_size == sb2.st_size); + + /* The bytes beyond the end of the copy should be unchanged. */ + SHA256_FdChunk(fd2, digest2, sb1.st_size, sb2.st_size - sb1.st_size); + ATF_REQUIRE_MSG(strcmp(digest1, digest2) == 0, + "trailing file contents differ after copy_file_range()"); + + /* + * Verify that the copy actually replicated bytes from the smaller file. + */ + ATF_REQUIRE(ftruncate(fd2, sb1.st_size) == 0); + ATF_REQUIRE(cmpfile(fd1, fd2)); +} + +ATF_TP_ADD_TCS(tp) +{ + ATF_TP_ADD_TC(tp, copy_file_range_invalid); + ATF_TP_ADD_TC(tp, copy_file_range_offset); + ATF_TP_ADD_TC(tp, copy_file_range_truncate); + + return (atf_no_error()); +} diff --git a/tests/sys/kern/exterr_test.c b/tests/sys/kern/exterr_test.c new file mode 100644 index 000000000000..17c84c1f8ed4 --- /dev/null +++ b/tests/sys/kern/exterr_test.c @@ -0,0 +1,108 @@ +/*- + * Copyright (C) 2025 ConnectWise, LLC. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#include <sys/exterrvar.h> +#include <sys/mman.h> + +#include <atf-c.h> +#include <errno.h> +#include <exterr.h> +#include <stdio.h> + +ATF_TC(gettext_extended); +ATF_TC_HEAD(gettext_extended, tc) +{ + atf_tc_set_md_var(tc, "descr", "Retrieve an extended error message"); +} +ATF_TC_BODY(gettext_extended, tc) +{ + char exterr[UEXTERROR_MAXLEN]; + int r; + + /* + * Use an invalid call to mmap() because it supports extended error + * messages, requires no special resources, and does not need root. + */ + ATF_CHECK_ERRNO(ENOTSUP, + mmap(NULL, 0, PROT_MAX(PROT_READ) | PROT_WRITE, 0, -1, 0)); + r = uexterr_gettext(exterr, sizeof(exterr)); + ATF_CHECK_EQ(0, r); + printf("Extended error: %s\n", exterr); + /* Note: error string may need to be updated due to kernel changes */ + ATF_CHECK(strstr(exterr, "prot is not subset of max_prot") != 0); +} + +ATF_TC(gettext_noextended); +ATF_TC_HEAD(gettext_noextended, tc) +{ + atf_tc_set_md_var(tc, "descr", + "Fail to retrieve an extended error message because none exists"); +} +ATF_TC_BODY(gettext_noextended, tc) +{ + char exterr[UEXTERROR_MAXLEN]; + int r; + + ATF_CHECK_ERRNO(EINVAL, exterrctl(EXTERRCTL_UD, 0, NULL)); + r = uexterr_gettext(exterr, sizeof(exterr)); + ATF_CHECK_EQ(0, r); + ATF_CHECK_STREQ(exterr, ""); +} + +ATF_TC(gettext_noextended_after_extended); +ATF_TC_HEAD(gettext_noextended_after_extended, tc) +{ + atf_tc_set_md_var(tc, "descr", + "uexterr_gettext should not return a stale extended error message"); +} +ATF_TC_BODY(gettext_noextended_after_extended, tc) +{ + char exterr[UEXTERROR_MAXLEN]; + int r; + + /* + * First do something that will create an extended error message, but + * ignore it. + */ + ATF_CHECK_ERRNO(ENOTSUP, + mmap(NULL, 0, PROT_MAX(PROT_READ) | PROT_WRITE, 0, -1, 0)); + + /* Then do something that won't create an extended error message */ + ATF_CHECK_ERRNO(EINVAL, exterrctl(EXTERRCTL_UD, 0, NULL)); + + /* Hopefully we won't see the stale extended error message */ + r = uexterr_gettext(exterr, sizeof(exterr)); + ATF_CHECK_EQ(0, r); + ATF_CHECK_STREQ(exterr, ""); +} + +ATF_TP_ADD_TCS(tp) +{ + ATF_TP_ADD_TC(tp, gettext_extended); + ATF_TP_ADD_TC(tp, gettext_noextended); + ATF_TP_ADD_TC(tp, gettext_noextended_after_extended); + + return (atf_no_error()); +} diff --git a/tests/sys/kern/getdirentries_test.c b/tests/sys/kern/getdirentries_test.c new file mode 100644 index 000000000000..e66872ffe5b6 --- /dev/null +++ b/tests/sys/kern/getdirentries_test.c @@ -0,0 +1,172 @@ +/*- + * Copyright (c) 2025 Klara, Inc. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include <sys/stat.h> +#include <sys/mount.h> + +#include <dirent.h> +#include <fcntl.h> +#include <errno.h> +#include <stdint.h> + +#include <atf-c.h> + +ATF_TC(getdirentries_ok); +ATF_TC_HEAD(getdirentries_ok, tc) +{ + atf_tc_set_md_var(tc, "descr", "Successfully read a directory."); +} +ATF_TC_BODY(getdirentries_ok, tc) +{ + char dbuf[4096]; + struct dirent *d; + off_t base; + ssize_t ret; + int dd, n; + + ATF_REQUIRE_EQ(0, mkdir("dir", 0755)); + ATF_REQUIRE((dd = open("dir", O_DIRECTORY | O_RDONLY)) >= 0); + ATF_REQUIRE((ret = getdirentries(dd, dbuf, sizeof(dbuf), &base)) > 0); + ATF_REQUIRE_EQ(0, getdirentries(dd, dbuf, sizeof(dbuf), &base)); + ATF_REQUIRE_EQ(base, lseek(dd, 0, SEEK_CUR)); + ATF_CHECK_EQ(0, close(dd)); + for (n = 0, d = (struct dirent *)dbuf; + d < (struct dirent *)(dbuf + ret); + d = (struct dirent *)((char *)d + d->d_reclen), n++) + /* nothing */ ; + ATF_CHECK_EQ((struct dirent *)(dbuf + ret), d); + ATF_CHECK_EQ(2, n); +} + +ATF_TC(getdirentries_ebadf); +ATF_TC_HEAD(getdirentries_ebadf, tc) +{ + atf_tc_set_md_var(tc, "descr", "Attempt to read a directory " + "from an invalid descriptor."); +} +ATF_TC_BODY(getdirentries_ebadf, tc) +{ + char dbuf[4096]; + off_t base; + int fd; + + ATF_REQUIRE((fd = open("file", O_CREAT | O_WRONLY, 0644)) >= 0); + ATF_REQUIRE_EQ(-1, getdirentries(fd, dbuf, sizeof(dbuf), &base)); + ATF_CHECK_EQ(EBADF, errno); + ATF_REQUIRE_EQ(0, close(fd)); + ATF_REQUIRE_EQ(-1, getdirentries(fd, dbuf, sizeof(dbuf), &base)); + ATF_CHECK_EQ(EBADF, errno); +} + +ATF_TC(getdirentries_efault); +ATF_TC_HEAD(getdirentries_efault, tc) +{ + atf_tc_set_md_var(tc, "descr", "Attempt to read a directory " + "to an invalid buffer."); +} +ATF_TC_BODY(getdirentries_efault, tc) +{ + char dbuf[4096]; + off_t base, *basep; + int dd; + + ATF_REQUIRE_EQ(0, mkdir("dir", 0755)); + ATF_REQUIRE((dd = open("dir", O_DIRECTORY | O_RDONLY)) >= 0); + ATF_REQUIRE_EQ(-1, getdirentries(dd, NULL, sizeof(dbuf), &base)); + ATF_CHECK_EQ(EFAULT, errno); + basep = NULL; + basep++; + ATF_REQUIRE_EQ(-1, getdirentries(dd, dbuf, sizeof(dbuf), basep)); + ATF_CHECK_EQ(EFAULT, errno); + ATF_CHECK_EQ(0, close(dd)); +} + +ATF_TC(getdirentries_einval); +ATF_TC_HEAD(getdirentries_einval, tc) +{ + atf_tc_set_md_var(tc, "descr", "Attempt to read a directory " + "with various invalid parameters."); +} +ATF_TC_BODY(getdirentries_einval, tc) +{ + struct statfs fsb; + char dbuf[4096]; + off_t base; + ssize_t ret; + int dd; + + ATF_REQUIRE_EQ(0, mkdir("dir", 0755)); + ATF_REQUIRE((dd = open("dir", O_DIRECTORY | O_RDONLY)) >= 0); + ATF_REQUIRE_EQ(0, fstatfs(dd, &fsb)); + /* nbytes too small */ + ATF_REQUIRE_EQ(-1, getdirentries(dd, dbuf, 8, &base)); + ATF_CHECK_EQ(EINVAL, errno); + /* nbytes too big */ + ATF_REQUIRE_EQ(-1, getdirentries(dd, dbuf, SIZE_MAX, &base)); + ATF_CHECK_EQ(EINVAL, errno); + /* invalid position */ + ATF_REQUIRE((ret = getdirentries(dd, dbuf, sizeof(dbuf), &base)) > 0); + ATF_REQUIRE_EQ(0, getdirentries(dd, dbuf, sizeof(dbuf), &base)); + ATF_REQUIRE(base > 0); + ATF_REQUIRE_EQ(base + 3, lseek(dd, 3, SEEK_CUR)); + /* known to fail on ufs (FFS2) and zfs, and work on tmpfs */ + if (strcmp(fsb.f_fstypename, "ufs") == 0 || + strcmp(fsb.f_fstypename, "zfs") == 0) { + atf_tc_expect_fail("incorrectly returns 0 instead of EINVAL " + "on %s", fsb.f_fstypename); + } + ATF_REQUIRE_EQ(-1, getdirentries(dd, dbuf, sizeof(dbuf), &base)); + ATF_CHECK_EQ(EINVAL, errno); + ATF_CHECK_EQ(0, close(dd)); +} + +ATF_TC(getdirentries_enoent); +ATF_TC_HEAD(getdirentries_enoent, tc) +{ + atf_tc_set_md_var(tc, "descr", "Attempt to read a directory " + "after it is deleted."); +} +ATF_TC_BODY(getdirentries_enoent, tc) +{ + char dbuf[4096]; + off_t base; + int dd; + + ATF_REQUIRE_EQ(0, mkdir("dir", 0755)); + ATF_REQUIRE((dd = open("dir", O_DIRECTORY | O_RDONLY)) >= 0); + ATF_REQUIRE_EQ(0, rmdir("dir")); + ATF_REQUIRE_EQ(-1, getdirentries(dd, dbuf, sizeof(dbuf), &base)); + ATF_CHECK_EQ(ENOENT, errno); +} + +ATF_TC(getdirentries_enotdir); +ATF_TC_HEAD(getdirentries_enotdir, tc) +{ + atf_tc_set_md_var(tc, "descr", "Attempt to read a directory " + "from a descriptor not associated with a directory."); +} +ATF_TC_BODY(getdirentries_enotdir, tc) +{ + char dbuf[4096]; + off_t base; + int fd; + + ATF_REQUIRE((fd = open("file", O_CREAT | O_RDWR, 0644)) >= 0); + ATF_REQUIRE_EQ(-1, getdirentries(fd, dbuf, sizeof(dbuf), &base)); + ATF_CHECK_EQ(ENOTDIR, errno); + ATF_CHECK_EQ(0, close(fd)); +} + +ATF_TP_ADD_TCS(tp) +{ + ATF_TP_ADD_TC(tp, getdirentries_ok); + ATF_TP_ADD_TC(tp, getdirentries_ebadf); + ATF_TP_ADD_TC(tp, getdirentries_efault); + ATF_TP_ADD_TC(tp, getdirentries_einval); + ATF_TP_ADD_TC(tp, getdirentries_enoent); + ATF_TP_ADD_TC(tp, getdirentries_enotdir); + return (atf_no_error()); +} diff --git a/tests/sys/kern/inotify_test.c b/tests/sys/kern/inotify_test.c new file mode 100644 index 000000000000..713db55afc22 --- /dev/null +++ b/tests/sys/kern/inotify_test.c @@ -0,0 +1,864 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2025 Klara, Inc. + */ + +#include <sys/capsicum.h> +#include <sys/filio.h> +#include <sys/inotify.h> +#include <sys/ioccom.h> +#include <sys/mount.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/sysctl.h> +#include <sys/un.h> + +#include <dirent.h> +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#include <mntopts.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include <atf-c.h> + +static const char * +ev2name(int event) +{ + switch (event) { + case IN_ACCESS: + return ("IN_ACCESS"); + case IN_ATTRIB: + return ("IN_ATTRIB"); + case IN_CLOSE_WRITE: + return ("IN_CLOSE_WRITE"); + case IN_CLOSE_NOWRITE: + return ("IN_CLOSE_NOWRITE"); + case IN_CREATE: + return ("IN_CREATE"); + case IN_DELETE: + return ("IN_DELETE"); + case IN_DELETE_SELF: + return ("IN_DELETE_SELF"); + case IN_MODIFY: + return ("IN_MODIFY"); + case IN_MOVE_SELF: + return ("IN_MOVE_SELF"); + case IN_MOVED_FROM: + return ("IN_MOVED_FROM"); + case IN_MOVED_TO: + return ("IN_MOVED_TO"); + case IN_OPEN: + return ("IN_OPEN"); + default: + return (NULL); + } +} + +static void +close_checked(int fd) +{ + ATF_REQUIRE(close(fd) == 0); +} + +/* + * Make sure that no other events are pending, and close the inotify descriptor. + */ +static void +close_inotify(int fd) +{ + int n; + + ATF_REQUIRE(ioctl(fd, FIONREAD, &n) == 0); + ATF_REQUIRE(n == 0); + close_checked(fd); +} + +static uint32_t +consume_event_cookie(int ifd, int wd, unsigned int event, unsigned int flags, + const char *name) +{ + struct inotify_event *ev; + size_t evsz, namelen; + ssize_t n; + uint32_t cookie; + + /* Only read one record. */ + namelen = name == NULL ? 0 : strlen(name); + evsz = sizeof(*ev) + _IN_NAMESIZE(namelen); + ev = malloc(evsz); + ATF_REQUIRE(ev != NULL); + + n = read(ifd, ev, evsz); + ATF_REQUIRE_MSG(n >= 0, "failed to read event %s", ev2name(event)); + ATF_REQUIRE((size_t)n >= sizeof(*ev)); + ATF_REQUIRE((size_t)n == sizeof(*ev) + ev->len); + ATF_REQUIRE((size_t)n == evsz); + + ATF_REQUIRE_MSG((ev->mask & IN_ALL_EVENTS) == event, + "expected event %#x, got %#x", event, ev->mask); + ATF_REQUIRE_MSG((ev->mask & _IN_ALL_RETFLAGS) == flags, + "expected flags %#x, got %#x", flags, ev->mask); + ATF_REQUIRE_MSG(ev->wd == wd, + "expected wd %d, got %d", wd, ev->wd); + ATF_REQUIRE_MSG(name == NULL || strcmp(name, ev->name) == 0, + "expected name '%s', got '%s'", name, ev->name); + cookie = ev->cookie; + if ((ev->mask & (IN_MOVED_FROM | IN_MOVED_TO)) == 0) + ATF_REQUIRE(cookie == 0); + free(ev); + return (cookie); +} + +/* + * Read an event from the inotify file descriptor and check that it + * matches the expected values. + */ +static void +consume_event(int ifd, int wd, unsigned int event, unsigned int flags, + const char *name) +{ + (void)consume_event_cookie(ifd, wd, event, flags, name); +} + +static int +inotify(int flags) +{ + int ifd; + + ifd = inotify_init1(flags); + ATF_REQUIRE(ifd != -1); + return (ifd); +} + +static void +mount_nullfs(char *dir, char *src) +{ + struct iovec *iov; + char errmsg[1024]; + int error, iovlen; + + iov = NULL; + iovlen = 0; + + build_iovec(&iov, &iovlen, "fstype", "nullfs", (size_t)-1); + build_iovec(&iov, &iovlen, "fspath", dir, (size_t)-1); + build_iovec(&iov, &iovlen, "target", src, (size_t)-1); + build_iovec(&iov, &iovlen, "errmsg", errmsg, sizeof(errmsg)); + + errmsg[0] = '\0'; + error = nmount(iov, iovlen, 0); + ATF_REQUIRE_MSG(error == 0, + "mount nullfs %s %s: %s", src, dir, + errmsg[0] == '\0' ? strerror(errno) : errmsg); + + free_iovec(&iov, &iovlen); +} + +static void +mount_tmpfs(const char *dir) +{ + struct iovec *iov; + char errmsg[1024]; + int error, iovlen; + + iov = NULL; + iovlen = 0; + + build_iovec(&iov, &iovlen, "fstype", "tmpfs", (size_t)-1); + build_iovec(&iov, &iovlen, "fspath", __DECONST(char *, dir), + (size_t)-1); + build_iovec(&iov, &iovlen, "errmsg", errmsg, sizeof(errmsg)); + + errmsg[0] = '\0'; + error = nmount(iov, iovlen, 0); + ATF_REQUIRE_MSG(error == 0, + "mount tmpfs %s: %s", dir, + errmsg[0] == '\0' ? strerror(errno) : errmsg); + + free_iovec(&iov, &iovlen); +} + +static int +watch_file(int ifd, int events, char *path) +{ + int fd, wd; + + strncpy(path, "test.XXXXXX", PATH_MAX); + fd = mkstemp(path); + ATF_REQUIRE(fd != -1); + close_checked(fd); + + wd = inotify_add_watch(ifd, path, events); + ATF_REQUIRE(wd != -1); + + return (wd); +} + +static int +watch_dir(int ifd, int events, char *path) +{ + char *p; + int wd; + + strlcpy(path, "test.XXXXXX", PATH_MAX); + p = mkdtemp(path); + ATF_REQUIRE(p == path); + + wd = inotify_add_watch(ifd, path, events); + ATF_REQUIRE(wd != -1); + + return (wd); +} + +/* + * Verify that Capsicum restrictions are applied as expected. + */ +ATF_TC_WITHOUT_HEAD(inotify_capsicum); +ATF_TC_BODY(inotify_capsicum, tc) +{ + int error, dfd, ifd, wd; + + ifd = inotify(IN_NONBLOCK); + ATF_REQUIRE(ifd != -1); + + dfd = open(".", O_RDONLY | O_DIRECTORY); + ATF_REQUIRE(dfd != -1); + + error = mkdirat(dfd, "testdir", 0755); + ATF_REQUIRE(error == 0); + + error = cap_enter(); + ATF_REQUIRE(error == 0); + + /* + * Plain inotify_add_watch() is disallowed. + */ + wd = inotify_add_watch(ifd, ".", IN_DELETE_SELF); + ATF_REQUIRE_ERRNO(ECAPMODE, wd == -1); + wd = inotify_add_watch_at(ifd, dfd, "testdir", IN_DELETE_SELF); + ATF_REQUIRE(wd >= 0); + + /* + * Generate a record and consume it. + */ + error = unlinkat(dfd, "testdir", AT_REMOVEDIR); + ATF_REQUIRE(error == 0); + consume_event(ifd, wd, IN_DELETE_SELF, IN_ISDIR, NULL); + consume_event(ifd, wd, 0, IN_IGNORED, NULL); + + close_checked(dfd); + close_inotify(ifd); +} + +/* + * Make sure that duplicate, back-to-back events are coalesced. + */ +ATF_TC_WITHOUT_HEAD(inotify_coalesce); +ATF_TC_BODY(inotify_coalesce, tc) +{ + char file[PATH_MAX], path[PATH_MAX]; + int fd, fd1, ifd, n, wd; + + ifd = inotify(IN_NONBLOCK); + + /* Create a directory and watch it. */ + wd = watch_dir(ifd, IN_OPEN, path); + /* Create a file in the directory and open it. */ + snprintf(file, sizeof(file), "%s/file", path); + fd = open(file, O_RDWR | O_CREAT, 0644); + ATF_REQUIRE(fd != -1); + close_checked(fd); + fd = open(file, O_RDWR); + ATF_REQUIRE(fd != -1); + fd1 = open(file, O_RDONLY); + ATF_REQUIRE(fd1 != -1); + close_checked(fd1); + close_checked(fd); + + consume_event(ifd, wd, IN_OPEN, 0, "file"); + ATF_REQUIRE(ioctl(ifd, FIONREAD, &n) == 0); + ATF_REQUIRE(n == 0); + + close_inotify(ifd); +} + +/* + * Check handling of IN_MASK_CREATE. + */ +ATF_TC_WITHOUT_HEAD(inotify_mask_create); +ATF_TC_BODY(inotify_mask_create, tc) +{ + char path[PATH_MAX]; + int ifd, wd, wd1; + + ifd = inotify(IN_NONBLOCK); + + /* Create a directory and watch it. */ + wd = watch_dir(ifd, IN_CREATE, path); + /* Updating the watch with IN_MASK_CREATE should result in an error. */ + wd1 = inotify_add_watch(ifd, path, IN_MODIFY | IN_MASK_CREATE); + ATF_REQUIRE_ERRNO(EEXIST, wd1 == -1); + /* It's an error to specify IN_MASK_ADD with IN_MASK_CREATE. */ + wd1 = inotify_add_watch(ifd, path, IN_MODIFY | IN_MASK_ADD | + IN_MASK_CREATE); + ATF_REQUIRE_ERRNO(EINVAL, wd1 == -1); + /* Updating the watch without IN_MASK_CREATE should work. */ + wd1 = inotify_add_watch(ifd, path, IN_MODIFY); + ATF_REQUIRE(wd1 != -1); + ATF_REQUIRE_EQ(wd, wd1); + + close_inotify(ifd); +} + +/* + * Make sure that inotify cooperates with nullfs: if a lower vnode is the + * subject of an event, the upper vnode should be notified, and if the upper + * vnode is the subject of an event, the lower vnode should be notified. + */ +ATF_TC_WITH_CLEANUP(inotify_nullfs); +ATF_TC_HEAD(inotify_nullfs, tc) +{ + atf_tc_set_md_var(tc, "require.user", "root"); +} +ATF_TC_BODY(inotify_nullfs, tc) +{ + char path[PATH_MAX], *p; + int dfd, error, fd, ifd, mask, wd; + + mask = IN_CREATE | IN_OPEN; + + ifd = inotify(IN_NONBLOCK); + + strlcpy(path, "./test.XXXXXX", sizeof(path)); + p = mkdtemp(path); + ATF_REQUIRE(p == path); + + error = mkdir("./mnt", 0755); + ATF_REQUIRE(error == 0); + + /* Mount the testdir onto ./mnt. */ + mount_nullfs("./mnt", path); + + wd = inotify_add_watch(ifd, "./mnt", mask); + ATF_REQUIRE(wd != -1); + + /* Create a file in the lower directory and open it. */ + dfd = open(path, O_RDONLY | O_DIRECTORY); + ATF_REQUIRE(dfd != -1); + fd = openat(dfd, "file", O_RDWR | O_CREAT, 0644); + close_checked(fd); + close_checked(dfd); + + /* We should see events via the nullfs mount. */ + consume_event(ifd, wd, IN_OPEN, IN_ISDIR, NULL); + consume_event(ifd, wd, IN_CREATE, 0, "file"); + consume_event(ifd, wd, IN_OPEN, 0, "file"); + + error = inotify_rm_watch(ifd, wd); + ATF_REQUIRE(error == 0); + consume_event(ifd, wd, 0, IN_IGNORED, NULL); + + /* Watch the lower directory. */ + wd = inotify_add_watch(ifd, path, mask); + ATF_REQUIRE(wd != -1); + /* ... and create a file in the upper directory and open it. */ + dfd = open("./mnt", O_RDONLY | O_DIRECTORY); + ATF_REQUIRE(dfd != -1); + fd = openat(dfd, "file2", O_RDWR | O_CREAT, 0644); + ATF_REQUIRE(fd != -1); + close_checked(fd); + close_checked(dfd); + + /* We should see events via the lower directory. */ + consume_event(ifd, wd, IN_OPEN, IN_ISDIR, NULL); + consume_event(ifd, wd, IN_CREATE, 0, "file2"); + consume_event(ifd, wd, IN_OPEN, 0, "file2"); + + close_inotify(ifd); +} +ATF_TC_CLEANUP(inotify_nullfs, tc) +{ + int error; + + error = unmount("./mnt", 0); + if (error != 0) { + perror("unmount"); + exit(1); + } +} + +/* + * Make sure that exceeding max_events pending events results in an overflow + * event. + */ +ATF_TC_WITHOUT_HEAD(inotify_queue_overflow); +ATF_TC_BODY(inotify_queue_overflow, tc) +{ + char path[PATH_MAX]; + size_t size; + int error, dfd, ifd, max, wd; + + size = sizeof(max); + error = sysctlbyname("vfs.inotify.max_queued_events", &max, &size, NULL, + 0); + ATF_REQUIRE(error == 0); + + ifd = inotify(IN_NONBLOCK); + + /* Create a directory and watch it for file creation events. */ + wd = watch_dir(ifd, IN_CREATE, path); + dfd = open(path, O_DIRECTORY); + ATF_REQUIRE(dfd != -1); + /* Generate max+1 file creation events. */ + for (int i = 0; i < max + 1; i++) { + char name[NAME_MAX]; + int fd; + + (void)snprintf(name, sizeof(name), "file%d", i); + fd = openat(dfd, name, O_CREAT | O_RDWR, 0644); + ATF_REQUIRE(fd != -1); + close_checked(fd); + } + + /* + * Read our events. We should see files 0..max-1 and then an overflow + * event. + */ + for (int i = 0; i < max; i++) { + char name[NAME_MAX]; + + (void)snprintf(name, sizeof(name), "file%d", i); + consume_event(ifd, wd, IN_CREATE, 0, name); + } + + /* Look for an overflow event. */ + consume_event(ifd, -1, 0, IN_Q_OVERFLOW, NULL); + + close_checked(dfd); + close_inotify(ifd); +} + +ATF_TC_WITHOUT_HEAD(inotify_event_access_file); +ATF_TC_BODY(inotify_event_access_file, tc) +{ + char path[PATH_MAX], buf[16]; + off_t nb; + ssize_t n; + int error, fd, fd1, ifd, s[2], wd; + + ifd = inotify(IN_NONBLOCK); + + wd = watch_file(ifd, IN_ACCESS, path); + + fd = open(path, O_RDWR); + n = write(fd, "test", 4); + ATF_REQUIRE(n == 4); + + /* A simple read(2) should generate an access. */ + ATF_REQUIRE(lseek(fd, 0, SEEK_SET) == 0); + n = read(fd, buf, sizeof(buf)); + ATF_REQUIRE(n == 4); + ATF_REQUIRE(memcmp(buf, "test", 4) == 0); + consume_event(ifd, wd, IN_ACCESS, 0, NULL); + + /* copy_file_range(2) should as well. */ + ATF_REQUIRE(lseek(fd, 0, SEEK_SET) == 0); + fd1 = open("sink", O_RDWR | O_CREAT, 0644); + ATF_REQUIRE(fd1 != -1); + n = copy_file_range(fd, NULL, fd1, NULL, 4, 0); + ATF_REQUIRE(n == 4); + close_checked(fd1); + consume_event(ifd, wd, IN_ACCESS, 0, NULL); + + /* As should sendfile(2). */ + error = socketpair(AF_UNIX, SOCK_STREAM, 0, s); + ATF_REQUIRE(error == 0); + error = sendfile(fd, s[0], 0, 4, NULL, &nb, 0); + ATF_REQUIRE(error == 0); + ATF_REQUIRE(nb == 4); + consume_event(ifd, wd, IN_ACCESS, 0, NULL); + close_checked(s[0]); + close_checked(s[1]); + + close_checked(fd); + + close_inotify(ifd); +} + +ATF_TC_WITHOUT_HEAD(inotify_event_access_dir); +ATF_TC_BODY(inotify_event_access_dir, tc) +{ + char root[PATH_MAX], path[PATH_MAX]; + struct dirent *ent; + DIR *dir; + int error, ifd, wd; + + ifd = inotify(IN_NONBLOCK); + + wd = watch_dir(ifd, IN_ACCESS, root); + snprintf(path, sizeof(path), "%s/dir", root); + error = mkdir(path, 0755); + ATF_REQUIRE(error == 0); + + /* Read an entry and generate an access. */ + dir = opendir(path); + ATF_REQUIRE(dir != NULL); + ent = readdir(dir); + ATF_REQUIRE(ent != NULL); + ATF_REQUIRE(strcmp(ent->d_name, ".") == 0 || + strcmp(ent->d_name, "..") == 0); + ATF_REQUIRE(closedir(dir) == 0); + consume_event(ifd, wd, IN_ACCESS, IN_ISDIR, "dir"); + + /* + * Reading the watched directory should generate an access event. + * This is contrary to Linux's inotify man page, which states that + * IN_ACCESS is only generated for accesses to objects in a watched + * directory. + */ + dir = opendir(root); + ATF_REQUIRE(dir != NULL); + ent = readdir(dir); + ATF_REQUIRE(ent != NULL); + ATF_REQUIRE(strcmp(ent->d_name, ".") == 0 || + strcmp(ent->d_name, "..") == 0); + ATF_REQUIRE(closedir(dir) == 0); + consume_event(ifd, wd, IN_ACCESS, IN_ISDIR, NULL); + + close_inotify(ifd); +} + +ATF_TC_WITHOUT_HEAD(inotify_event_attrib); +ATF_TC_BODY(inotify_event_attrib, tc) +{ + char path[PATH_MAX]; + int error, ifd, fd, wd; + + ifd = inotify(IN_NONBLOCK); + + wd = watch_file(ifd, IN_ATTRIB, path); + + fd = open(path, O_RDWR); + ATF_REQUIRE(fd != -1); + error = fchmod(fd, 0600); + ATF_REQUIRE(error == 0); + consume_event(ifd, wd, IN_ATTRIB, 0, NULL); + + error = fchown(fd, getuid(), getgid()); + ATF_REQUIRE(error == 0); + consume_event(ifd, wd, IN_ATTRIB, 0, NULL); + + close_checked(fd); + close_inotify(ifd); +} + +ATF_TC_WITHOUT_HEAD(inotify_event_close_nowrite); +ATF_TC_BODY(inotify_event_close_nowrite, tc) +{ + char file[PATH_MAX], file1[PATH_MAX], dir[PATH_MAX]; + int ifd, fd, wd1, wd2; + + ifd = inotify(IN_NONBLOCK); + + wd1 = watch_dir(ifd, IN_CLOSE_NOWRITE, dir); + wd2 = watch_file(ifd, IN_CLOSE_NOWRITE | IN_CLOSE_WRITE, file); + + fd = open(dir, O_DIRECTORY); + ATF_REQUIRE(fd != -1); + close_checked(fd); + consume_event(ifd, wd1, IN_CLOSE_NOWRITE, IN_ISDIR, NULL); + + fd = open(file, O_RDONLY); + ATF_REQUIRE(fd != -1); + close_checked(fd); + consume_event(ifd, wd2, IN_CLOSE_NOWRITE, 0, NULL); + + snprintf(file1, sizeof(file1), "%s/file", dir); + fd = open(file1, O_RDONLY | O_CREAT, 0644); + ATF_REQUIRE(fd != -1); + close_checked(fd); + consume_event(ifd, wd1, IN_CLOSE_NOWRITE, 0, "file"); + + close_inotify(ifd); +} + +ATF_TC_WITHOUT_HEAD(inotify_event_close_write); +ATF_TC_BODY(inotify_event_close_write, tc) +{ + char path[PATH_MAX]; + int ifd, fd, wd; + + ifd = inotify(IN_NONBLOCK); + + wd = watch_file(ifd, IN_CLOSE_NOWRITE | IN_CLOSE_WRITE, path); + + fd = open(path, O_RDWR); + ATF_REQUIRE(fd != -1); + close_checked(fd); + consume_event(ifd, wd, IN_CLOSE_WRITE, 0, NULL); + + close_inotify(ifd); +} + +/* Verify that various operations in a directory generate IN_CREATE events. */ +ATF_TC_WITHOUT_HEAD(inotify_event_create); +ATF_TC_BODY(inotify_event_create, tc) +{ + struct sockaddr_un sun; + char path[PATH_MAX], path1[PATH_MAX], root[PATH_MAX]; + ssize_t n; + int error, ifd, ifd1, fd, s, wd, wd1; + char b; + + ifd = inotify(IN_NONBLOCK); + + wd = watch_dir(ifd, IN_CREATE, root); + + /* Regular file. */ + snprintf(path, sizeof(path), "%s/file", root); + fd = open(path, O_RDWR | O_CREAT, 0644); + ATF_REQUIRE(fd != -1); + /* + * Make sure we get an event triggered by the fd used to create the + * file. + */ + ifd1 = inotify(IN_NONBLOCK); + wd1 = inotify_add_watch(ifd1, root, IN_MODIFY); + b = 42; + n = write(fd, &b, sizeof(b)); + ATF_REQUIRE(n == sizeof(b)); + close_checked(fd); + consume_event(ifd, wd, IN_CREATE, 0, "file"); + consume_event(ifd1, wd1, IN_MODIFY, 0, "file"); + close_inotify(ifd1); + + /* Hard link. */ + snprintf(path1, sizeof(path1), "%s/link", root); + error = link(path, path1); + ATF_REQUIRE(error == 0); + consume_event(ifd, wd, IN_CREATE, 0, "link"); + + /* Directory. */ + snprintf(path, sizeof(path), "%s/dir", root); + error = mkdir(path, 0755); + ATF_REQUIRE(error == 0); + consume_event(ifd, wd, IN_CREATE, IN_ISDIR, "dir"); + + /* Symbolic link. */ + snprintf(path1, sizeof(path1), "%s/symlink", root); + error = symlink(path, path1); + ATF_REQUIRE(error == 0); + consume_event(ifd, wd, IN_CREATE, 0, "symlink"); + + /* FIFO. */ + snprintf(path, sizeof(path), "%s/fifo", root); + error = mkfifo(path, 0644); + ATF_REQUIRE(error == 0); + consume_event(ifd, wd, IN_CREATE, 0, "fifo"); + + /* Binding a socket. */ + s = socket(AF_UNIX, SOCK_STREAM, 0); + memset(&sun, 0, sizeof(sun)); + sun.sun_family = AF_UNIX; + sun.sun_len = sizeof(sun); + snprintf(sun.sun_path, sizeof(sun.sun_path), "%s/socket", root); + error = bind(s, (struct sockaddr *)&sun, sizeof(sun)); + ATF_REQUIRE(error == 0); + close_checked(s); + consume_event(ifd, wd, IN_CREATE, 0, "socket"); + + close_inotify(ifd); +} + +ATF_TC_WITHOUT_HEAD(inotify_event_delete); +ATF_TC_BODY(inotify_event_delete, tc) +{ + char root[PATH_MAX], path[PATH_MAX], file[PATH_MAX]; + int error, fd, ifd, wd, wd2; + + ifd = inotify(IN_NONBLOCK); + + wd = watch_dir(ifd, IN_DELETE | IN_DELETE_SELF, root); + + snprintf(path, sizeof(path), "%s/file", root); + fd = open(path, O_RDWR | O_CREAT, 0644); + ATF_REQUIRE(fd != -1); + error = unlink(path); + ATF_REQUIRE(error == 0); + consume_event(ifd, wd, IN_DELETE, 0, "file"); + close_checked(fd); + + /* + * Make sure that renaming over a file generates a delete event when and + * only when that file is watched. + */ + fd = open(path, O_RDWR | O_CREAT, 0644); + ATF_REQUIRE(fd != -1); + close_checked(fd); + wd2 = inotify_add_watch(ifd, path, IN_DELETE | IN_DELETE_SELF); + ATF_REQUIRE(wd2 != -1); + snprintf(file, sizeof(file), "%s/file2", root); + fd = open(file, O_RDWR | O_CREAT, 0644); + ATF_REQUIRE(fd != -1); + close_checked(fd); + error = rename(file, path); + ATF_REQUIRE(error == 0); + consume_event(ifd, wd2, IN_DELETE_SELF, 0, NULL); + consume_event(ifd, wd2, 0, IN_IGNORED, NULL); + + error = unlink(path); + ATF_REQUIRE(error == 0); + consume_event(ifd, wd, IN_DELETE, 0, "file"); + error = rmdir(root); + ATF_REQUIRE(error == 0); + consume_event(ifd, wd, IN_DELETE_SELF, IN_ISDIR, NULL); + consume_event(ifd, wd, 0, IN_IGNORED, NULL); + + close_inotify(ifd); +} + +ATF_TC_WITHOUT_HEAD(inotify_event_move); +ATF_TC_BODY(inotify_event_move, tc) +{ + char dir1[PATH_MAX], dir2[PATH_MAX], path1[PATH_MAX], path2[PATH_MAX]; + char path3[PATH_MAX]; + int error, ifd, fd, wd1, wd2, wd3; + uint32_t cookie1, cookie2; + + ifd = inotify(IN_NONBLOCK); + + wd1 = watch_dir(ifd, IN_MOVE | IN_MOVE_SELF, dir1); + wd2 = watch_dir(ifd, IN_MOVE | IN_MOVE_SELF, dir2); + + snprintf(path1, sizeof(path1), "%s/file", dir1); + fd = open(path1, O_RDWR | O_CREAT, 0644); + ATF_REQUIRE(fd != -1); + close_checked(fd); + snprintf(path2, sizeof(path2), "%s/file2", dir2); + error = rename(path1, path2); + ATF_REQUIRE(error == 0); + cookie1 = consume_event_cookie(ifd, wd1, IN_MOVED_FROM, 0, "file"); + cookie2 = consume_event_cookie(ifd, wd2, IN_MOVED_TO, 0, "file2"); + ATF_REQUIRE_MSG(cookie1 == cookie2, + "expected cookie %u, got %u", cookie1, cookie2); + + snprintf(path2, sizeof(path2), "%s/dir", dir2); + error = rename(dir1, path2); + ATF_REQUIRE(error == 0); + consume_event(ifd, wd1, IN_MOVE_SELF, IN_ISDIR, NULL); + consume_event(ifd, wd2, IN_MOVED_TO, IN_ISDIR, "dir"); + + wd3 = watch_file(ifd, IN_MOVE_SELF, path3); + error = rename(path3, "foo"); + ATF_REQUIRE(error == 0); + consume_event(ifd, wd3, IN_MOVE_SELF, 0, NULL); + + close_inotify(ifd); +} + +ATF_TC_WITHOUT_HEAD(inotify_event_open); +ATF_TC_BODY(inotify_event_open, tc) +{ + char root[PATH_MAX], path[PATH_MAX]; + int error, ifd, fd, wd; + + ifd = inotify(IN_NONBLOCK); + + wd = watch_dir(ifd, IN_OPEN, root); + + snprintf(path, sizeof(path), "%s/file", root); + fd = open(path, O_RDWR | O_CREAT, 0644); + ATF_REQUIRE(fd != -1); + close_checked(fd); + consume_event(ifd, wd, IN_OPEN, 0, "file"); + + fd = open(path, O_PATH); + ATF_REQUIRE(fd != -1); + close_checked(fd); + consume_event(ifd, wd, IN_OPEN, 0, "file"); + + fd = open(root, O_DIRECTORY); + ATF_REQUIRE(fd != -1); + close_checked(fd); + consume_event(ifd, wd, IN_OPEN, IN_ISDIR, NULL); + + snprintf(path, sizeof(path), "%s/fifo", root); + error = mkfifo(path, 0644); + ATF_REQUIRE(error == 0); + fd = open(path, O_RDWR); + ATF_REQUIRE(fd != -1); + close_checked(fd); + consume_event(ifd, wd, IN_OPEN, 0, "fifo"); + + close_inotify(ifd); +} + +ATF_TC_WITH_CLEANUP(inotify_event_unmount); +ATF_TC_HEAD(inotify_event_unmount, tc) +{ + atf_tc_set_md_var(tc, "require.user", "root"); +} +ATF_TC_BODY(inotify_event_unmount, tc) +{ + int error, fd, ifd, wd; + + ifd = inotify(IN_NONBLOCK); + + error = mkdir("./root", 0755); + ATF_REQUIRE(error == 0); + + mount_tmpfs("./root"); + + error = mkdir("./root/dir", 0755); + ATF_REQUIRE(error == 0); + wd = inotify_add_watch(ifd, "./root/dir", IN_OPEN); + ATF_REQUIRE(wd >= 0); + + fd = open("./root/dir", O_RDONLY | O_DIRECTORY); + ATF_REQUIRE(fd != -1); + consume_event(ifd, wd, IN_OPEN, IN_ISDIR, NULL); + close_checked(fd); + + /* A regular unmount should fail, as inotify holds a vnode reference. */ + error = unmount("./root", 0); + ATF_REQUIRE_ERRNO(EBUSY, error == -1); + error = unmount("./root", MNT_FORCE); + ATF_REQUIRE_MSG(error == 0, + "unmounting ./root failed: %s", strerror(errno)); + + consume_event(ifd, wd, 0, IN_UNMOUNT, NULL); + consume_event(ifd, wd, 0, IN_IGNORED, NULL); + + close_inotify(ifd); +} +ATF_TC_CLEANUP(inotify_event_unmount, tc) +{ + (void)unmount("./root", MNT_FORCE); +} + +ATF_TP_ADD_TCS(tp) +{ + /* Tests for the inotify syscalls. */ + ATF_TP_ADD_TC(tp, inotify_capsicum); + ATF_TP_ADD_TC(tp, inotify_coalesce); + ATF_TP_ADD_TC(tp, inotify_mask_create); + ATF_TP_ADD_TC(tp, inotify_nullfs); + ATF_TP_ADD_TC(tp, inotify_queue_overflow); + /* Tests for the various inotify event types. */ + ATF_TP_ADD_TC(tp, inotify_event_access_file); + ATF_TP_ADD_TC(tp, inotify_event_access_dir); + ATF_TP_ADD_TC(tp, inotify_event_attrib); + ATF_TP_ADD_TC(tp, inotify_event_close_nowrite); + ATF_TP_ADD_TC(tp, inotify_event_close_write); + ATF_TP_ADD_TC(tp, inotify_event_create); + ATF_TP_ADD_TC(tp, inotify_event_delete); + ATF_TP_ADD_TC(tp, inotify_event_move); + ATF_TP_ADD_TC(tp, inotify_event_open); + ATF_TP_ADD_TC(tp, inotify_event_unmount); + return (atf_no_error()); +} diff --git a/tests/sys/kern/jail_lookup_root.c b/tests/sys/kern/jail_lookup_root.c new file mode 100644 index 000000000000..34e89f4aea2b --- /dev/null +++ b/tests/sys/kern/jail_lookup_root.c @@ -0,0 +1,133 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2025 Mark Johnston <markj@FreeBSD.org> + */ + +#include <sys/param.h> +#include <sys/jail.h> +#include <sys/mount.h> +#include <sys/stat.h> + +#include <err.h> +#include <errno.h> +#include <fcntl.h> +#include <jail.h> +#include <mntopts.h> +#include <stdio.h> +#include <stdlib.h> + +#include <atf-c.h> + +static void +mkdir_checked(const char *dir, mode_t mode) +{ + int error; + + error = mkdir(dir, mode); + ATF_REQUIRE_MSG(error == 0 || errno == EEXIST, + "mkdir %s: %s", dir, strerror(errno)); +} + +static void __unused +mount_nullfs(const char *dir, const char *target) +{ + struct iovec *iov; + char errmsg[1024]; + int error, iovlen; + + iov = NULL; + iovlen = 0; + + build_iovec(&iov, &iovlen, __DECONST(char *, "fstype"), + __DECONST(char *, "nullfs"), (size_t)-1); + build_iovec(&iov, &iovlen, __DECONST(char *, "fspath"), + __DECONST(char *, target), (size_t)-1); + build_iovec(&iov, &iovlen, __DECONST(char *, "from"), + __DECONST(char *, dir), (size_t)-1); + build_iovec(&iov, &iovlen, __DECONST(char *, "errmsg"), + errmsg, sizeof(errmsg)); + + errmsg[0] = '\0'; + error = nmount(iov, iovlen, 0); + ATF_REQUIRE_MSG(error == 0, "nmount: %s", + errmsg[0] != '\0' ? errmsg : strerror(errno)); + + free_iovec(&iov, &iovlen); +} + +ATF_TC_WITH_CLEANUP(jail_root); +ATF_TC_HEAD(jail_root, tc) +{ + atf_tc_set_md_var(tc, "require.user", "root"); +} +ATF_TC_BODY(jail_root, tc) +{ + int error, fd, jid; + + mkdir_checked("./root", 0755); + mkdir_checked("./root/a", 0755); + mkdir_checked("./root/b", 0755); + mkdir_checked("./root/a/c", 0755); + + jid = jail_setv(JAIL_CREATE | JAIL_ATTACH, + "name", "nullfs_jail_root_test", + "allow.mount", "true", + "allow.mount.nullfs", "true", + "enforce_statfs", "1", + "path", "./root", + "persist", NULL, + NULL); + ATF_REQUIRE_MSG(jid >= 0, "jail_setv: %s", jail_errmsg); + + mount_nullfs("/a", "/b"); + + error = chdir("/b/c"); + ATF_REQUIRE(error == 0); + + error = rename("/a/c", "/c"); + ATF_REQUIRE(error == 0); + + /* Descending to the jail root should be ok. */ + error = chdir(".."); + ATF_REQUIRE(error == 0); + + /* Going beyond the root will trigger an error. */ + error = chdir(".."); + ATF_REQUIRE_ERRNO(ENOENT, error != 0); + fd = open("..", O_RDONLY | O_DIRECTORY); + ATF_REQUIRE_ERRNO(ENOENT, fd < 0); +} +ATF_TC_CLEANUP(jail_root, tc) +{ + struct statfs fs; + fsid_t fsid; + int error, jid; + + error = statfs("./root/b", &fs); + if (error != 0) + err(1, "statfs ./b"); + fsid = fs.f_fsid; + error = statfs("./root", &fs); + if (error != 0) + err(1, "statfs ./root"); + if (fsid.val[0] != fs.f_fsid.val[0] || + fsid.val[1] != fs.f_fsid.val[1]) { + error = unmount("./root/b", 0); + if (error != 0) + err(1, "unmount ./root/b"); + } + + jid = jail_getid("nullfs_jail_root_test"); + if (jid >= 0) { + error = jail_remove(jid); + if (error != 0) + err(1, "jail_remove"); + } +} + +ATF_TP_ADD_TCS(tp) +{ + ATF_TP_ADD_TC(tp, jail_root); + return (atf_no_error()); +} diff --git a/tests/sys/kern/ptrace_test.c b/tests/sys/kern/ptrace_test.c index db681293f043..fee0bd2ffa38 100644 --- a/tests/sys/kern/ptrace_test.c +++ b/tests/sys/kern/ptrace_test.c @@ -28,13 +28,13 @@ #include <sys/elf.h> #include <sys/event.h> #include <sys/file.h> +#include <sys/mman.h> #include <sys/time.h> #include <sys/procctl.h> #include <sys/procdesc.h> #include <sys/ptrace.h> #include <sys/procfs.h> #include <sys/queue.h> -#include <sys/runq.h> #include <sys/syscall.h> #include <sys/sysctl.h> #include <sys/user.h> @@ -2027,7 +2027,7 @@ ATF_TC_BODY(ptrace__PT_KILL_competing_signal, tc) sched_get_priority_min(SCHED_FIFO)) / 2; CHILD_REQUIRE(pthread_setschedparam(pthread_self(), SCHED_FIFO, &sched_param) == 0); - sched_param.sched_priority -= RQ_PPQ; + sched_param.sched_priority -= 1; CHILD_REQUIRE(pthread_setschedparam(t, SCHED_FIFO, &sched_param) == 0); @@ -2130,7 +2130,7 @@ ATF_TC_BODY(ptrace__PT_KILL_competing_stop, tc) sched_get_priority_min(SCHED_FIFO)) / 2; CHILD_REQUIRE(pthread_setschedparam(pthread_self(), SCHED_FIFO, &sched_param) == 0); - sched_param.sched_priority -= RQ_PPQ; + sched_param.sched_priority -= 1; CHILD_REQUIRE(pthread_setschedparam(t, SCHED_FIFO, &sched_param) == 0); @@ -3239,7 +3239,7 @@ ATF_TC_BODY(ptrace__PT_REGSET, tc) ATF_REQUIRE(ptrace(PT_GETREGSET, wpid, (caddr_t)&vec, NT_ARM_ADDR_MASK) != -1); REQUIRE_EQ(addr_mask.code, addr_mask.data); - ATF_REQUIRE(addr_mask.code == 0 || + ATF_REQUIRE(addr_mask.code == 0xff00000000000000ul || addr_mask.code == 0xff7f000000000000UL); #endif @@ -4378,7 +4378,10 @@ ATF_TC_BODY(ptrace__PT_SC_REMOTE_getpid, tc) exit(0); } - attach_child(fpid); + wpid = waitpid(fpid, &status, 0); + REQUIRE_EQ(wpid, fpid); + ATF_REQUIRE(WIFSTOPPED(status)); + REQUIRE_EQ(WSTOPSIG(status), SIGSTOP); pscr.pscr_syscall = SYS_getpid; pscr.pscr_nargs = 0; @@ -4461,6 +4464,132 @@ ATF_TC_BODY(ptrace__reap_kill_stopped, tc) REQUIRE_EQ(-1, prk.rk_fpid); } +struct child_res { + struct timespec sleep_time; + int nanosleep_res; + int nanosleep_errno; +}; + +static const long nsec = 1000000000L; +static const struct timespec ten_sec = { + .tv_sec = 10, + .tv_nsec = 0, +}; +static const struct timespec twelve_sec = { + .tv_sec = 12, + .tv_nsec = 0, +}; + +ATF_TC_WITHOUT_HEAD(ptrace__PT_ATTACH_no_EINTR); +ATF_TC_BODY(ptrace__PT_ATTACH_no_EINTR, tc) +{ + struct child_res *shm; + struct timespec rqt, now, wake; + pid_t debuggee; + int status; + + shm = mmap(NULL, sizeof(*shm), PROT_READ | PROT_WRITE, + MAP_SHARED | MAP_ANON, -1, 0); + ATF_REQUIRE(shm != MAP_FAILED); + + ATF_REQUIRE((debuggee = fork()) != -1); + if (debuggee == 0) { + rqt.tv_sec = 10; + rqt.tv_nsec = 0; + clock_gettime(CLOCK_MONOTONIC_PRECISE, &now); + errno = 0; + shm->nanosleep_res = nanosleep(&rqt, NULL); + shm->nanosleep_errno = errno; + clock_gettime(CLOCK_MONOTONIC_PRECISE, &wake); + timespecsub(&wake, &now, &shm->sleep_time); + _exit(0); + } + + /* Give the debuggee some time to go to sleep. */ + sleep(2); + REQUIRE_EQ(ptrace(PT_ATTACH, debuggee, 0, 0), 0); + REQUIRE_EQ(waitpid(debuggee, &status, 0), debuggee); + ATF_REQUIRE(WIFSTOPPED(status)); + REQUIRE_EQ(WSTOPSIG(status), SIGSTOP); + + REQUIRE_EQ(ptrace(PT_DETACH, debuggee, 0, 0), 0); + REQUIRE_EQ(waitpid(debuggee, &status, 0), debuggee); + ATF_REQUIRE(WIFEXITED(status)); + REQUIRE_EQ(WEXITSTATUS(status), 0); + + ATF_REQUIRE(shm->nanosleep_res == 0); + ATF_REQUIRE(shm->nanosleep_errno == 0); + ATF_REQUIRE(timespeccmp(&shm->sleep_time, &ten_sec, >=)); + ATF_REQUIRE(timespeccmp(&shm->sleep_time, &twelve_sec, <=)); +} + +ATF_TC_WITHOUT_HEAD(ptrace__PT_DETACH_continued); +ATF_TC_BODY(ptrace__PT_DETACH_continued, tc) +{ + char buf[256]; + pid_t debuggee, debugger; + int dpipe[2] = {-1, -1}, status; + + /* Setup the debuggee's pipe, which we'll use to let it terminate. */ + ATF_REQUIRE(pipe(dpipe) == 0); + ATF_REQUIRE((debuggee = fork()) != -1); + + if (debuggee == 0) { + ssize_t readsz; + + /* + * The debuggee will just absorb everything until the parent + * closes it. In the process, we expect it to get SIGSTOP'd, + * then ptrace(2)d and finally, it should resume after we detach + * and the parent will be notified. + */ + close(dpipe[1]); + while ((readsz = read(dpipe[0], buf, sizeof(buf))) != 0) { + if (readsz > 0 || errno == EINTR) + continue; + _exit(1); + } + + _exit(0); + } + + close(dpipe[0]); + + ATF_REQUIRE(kill(debuggee, SIGSTOP) == 0); + REQUIRE_EQ(waitpid(debuggee, &status, WUNTRACED), debuggee); + ATF_REQUIRE(WIFSTOPPED(status)); + + /* Child is stopped, enter the debugger to attach/detach. */ + ATF_REQUIRE((debugger = fork()) != -1); + if (debugger == 0) { + REQUIRE_EQ(ptrace(PT_ATTACH, debuggee, 0, 0), 0); + REQUIRE_EQ(waitpid(debuggee, &status, 0), debuggee); + ATF_REQUIRE(WIFSTOPPED(status)); + REQUIRE_EQ(WSTOPSIG(status), SIGSTOP); + + REQUIRE_EQ(ptrace(PT_DETACH, debuggee, 0, 0), 0); + _exit(0); + } + + REQUIRE_EQ(waitpid(debugger, &status, 0), debugger); + ATF_REQUIRE(WIFEXITED(status)); + REQUIRE_EQ(WEXITSTATUS(status), 0); + + REQUIRE_EQ(waitpid(debuggee, &status, WCONTINUED), debuggee); + ATF_REQUIRE(WIFCONTINUED(status)); + + /* + * Closing the pipe will trigger the debuggee to exit now that the + * child has resumed following detach. + */ + close(dpipe[1]); + + REQUIRE_EQ(waitpid(debuggee, &status, 0), debuggee); + ATF_REQUIRE(WIFEXITED(status)); + REQUIRE_EQ(WEXITSTATUS(status), 0); + +} + ATF_TP_ADD_TCS(tp) { ATF_TP_ADD_TC(tp, ptrace__parent_wait_after_trace_me); @@ -4529,6 +4658,8 @@ ATF_TP_ADD_TCS(tp) 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__reap_kill_stopped); + ATF_TP_ADD_TC(tp, ptrace__PT_ATTACH_no_EINTR); + ATF_TP_ADD_TC(tp, ptrace__PT_DETACH_continued); return (atf_no_error()); } diff --git a/tests/sys/kern/socket_splice.c b/tests/sys/kern/socket_splice.c index 3a85ae91ecc7..dfd4cb4f5957 100644 --- a/tests/sys/kern/socket_splice.c +++ b/tests/sys/kern/socket_splice.c @@ -84,7 +84,7 @@ tcp_socketpair(int out[2], int domain) memset(&sin, 0, sizeof(sin)); sin.sin_family = AF_INET; sin.sin_len = sizeof(sin); - sin.sin_addr.s_addr = htonl(INADDR_ANY); + sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK); sin.sin_port = htons(0); sinp = (struct sockaddr *)&sin; } else { @@ -92,7 +92,7 @@ tcp_socketpair(int out[2], int domain) memset(&sin6, 0, sizeof(sin6)); sin6.sin6_family = AF_INET6; sin6.sin6_len = sizeof(sin6); - sin6.sin6_addr = in6addr_any; + sin6.sin6_addr = in6addr_loopback; sin6.sin6_port = htons(0); sinp = (struct sockaddr *)&sin6; } diff --git a/tests/sys/kern/tty/Makefile b/tests/sys/kern/tty/Makefile index c362793a8b64..8628ab79875f 100644 --- a/tests/sys/kern/tty/Makefile +++ b/tests/sys/kern/tty/Makefile @@ -5,8 +5,11 @@ PLAIN_TESTS_PORCH+= test_canon PLAIN_TESTS_PORCH+= test_canon_fullbuf PLAIN_TESTS_PORCH+= test_ncanon PLAIN_TESTS_PORCH+= test_recanon +ATF_TESTS_C+= test_sti PROGS+= fionread PROGS+= readsz +LIBADD.test_sti= util + .include <bsd.test.mk> diff --git a/tests/sys/kern/tty/test_sti.c b/tests/sys/kern/tty/test_sti.c new file mode 100644 index 000000000000..f792001b4e3f --- /dev/null +++ b/tests/sys/kern/tty/test_sti.c @@ -0,0 +1,337 @@ +/*- + * Copyright (c) 2025 Kyle Evans <kevans@FreeBSD.org> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include <sys/param.h> +#include <sys/ioctl.h> +#include <sys/wait.h> + +#include <assert.h> +#include <errno.h> +#include <fcntl.h> +#include <signal.h> +#include <stdbool.h> +#include <stdlib.h> +#include <termios.h> + +#include <atf-c.h> +#include <libutil.h> + +enum stierr { + STIERR_CONFIG_FETCH, + STIERR_CONFIG, + STIERR_INJECT, + STIERR_READFAIL, + STIERR_BADTEXT, + STIERR_DATAFOUND, + STIERR_ROTTY, + STIERR_WOTTY, + STIERR_WOOK, + STIERR_BADERR, + + STIERR_MAXERR +}; + +static const struct stierr_map { + enum stierr stierr; + const char *msg; +} stierr_map[] = { + { STIERR_CONFIG_FETCH, "Failed to fetch ctty configuration" }, + { STIERR_CONFIG, "Failed to configure ctty in the child" }, + { STIERR_INJECT, "Failed to inject characters via TIOCSTI" }, + { STIERR_READFAIL, "Failed to read(2) from stdin" }, + { STIERR_BADTEXT, "read(2) data did not match injected data" }, + { STIERR_DATAFOUND, "read(2) data when we did not expected to" }, + { STIERR_ROTTY, "Failed to open tty r/o" }, + { STIERR_WOTTY, "Failed to open tty w/o" }, + { STIERR_WOOK, "TIOCSTI on w/o tty succeeded" }, + { STIERR_BADERR, "Received wrong error from failed TIOCSTI" }, +}; +_Static_assert(nitems(stierr_map) == STIERR_MAXERR, + "Failed to describe all errors"); + +/* + * Inject each character of the input string into the TTY. The caller can + * assume that errno is preserved on return. + */ +static ssize_t +inject(int fileno, const char *str) +{ + size_t nb = 0; + + for (const char *walker = str; *walker != '\0'; walker++) { + if (ioctl(fileno, TIOCSTI, walker) != 0) + return (-1); + nb++; + } + + return (nb); +} + +/* + * Forks off a new process, stashes the parent's handle for the pty in *termfd + * and returns the pid. 0 for the child, >0 for the parent, as usual. + * + * Most tests fork so that we can do them while unprivileged, which we can only + * do if we're operating on our ctty (and we don't want to touch the tty of + * whatever may be running the tests). + */ +static int +init_pty(int *termfd, bool canon) +{ + int pid; + + pid = forkpty(termfd, NULL, NULL, NULL); + ATF_REQUIRE(pid != -1); + + if (pid == 0) { + struct termios term; + + /* + * Child reconfigures tty to disable echo and put it into raw + * mode if requested. + */ + if (tcgetattr(STDIN_FILENO, &term) == -1) + _exit(STIERR_CONFIG_FETCH); + term.c_lflag &= ~ECHO; + if (!canon) + term.c_lflag &= ~ICANON; + if (tcsetattr(STDIN_FILENO, TCSANOW, &term) == -1) + _exit(STIERR_CONFIG); + } + + return (pid); +} + +static void +finalize_child(pid_t pid, int signo) +{ + int status, wpid; + + while ((wpid = waitpid(pid, &status, 0)) != pid) { + if (wpid != -1) + continue; + ATF_REQUIRE_EQ_MSG(EINTR, errno, + "waitpid: %s", strerror(errno)); + } + + /* + * Some tests will signal the child for whatever reason, and we're + * expecting it to terminate it. For those cases, it's OK to just see + * that termination. For all other cases, we expect a graceful exit + * with an exit status that reflects a cause that we have an error + * mapped for. + */ + if (signo >= 0) { + ATF_REQUIRE(WIFSIGNALED(status)); + ATF_REQUIRE_EQ(signo, WTERMSIG(status)); + } else { + ATF_REQUIRE(WIFEXITED(status)); + if (WEXITSTATUS(status) != 0) { + int err = WEXITSTATUS(status); + + for (size_t i = 0; i < nitems(stierr_map); i++) { + const struct stierr_map *map = &stierr_map[i]; + + if ((int)map->stierr == err) { + atf_tc_fail("%s", map->msg); + __assert_unreachable(); + } + } + } + } +} + +ATF_TC(basic); +ATF_TC_HEAD(basic, tc) +{ + atf_tc_set_md_var(tc, "descr", + "Test for basic functionality of TIOCSTI"); + atf_tc_set_md_var(tc, "require.user", "unprivileged"); +} +ATF_TC_BODY(basic, tc) +{ + int pid, term; + + /* + * We don't canonicalize on this test because we can assume that the + * injected data will be available after TIOCSTI returns. This is all + * within a single thread for the basic test, so we simplify our lives + * slightly in raw mode. + */ + pid = init_pty(&term, false); + if (pid == 0) { + static const char sending[] = "Text"; + char readbuf[32]; + ssize_t injected, readsz; + + injected = inject(STDIN_FILENO, sending); + if (injected != sizeof(sending) - 1) + _exit(STIERR_INJECT); + + readsz = read(STDIN_FILENO, readbuf, sizeof(readbuf)); + + if (readsz < 0 || readsz != injected) + _exit(STIERR_READFAIL); + if (memcmp(readbuf, sending, readsz) != 0) + _exit(STIERR_BADTEXT); + + _exit(0); + } + + finalize_child(pid, -1); +} + +ATF_TC(root); +ATF_TC_HEAD(root, tc) +{ + atf_tc_set_md_var(tc, "descr", + "Test that root can inject into another TTY"); + atf_tc_set_md_var(tc, "require.user", "root"); +} +ATF_TC_BODY(root, tc) +{ + static const char sending[] = "Text\r"; + ssize_t injected; + int pid, term; + + /* + * We leave canonicalization enabled for this one so that the read(2) + * below hangs until we have all of the data available, rather than + * having to signal OOB that it's safe to read. + */ + pid = init_pty(&term, true); + if (pid == 0) { + char readbuf[32]; + ssize_t readsz; + + readsz = read(STDIN_FILENO, readbuf, sizeof(readbuf)); + if (readsz < 0 || readsz != sizeof(sending) - 1) + _exit(STIERR_READFAIL); + + /* + * Here we ignore the trailing \r, because it won't have + * surfaced in our read(2). + */ + if (memcmp(readbuf, sending, readsz - 1) != 0) + _exit(STIERR_BADTEXT); + + _exit(0); + } + + injected = inject(term, sending); + ATF_REQUIRE_EQ_MSG(sizeof(sending) - 1, injected, + "Injected %zu characters, expected %zu", injected, + sizeof(sending) - 1); + + finalize_child(pid, -1); +} + +ATF_TC(unprivileged_fail_noctty); +ATF_TC_HEAD(unprivileged_fail_noctty, tc) +{ + atf_tc_set_md_var(tc, "descr", + "Test that unprivileged cannot inject into non-controlling TTY"); + atf_tc_set_md_var(tc, "require.user", "unprivileged"); +} +ATF_TC_BODY(unprivileged_fail_noctty, tc) +{ + const char sending[] = "Text"; + ssize_t injected; + int pid, serrno, term; + + pid = init_pty(&term, false); + if (pid == 0) { + char readbuf[32]; + ssize_t readsz; + + /* + * This should hang until we get terminated by the parent. + */ + readsz = read(STDIN_FILENO, readbuf, sizeof(readbuf)); + if (readsz > 0) + _exit(STIERR_DATAFOUND); + + _exit(0); + } + + /* Should fail. */ + injected = inject(term, sending); + serrno = errno; + + /* Done with the child, just kill it now to avoid problems later. */ + kill(pid, SIGINT); + finalize_child(pid, SIGINT); + + ATF_REQUIRE_EQ_MSG(-1, (ssize_t)injected, + "TIOCSTI into non-ctty succeeded"); + ATF_REQUIRE_EQ(EACCES, serrno); +} + +ATF_TC(unprivileged_fail_noread); +ATF_TC_HEAD(unprivileged_fail_noread, tc) +{ + atf_tc_set_md_var(tc, "descr", + "Test that unprivileged cannot inject into TTY not opened for read"); + atf_tc_set_md_var(tc, "require.user", "unprivileged"); +} +ATF_TC_BODY(unprivileged_fail_noread, tc) +{ + int pid, term; + + /* + * Canonicalization actually doesn't matter for this one, we'll trust + * that the failure means we didn't inject anything. + */ + pid = init_pty(&term, true); + if (pid == 0) { + static const char sending[] = "Text"; + ssize_t injected; + int rotty, wotty; + + /* + * We open the tty both r/o and w/o to ensure we got the device + * name right; one of these will pass, one of these will fail. + */ + wotty = openat(STDIN_FILENO, "", O_EMPTY_PATH | O_WRONLY); + if (wotty == -1) + _exit(STIERR_WOTTY); + rotty = openat(STDIN_FILENO, "", O_EMPTY_PATH | O_RDONLY); + if (rotty == -1) + _exit(STIERR_ROTTY); + + /* + * This injection is expected to fail with EPERM, because it may + * be our controlling tty but it is not open for reading. + */ + injected = inject(wotty, sending); + if (injected != -1) + _exit(STIERR_WOOK); + if (errno != EPERM) + _exit(STIERR_BADERR); + + /* + * Demonstrate that it does succeed on the other fd we opened, + * which is r/o. + */ + injected = inject(rotty, sending); + if (injected != sizeof(sending) - 1) + _exit(STIERR_INJECT); + + _exit(0); + } + + finalize_child(pid, -1); +} + +ATF_TP_ADD_TCS(tp) +{ + ATF_TP_ADD_TC(tp, basic); + ATF_TP_ADD_TC(tp, root); + ATF_TP_ADD_TC(tp, unprivileged_fail_noctty); + ATF_TP_ADD_TC(tp, unprivileged_fail_noread); + + return (atf_no_error()); +} diff --git a/tests/sys/kern/unix_passfd_test.c b/tests/sys/kern/unix_passfd_test.c index 74095859d899..7dc4541ad402 100644 --- a/tests/sys/kern/unix_passfd_test.c +++ b/tests/sys/kern/unix_passfd_test.c @@ -27,15 +27,19 @@ */ #include <sys/param.h> +#include <sys/jail.h> #include <sys/socket.h> #include <sys/stat.h> #include <sys/sysctl.h> #include <sys/time.h> #include <sys/resource.h> #include <sys/un.h> +#include <sys/wait.h> +#include <err.h> #include <errno.h> #include <fcntl.h> +#include <jail.h> #include <limits.h> #include <stdio.h> #include <stdlib.h> @@ -376,6 +380,30 @@ ATF_TC_BODY(simple_send_fd_msg_cmsg_cloexec, tc) } /* + * Like simple_send_fd but also sets MSG_CMSG_CLOFORK and checks that the + * received file descriptor has the FD_CLOFORK flag set. + */ +ATF_TC_WITHOUT_HEAD(simple_send_fd_msg_cmsg_clofork); +ATF_TC_BODY(simple_send_fd_msg_cmsg_clofork, tc) +{ + struct stat getfd_stat, putfd_stat; + int fd[2], getfd, putfd; + + domainsocketpair(fd); + tempfile(&putfd); + dofstat(putfd, &putfd_stat); + sendfd(fd[0], putfd); + recvfd(fd[1], &getfd, MSG_CMSG_CLOFORK); + dofstat(getfd, &getfd_stat); + samefile(&putfd_stat, &getfd_stat); + ATF_REQUIRE_EQ_MSG(fcntl(getfd, F_GETFD) & FD_CLOFORK, FD_CLOFORK, + "FD_CLOFORK not set on the received file descriptor"); + close(putfd); + close(getfd); + closesocketpair(fd); +} + +/* * Same as simple_send_fd, only close the file reference after sending, so that * the only reference is the descriptor in the UNIX domain socket buffer. */ @@ -544,6 +572,51 @@ ATF_TC_BODY(send_overflow, tc) closesocketpair(fd); } +/* + * Make sure that we do not receive descriptors with MSG_PEEK. + */ +ATF_TC_WITHOUT_HEAD(peek); +ATF_TC_BODY(peek, tc) +{ + int fd[2], getfd, putfd, nfds; + + domainsocketpair(fd); + tempfile(&putfd); + nfds = getnfds(); + sendfd(fd[0], putfd); + ATF_REQUIRE(getnfds() == nfds); + + /* First make MSG_PEEK recvmsg(2)... */ + char cbuf[CMSG_SPACE(sizeof(int))]; + char buf[1]; + struct iovec iov = { + .iov_base = buf, + .iov_len = sizeof(buf) + }; + struct msghdr msghdr = { + .msg_iov = &iov, + .msg_iovlen = 1, + .msg_control = cbuf, + .msg_controllen = sizeof(cbuf), + }; + ATF_REQUIRE(1 == recvmsg(fd[1], &msghdr, MSG_PEEK)); + for (struct cmsghdr *cmsghdr = CMSG_FIRSTHDR(&msghdr); + cmsghdr != NULL; cmsghdr = CMSG_NXTHDR(&msghdr, cmsghdr)) { + /* Usually this is some garbage. */ + printf("level %d type %d len %u\n", + cmsghdr->cmsg_level, cmsghdr->cmsg_type, cmsghdr->cmsg_len); + } + + /* ... and make sure we did not receive any descriptors! */ + ATF_REQUIRE(getnfds() == nfds); + + /* Now really receive a descriptor. */ + recvfd(fd[1], &getfd, 0); + ATF_REQUIRE(getnfds() == nfds + 1); + close(putfd); + close(getfd); + closesocketpair(fd); +} /* * Send two files. Then receive them. Make sure they are returned in the @@ -987,16 +1060,147 @@ ATF_TC_BODY(control_creates_records, tc) closesocketpair(fd); } +ATF_TC_WITH_CLEANUP(cross_jail_dirfd); +ATF_TC_HEAD(cross_jail_dirfd, tc) +{ + atf_tc_set_md_var(tc, "require.user", "root"); +} +ATF_TC_BODY(cross_jail_dirfd, tc) +{ + int error, sock[2], jid1, jid2, status; + pid_t pid1, pid2; + + domainsocketpair(sock); + + error = mkdir("./a", 0755); + ATF_REQUIRE(error == 0); + error = mkdir("./b", 0755); + ATF_REQUIRE(error == 0); + error = mkdir("./c", 0755); + ATF_REQUIRE(error == 0); + error = mkdir("./a/c", 0755); + ATF_REQUIRE(error == 0); + + jid1 = jail_setv(JAIL_CREATE, + "name", "passfd_test_cross_jail_dirfd1", + "path", "./a", + "persist", NULL, + NULL); + ATF_REQUIRE_MSG(jid1 >= 0, "jail_setv: %s", jail_errmsg); + + jid2 = jail_setv(JAIL_CREATE, + "name", "passfd_test_cross_jail_dirfd2", + "path", "./b", + "persist", NULL, + NULL); + ATF_REQUIRE_MSG(jid2 >= 0, "jail_setv: %s", jail_errmsg); + + pid1 = fork(); + ATF_REQUIRE(pid1 >= 0); + if (pid1 == 0) { + ssize_t len; + int dfd, error; + char ch; + + error = jail_attach(jid1); + if (error != 0) + err(1, "jail_attach"); + + dfd = open(".", O_RDONLY | O_DIRECTORY); + if (dfd < 0) + err(1, "open(\".\") in jail %d", jid1); + + ch = 0; + len = sendfd_payload(sock[0], dfd, &ch, sizeof(ch)); + if (len == -1) + err(1, "sendmsg"); + + _exit(0); + } + + pid2 = fork(); + ATF_REQUIRE(pid2 >= 0); + if (pid2 == 0) { + ssize_t len; + int dfd, dfd2, error, fd; + char ch; + + error = jail_attach(jid2); + if (error != 0) + err(1, "jail_attach"); + + /* Get a directory from outside the jail root. */ + len = recvfd_payload(sock[1], &dfd, &ch, sizeof(ch), + CMSG_SPACE(sizeof(int)), 0); + if (len == -1) + err(1, "recvmsg"); + + if ((fcntl(dfd, F_GETFD) & FD_RESOLVE_BENEATH) == 0) + errx(1, "dfd does not have FD_RESOLVE_BENEATH set"); + + /* Make sure we can't chdir. */ + error = fchdir(dfd); + if (error == 0) + errx(1, "fchdir succeeded"); + if (errno != ENOTCAPABLE) + err(1, "fchdir"); + + /* Make sure a dotdot access fails. */ + fd = openat(dfd, "../c", O_RDONLY | O_DIRECTORY); + if (fd >= 0) + errx(1, "openat(\"../c\") succeeded"); + if (errno != ENOTCAPABLE) + err(1, "openat"); + + /* Accesses within the sender's jail root are ok. */ + fd = openat(dfd, "c", O_RDONLY | O_DIRECTORY); + if (fd < 0) + err(1, "openat(\"c\")"); + + dfd2 = openat(dfd, "", O_EMPTY_PATH | O_RDONLY | O_DIRECTORY); + if (dfd2 < 0) + err(1, "openat(\"\")"); + if ((fcntl(dfd2, F_GETFD) & FD_RESOLVE_BENEATH) == 0) + errx(1, "dfd2 does not have FD_RESOLVE_BENEATH set"); + + _exit(0); + } + + error = waitpid(pid1, &status, 0); + ATF_REQUIRE(error != -1); + ATF_REQUIRE(WIFEXITED(status)); + ATF_REQUIRE(WEXITSTATUS(status) == 0); + error = waitpid(pid2, &status, 0); + ATF_REQUIRE(error != -1); + ATF_REQUIRE(WIFEXITED(status)); + ATF_REQUIRE(WEXITSTATUS(status) == 0); + + closesocketpair(sock); +} +ATF_TC_CLEANUP(cross_jail_dirfd, tc) +{ + int jid; + + jid = jail_getid("passfd_test_cross_jail_dirfd1"); + if (jid >= 0 && jail_remove(jid) != 0) + err(1, "jail_remove"); + jid = jail_getid("passfd_test_cross_jail_dirfd2"); + if (jid >= 0 && jail_remove(jid) != 0) + err(1, "jail_remove"); +} + ATF_TP_ADD_TCS(tp) { ATF_TP_ADD_TC(tp, simple_send_fd); ATF_TP_ADD_TC(tp, simple_send_fd_msg_cmsg_cloexec); + ATF_TP_ADD_TC(tp, simple_send_fd_msg_cmsg_clofork); ATF_TP_ADD_TC(tp, send_and_close); ATF_TP_ADD_TC(tp, send_and_cancel); ATF_TP_ADD_TC(tp, send_and_shutdown); ATF_TP_ADD_TC(tp, send_a_lot); ATF_TP_ADD_TC(tp, send_overflow); + ATF_TP_ADD_TC(tp, peek); ATF_TP_ADD_TC(tp, two_files); ATF_TP_ADD_TC(tp, bundle); ATF_TP_ADD_TC(tp, bundle_cancel); @@ -1006,6 +1210,7 @@ ATF_TP_ADD_TCS(tp) ATF_TP_ADD_TC(tp, copyout_rights_error); ATF_TP_ADD_TC(tp, empty_rights_message); ATF_TP_ADD_TC(tp, control_creates_records); + ATF_TP_ADD_TC(tp, cross_jail_dirfd); return (atf_no_error()); } diff --git a/tests/sys/kern/unix_seqpacket_test.c b/tests/sys/kern/unix_seqpacket_test.c index d142e228b036..b9a6be015241 100644 --- a/tests/sys/kern/unix_seqpacket_test.c +++ b/tests/sys/kern/unix_seqpacket_test.c @@ -894,6 +894,38 @@ ATF_TC_BODY(shutdown_send_sigpipe, tc) close(s2); } +/* + * https://syzkaller.appspot.com/bug?id=ac94349a29f2efc40e9274239e4ca9b2c473a4e7 + */ +ATF_TC_WITHOUT_HEAD(shutdown_o_async); +ATF_TC_BODY(shutdown_o_async, tc) +{ + int sv[2]; + + do_socketpair(sv); + + ATF_CHECK_EQ(0, fcntl(sv[0], F_SETFL, O_ASYNC)); + ATF_CHECK_EQ(0, shutdown(sv[0], SHUT_WR)); + close(sv[0]); + close(sv[1]); +} + +/* + * If peer had done SHUT_WR on their side, our recv(2) shouldn't block. + */ +ATF_TC_WITHOUT_HEAD(shutdown_recv); +ATF_TC_BODY(shutdown_recv, tc) +{ + char buf[10]; + int sv[2]; + + do_socketpair(sv); + ATF_CHECK_EQ(0, shutdown(sv[0], SHUT_WR)); + ATF_CHECK_EQ(0, recv(sv[1], buf, sizeof(buf), 0)); + close(sv[0]); + close(sv[1]); +} + /* nonblocking send(2) and recv(2) a single short record */ ATF_TC_WITHOUT_HEAD(send_recv_nonblocking); ATF_TC_BODY(send_recv_nonblocking, tc) @@ -1197,8 +1229,6 @@ ATF_TC_BODY(random_eor_and_waitall, tc) size_t off; int fd[2], eor; - atf_tc_skip("https://bugs.freebsd.org/279354"); - arc4random_buf(params.seed, sizeof(params.seed)); printf("Using seed:"); for (u_int i = 0; i < (u_int)sizeof(params.seed)/sizeof(u_short); i++) @@ -1312,6 +1342,8 @@ ATF_TP_ADD_TCS(tp) ATF_TP_ADD_TC(tp, implied_connect); ATF_TP_ADD_TC(tp, shutdown_send); ATF_TP_ADD_TC(tp, shutdown_send_sigpipe); + ATF_TP_ADD_TC(tp, shutdown_o_async); + ATF_TP_ADD_TC(tp, shutdown_recv); ATF_TP_ADD_TC(tp, eagain_8k_8k); ATF_TP_ADD_TC(tp, eagain_8k_128k); ATF_TP_ADD_TC(tp, eagain_128k_8k); diff --git a/tests/sys/kern/unix_stream.c b/tests/sys/kern/unix_stream.c index d93bbeff4e41..bb811f78f620 100644 --- a/tests/sys/kern/unix_stream.c +++ b/tests/sys/kern/unix_stream.c @@ -28,6 +28,7 @@ #include <sys/cdefs.h> #include <sys/socket.h> #include <sys/event.h> +#include <sys/select.h> #include <sys/sysctl.h> #include <sys/un.h> #include <errno.h> @@ -35,6 +36,8 @@ #include <stdio.h> #include <stdlib.h> #include <poll.h> +#include <pthread.h> +#include <pthread_np.h> #include <atf-c.h> @@ -99,48 +102,83 @@ ATF_TC_BODY(send_0, tc) close(sv[1]); } +struct check_ctx; +typedef void check_func_t(struct check_ctx *); +struct check_ctx { + check_func_t *method; + int sv[2]; + bool timeout; + union { + enum { SELECT_RD, SELECT_WR } select_what; + short poll_events; + short kev_filter; + }; + int nfds; + union { + short poll_revents; + unsigned short kev_flags; + }; +}; + static void -check_writable(int fd, int expect) +check_select(struct check_ctx *ctx) { - fd_set wrfds; - struct pollfd pfd[1]; - struct kevent kev; - int nfds, kq; + fd_set fds; + int nfds; - FD_ZERO(&wrfds); - FD_SET(fd, &wrfds); - nfds = select(fd + 1, NULL, &wrfds, NULL, - &(struct timeval){.tv_usec = 1000}); - ATF_REQUIRE_MSG(nfds == expect, + FD_ZERO(&fds); + FD_SET(ctx->sv[0], &fds); + nfds = select(ctx->sv[0] + 1, + ctx->select_what == SELECT_RD ? &fds : NULL, + ctx->select_what == SELECT_WR ? &fds : NULL, + NULL, + ctx->timeout ? &(struct timeval){.tv_usec = 1000} : NULL); + ATF_REQUIRE_MSG(nfds == ctx->nfds, "select() returns %d errno %d", nfds, errno); +} + +static void +check_poll(struct check_ctx *ctx) +{ + struct pollfd pfd[1]; + int nfds; pfd[0] = (struct pollfd){ - .fd = fd, - .events = POLLOUT | POLLWRNORM, + .fd = ctx->sv[0], + .events = ctx->poll_events, }; - nfds = poll(pfd, 1, 1); - ATF_REQUIRE_MSG(nfds == expect, + nfds = poll(pfd, 1, ctx->timeout ? 1 : INFTIM); + ATF_REQUIRE_MSG(nfds == ctx->nfds, "poll() returns %d errno %d", nfds, errno); + ATF_REQUIRE((pfd[0].revents & ctx->poll_revents) == ctx->poll_revents); +} + +static void +check_kevent(struct check_ctx *ctx) +{ + struct kevent kev; + int nfds, kq; ATF_REQUIRE(kq = kqueue()); - EV_SET(&kev, fd, EVFILT_WRITE, EV_ADD, 0, 0, NULL); - ATF_REQUIRE(kevent(kq, &kev, 1, NULL, 0, NULL) == 0); - nfds = kevent(kq, NULL, 0, &kev, 1, - &(struct timespec){.tv_nsec = 1000000}); - ATF_REQUIRE_MSG(nfds == expect, - "kevent() returns %d errno %d", nfds, errno); + EV_SET(&kev, ctx->sv[0], ctx->kev_filter, EV_ADD, 0, 0, NULL); + nfds = kevent(kq, &kev, 1, NULL, 0, NULL); + ATF_REQUIRE_MSG(nfds == 0, + "kevent() returns %d errno %d", nfds, errno); + nfds = kevent(kq, NULL, 0, &kev, 1, ctx->timeout ? + &(struct timespec){.tv_nsec = 1000000} : NULL); + ATF_REQUIRE_MSG(nfds == ctx->nfds, + "kevent() returns %d errno %d", nfds, errno); + ATF_REQUIRE(kev.ident == (uintptr_t)ctx->sv[0] && + kev.filter == ctx->kev_filter && + (kev.flags & ctx->kev_flags) == ctx->kev_flags); close(kq); } -/* - * Make sure that a full socket is not reported as writable by event APIs. - */ -ATF_TC_WITHOUT_HEAD(full_not_writable); -ATF_TC_BODY(full_not_writable, tc) +static void +full_socketpair(int *sv) { void *buf; u_long sendspace; - int sv[2]; sendspace = getsendspace(); ATF_REQUIRE((buf = malloc(sendspace)) != NULL); @@ -149,24 +187,301 @@ ATF_TC_BODY(full_not_writable, tc) do {} while (send(sv[0], buf, sendspace, 0) == (ssize_t)sendspace); ATF_REQUIRE(errno == EAGAIN); ATF_REQUIRE(fcntl(sv[0], F_SETFL, 0) != -1); + free(buf); +} + +static void * +pthread_wrap(void *arg) +{ + struct check_ctx *ctx = arg; + + ctx->method(ctx); + + return (NULL); +} - check_writable(sv[0], 0); +/* + * Launch a thread that would block in event mech and return it. + */ +static pthread_t +pthread_create_blocked(struct check_ctx *ctx) +{ + pthread_t thr; + + ctx->timeout = false; + ctx->nfds = 1; + ATF_REQUIRE(pthread_create(&thr, NULL, pthread_wrap, ctx) == 0); + + /* Sleep a bit to make sure that thread is put to sleep. */ + usleep(10000); + ATF_REQUIRE(pthread_peekjoin_np(thr, NULL) == EBUSY); + + return (thr); +} + +static void +full_writability_check(struct check_ctx *ctx) +{ + pthread_t thr; + void *buf; + u_long space; + + space = getsendspace() / 2; + ATF_REQUIRE((buf = malloc(space)) != NULL); + + /* First check with timeout, expecting 0 fds returned. */ + ctx->timeout = true; + ctx->nfds = 0; + ctx->method(ctx); + + thr = pthread_create_blocked(ctx); + + /* Read some data and re-check, the fd is expected to be returned. */ + ATF_REQUIRE(read(ctx->sv[1], buf, space) == (ssize_t)space); - /* Read some data and re-check. */ - ATF_REQUIRE(read(sv[1], buf, sendspace / 2) == (ssize_t)sendspace / 2); + /* Now check that thread was successfully woken up and exited. */ + ATF_REQUIRE(pthread_join(thr, NULL) == 0); - check_writable(sv[0], 1); + /* Extra check repeating what joined thread already did. */ + ctx->method(ctx); + close(ctx->sv[0]); + close(ctx->sv[1]); free(buf); - close(sv[0]); - close(sv[1]); +} + +/* + * Make sure that a full socket is not reported as writable by event APIs. + */ +ATF_TC_WITHOUT_HEAD(full_writability_select); +ATF_TC_BODY(full_writability_select, tc) +{ + struct check_ctx ctx = { + .method = check_select, + .select_what = SELECT_WR, + }; + + full_socketpair(ctx.sv); + full_writability_check(&ctx); + close(ctx.sv[0]); + close(ctx.sv[1]); +} + +ATF_TC_WITHOUT_HEAD(full_writability_poll); +ATF_TC_BODY(full_writability_poll, tc) +{ + struct check_ctx ctx = { + .method = check_poll, + .poll_events = POLLOUT | POLLWRNORM, + }; + + full_socketpair(ctx.sv); + full_writability_check(&ctx); + close(ctx.sv[0]); + close(ctx.sv[1]); +} + +ATF_TC_WITHOUT_HEAD(full_writability_kevent); +ATF_TC_BODY(full_writability_kevent, tc) +{ + struct check_ctx ctx = { + .method = check_kevent, + .kev_filter = EVFILT_WRITE, + }; + + full_socketpair(ctx.sv); + full_writability_check(&ctx); + close(ctx.sv[0]); + close(ctx.sv[1]); +} + +ATF_TC_WITHOUT_HEAD(connected_writability); +ATF_TC_BODY(connected_writability, tc) +{ + struct check_ctx ctx = { + .timeout = true, + .nfds = 1, + }; + + do_socketpair(ctx.sv); + + ctx.select_what = SELECT_WR; + check_select(&ctx); + ctx.poll_events = POLLOUT | POLLWRNORM; + check_poll(&ctx); + ctx.kev_filter = EVFILT_WRITE; + check_kevent(&ctx); + + close(ctx.sv[0]); + close(ctx.sv[1]); +} + +ATF_TC_WITHOUT_HEAD(unconnected_writability); +ATF_TC_BODY(unconnected_writability, tc) +{ + struct check_ctx ctx = { + .timeout = true, + .nfds = 0, + }; + + ATF_REQUIRE((ctx.sv[0] = socket(PF_LOCAL, SOCK_STREAM, 0)) > 0); + + ctx.select_what = SELECT_WR; + check_select(&ctx); + ctx.poll_events = POLLOUT | POLLWRNORM; + check_poll(&ctx); + ctx.kev_filter = EVFILT_WRITE; + check_kevent(&ctx); + + close(ctx.sv[0]); +} + +ATF_TC_WITHOUT_HEAD(peerclosed_writability); +ATF_TC_BODY(peerclosed_writability, tc) +{ + struct check_ctx ctx = { + .timeout = false, + .nfds = 1, + }; + + do_socketpair(ctx.sv); + close(ctx.sv[1]); + + ctx.select_what = SELECT_WR; + check_select(&ctx); + ctx.poll_events = POLLOUT | POLLWRNORM; + check_poll(&ctx); + ctx.kev_filter = EVFILT_WRITE; + ctx.kev_flags = EV_EOF; + check_kevent(&ctx); + + close(ctx.sv[0]); +} + +ATF_TC_WITHOUT_HEAD(peershutdown_writability); +ATF_TC_BODY(peershutdown_writability, tc) +{ + struct check_ctx ctx = { + .timeout = false, + .nfds = 1, + }; + + do_socketpair(ctx.sv); + shutdown(ctx.sv[1], SHUT_RD); + + ctx.select_what = SELECT_WR; + check_select(&ctx); + ctx.poll_events = POLLOUT | POLLWRNORM; + check_poll(&ctx); + /* + * XXXGL: historically unix(4) sockets were not reporting peer's + * shutdown(SHUT_RD) as our EV_EOF. The kevent(2) manual page says + * "filter will set EV_EOF when the reader disconnects", which is hard + * to interpret unambigously. For now leave the historic behavior, + * but we may want to change that in uipc_usrreq.c:uipc_filt_sowrite(), + * and then this test will also expect EV_EOF in returned flags. + */ + ctx.kev_filter = EVFILT_WRITE; + check_kevent(&ctx); + + close(ctx.sv[0]); + close(ctx.sv[1]); +} + +ATF_TC_WITHOUT_HEAD(peershutdown_readability); +ATF_TC_BODY(peershutdown_readability, tc) +{ + struct check_ctx ctx = { + .timeout = false, + .nfds = 1, + }; + ssize_t readsz; + char c; + + do_socketpair(ctx.sv); + shutdown(ctx.sv[1], SHUT_WR); + + /* + * The other side should flag as readable in select(2) to allow it to + * read(2) and observe EOF. Ensure that both poll(2) and select(2) + * are consistent here. + */ + ctx.select_what = SELECT_RD; + check_select(&ctx); + ctx.poll_events = POLLIN | POLLRDNORM; + check_poll(&ctx); + + /* + * Also check that read doesn't block. + */ + readsz = read(ctx.sv[0], &c, sizeof(c)); + ATF_REQUIRE_INTEQ(0, readsz); + + close(ctx.sv[0]); + close(ctx.sv[1]); +} + +static void +peershutdown_wakeup(struct check_ctx *ctx) +{ + pthread_t thr; + + ctx->timeout = false; + ctx->nfds = 1; + + do_socketpair(ctx->sv); + thr = pthread_create_blocked(ctx); + shutdown(ctx->sv[1], SHUT_WR); + ATF_REQUIRE(pthread_join(thr, NULL) == 0); + + close(ctx->sv[0]); + close(ctx->sv[1]); +} + +ATF_TC_WITHOUT_HEAD(peershutdown_wakeup_select); +ATF_TC_BODY(peershutdown_wakeup_select, tc) +{ + peershutdown_wakeup(&(struct check_ctx){ + .method = check_select, + .select_what = SELECT_RD, + }); +} + +ATF_TC_WITHOUT_HEAD(peershutdown_wakeup_poll); +ATF_TC_BODY(peershutdown_wakeup_poll, tc) +{ + peershutdown_wakeup(&(struct check_ctx){ + .method = check_poll, + .poll_events = POLLIN | POLLRDNORM | POLLRDHUP, + .poll_revents = POLLRDHUP, + }); +} + +ATF_TC_WITHOUT_HEAD(peershutdown_wakeup_kevent); +ATF_TC_BODY(peershutdown_wakeup_kevent, tc) +{ + peershutdown_wakeup(&(struct check_ctx){ + .method = check_kevent, + .kev_filter = EVFILT_READ, + .kev_flags = EV_EOF, + }); } ATF_TP_ADD_TCS(tp) { ATF_TP_ADD_TC(tp, getpeereid); ATF_TP_ADD_TC(tp, send_0); - ATF_TP_ADD_TC(tp, full_not_writable); + ATF_TP_ADD_TC(tp, connected_writability); + ATF_TP_ADD_TC(tp, unconnected_writability); + ATF_TP_ADD_TC(tp, full_writability_select); + ATF_TP_ADD_TC(tp, full_writability_poll); + ATF_TP_ADD_TC(tp, full_writability_kevent); + ATF_TP_ADD_TC(tp, peerclosed_writability); + ATF_TP_ADD_TC(tp, peershutdown_writability); + ATF_TP_ADD_TC(tp, peershutdown_readability); + ATF_TP_ADD_TC(tp, peershutdown_wakeup_select); + ATF_TP_ADD_TC(tp, peershutdown_wakeup_poll); + ATF_TP_ADD_TC(tp, peershutdown_wakeup_kevent); return atf_no_error(); } |