diff --git a/tests/sys/kern/Makefile b/tests/sys/kern/Makefile index b2d133a0457f..8785caf4e293 100644 --- a/tests/sys/kern/Makefile +++ b/tests/sys/kern/Makefile @@ -1,133 +1,134 @@ .include PACKAGE= tests TESTSRC= ${SRCTOP}/contrib/netbsd-tests/kernel .PATH: ${SRCTOP}/sys/kern TESTSDIR= ${TESTSBASE}/sys/kern ATF_TESTS_C+= basic_signal .if ${MACHINE_ARCH} != "i386" && ${MACHINE_ARCH} != "powerpc" && \ ${MACHINE_ARCH} != "powerpcspe" # No support for atomic_load_64 on i386 or (32-bit) powerpc ATF_TESTS_C+= kcov .endif ATF_TESTS_C+= kern_copyin ATF_TESTS_C+= kern_descrip_test ATF_TESTS_C+= fdgrowtable_test ATF_TESTS_C+= kill_zombie .if ${MK_OPENSSL} != "no" ATF_TESTS_C+= ktls_test .endif ATF_TESTS_C+= ktrace_test ATF_TESTS_C+= listener_wakeup ATF_TESTS_C+= module_test +ATF_TESTS_C+= prace ATF_TESTS_C+= ptrace_test TEST_METADATA.ptrace_test+= timeout="15" ATF_TESTS_C+= reaper ATF_TESTS_C+= sched_affinity ATF_TESTS_C+= shutdown_dgram ATF_TESTS_C+= sigaltstack ATF_TESTS_C+= sigwait ATF_TESTS_C+= socket_accept ATF_TESTS_C+= socket_accf ATF_TESTS_C+= socket_msg_trunc ATF_TESTS_C+= socket_msg_waitall ATF_TESTS_C+= socket_splice TEST_METADATA.sigwait+= is_exclusive="true" .if ${MACHINE_ARCH} != "i386" && ${MACHINE_ARCH:Mpowerpc*} == "" ATF_TESTS_C+= subr_physmem_test .endif PLAIN_TESTS_C+= subr_unit_test ATF_TESTS_C+= sysctl_kern_proc ATF_TESTS_C+= sys_getrandom ATF_TESTS_C+= tty_pts ATF_TESTS_C+= unix_dgram ATF_TESTS_C+= unix_passfd_dgram TEST_METADATA.unix_passfd_dgram+= is_exclusive="true" ATF_TESTS_C+= unix_passfd_stream TEST_METADATA.unix_passfd_stream+= is_exclusive="true" ATF_TESTS_C+= unix_seqpacket_test TEST_METADATA.unix_seqpacket_test+= timeout="15" ATF_TESTS_C+= unix_stream ATF_TESTS_C+= waitpid_nohang ATF_TESTS_C+= pdeathsig ATF_TESTS_C+= sigsys TEST_METADATA.sigsys+= is_exclusive="true" ATF_TESTS_SH+= coredump_phnum_test ATF_TESTS_SH+= sonewconn_overflow TEST_METADATA.sonewconn_overflow+= required_programs="python" TEST_METADATA.sonewconn_overflow+= required_user="root" TEST_METADATA.sonewconn_overflow+= is_exclusive="true" ATF_TESTS_SH+= sendfile_test ATF_TESTS_SH+= sysctl_security_jail_children ${PACKAGE}FILES+= sonewconn_overflow.py ${PACKAGE}FILESMODE_sonewconn_overflow.py=0555 BINDIR= ${TESTSDIR} PROGS+= coredump_phnum_helper PROGS+= pdeathsig_helper PROGS+= sendfile_helper 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.kcov+= pthread CFLAGS.ktls_test+= -DOPENSSL_API_COMPAT=0x10100000L LIBADD.ktls_test+= crypto util LIBADD.listener_wakeup+= pthread LIBADD.shutdown_dgram+= pthread LIBADD.socket_msg_waitall+= pthread LIBADD.socket_splice+= pthread LIBADD.sendfile_helper+= pthread LIBADD.fdgrowtable_test+= util pthread kvm procstat LIBADD.sigwait+= rt LIBADD.ktrace_test+= sysdecode NETBSD_ATF_TESTS_C+= lockf_test NETBSD_ATF_TESTS_C+= mqueue_test NETBSD_ATF_TESTS_C+= sysv_test CFLAGS.mqueue_test+= -I${SRCTOP}/tests LIBADD.mqueue_test+= rt LIBADD.tty_pts+= atf_c util ATF_TESTS_C+= libkern_crc32 SRCS.libkern_crc32+= libkern_crc32.c .PATH: ${SRCTOP}/sys/libkern SRCS.libkern_crc32+= gsb_crc32.c CFLAGS.libkern_crc32+= -DTESTING .if ${MACHINE_ARCH} == "amd64" || ${MACHINE_ARCH} == "i386" .PATH: ${SRCTOP}/sys/libkern/x86 SRCS.libkern_crc32+= crc32_sse42.c .elif ${MACHINE_CPUARCH} == "aarch64" .PATH: ${SRCTOP}/sys/libkern/arm64 SRCS.libkern_crc32+= crc32c_armv8.S .endif CFLAGS.subr_physmem.c+= -D_WANT_FREEBSD_BITSET SRCS.subr_physmem_test+= subr_physmem_test.c subr_physmem.c # subr_unit.c contains functions whose prototypes lie in headers that cannot be # included in userland. But as far as subr_unit_test goes, they're effectively # static. So it's ok to disable -Wmissing-prototypes for this program. CFLAGS.subr_unit.c+= -Wno-missing-prototypes SRCS.subr_unit_test+= subr_unit.c WARNS?= 3 TESTS_SUBDIRS+= acct TESTS_SUBDIRS+= execve TESTS_SUBDIRS+= pipe TESTS_SUBDIRS+= tty .include .include diff --git a/tests/sys/kern/prace.c b/tests/sys/kern/prace.c new file mode 100644 index 000000000000..e6aa09ec2180 --- /dev/null +++ b/tests/sys/kern/prace.c @@ -0,0 +1,144 @@ +/*- + * Copyright (c) 2024 Klara, Inc. + * + * SPDX-License-Identifier: BSD-2-Clause + * + * These tests demonstrate a bug in ppoll() and pselect() where a blocked + * signal can fire after the timer runs out but before the signal mask is + * restored. To do this, we fork a child process which installs a SIGINT + * handler and repeatedly calls either ppoll() or pselect() with a 1 ms + * timeout, while the parent repeatedly sends SIGINT to the child at + * intervals that start out at 1100 us and gradually decrease to 900 us. + * Each SIGINT resynchronizes parent and child, and sooner or later the + * parent hits the sweet spot and the SIGINT arrives at just the right + * time to demonstrate the bug. + */ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +static volatile sig_atomic_t caught[NSIG]; + +static void +handler(int signo) +{ + caught[signo]++; +} + +static void +child(int rd, bool poll) +{ + struct timespec timeout = { .tv_nsec = 1000000 }; + sigset_t set0, set1; + int ret; + + /* empty mask for ppoll() / pselect() */ + sigemptyset(&set0); + + /* block SIGINT, then install a handler for it */ + sigemptyset(&set1); + sigaddset(&set1, SIGINT); + sigprocmask(SIG_BLOCK, &set1, NULL); + signal(SIGINT, handler); + + /* signal parent that we are ready */ + close(rd); + for (;;) { + /* sleep for 1 ms with signals unblocked */ + ret = poll ? ppoll(NULL, 0, &timeout, &set0) : + pselect(0, NULL, NULL, NULL, &timeout, &set0); + /* + * At this point, either ret == 0 (timer ran out) errno == + * EINTR (a signal was received). Any other outcome is + * abnormal. + */ + if (ret != 0 && errno != EINTR) + err(1, "p%s()", poll ? "poll" : "select"); + /* if ret == 0, we should not have caught any signals */ + if (ret == 0 && caught[SIGINT]) { + /* + * We successfully demonstrated the race. Restore + * the default action and re-raise SIGINT. + */ + signal(SIGINT, SIG_DFL); + raise(SIGINT); + /* Not reached */ + } + /* reset for next attempt */ + caught[SIGINT] = 0; + } + /* Not reached */ +} + +static void +prace(bool poll) +{ + int pd[2], status; + pid_t pid; + + /* fork child process */ + if (pipe(pd) != 0) + err(1, "pipe()"); + if ((pid = fork()) < 0) + err(1, "fork()"); + if (pid == 0) { + close(pd[0]); + child(pd[1], poll); + /* Not reached */ + } + close(pd[1]); + + /* wait for child to signal readiness */ + (void)read(pd[0], &pd[0], sizeof(pd[0])); + close(pd[0]); + + /* repeatedly attempt to signal at just the right moment */ + for (useconds_t timeout = 1100; timeout > 900; timeout--) { + usleep(timeout); + if (kill(pid, SIGINT) != 0) { + if (errno != ENOENT) + err(1, "kill()"); + /* ENOENT means the child has terminated */ + break; + } + } + + /* we're done, kill the child for sure */ + (void)kill(pid, SIGKILL); + if (waitpid(pid, &status, 0) < 0) + err(1, "waitpid()"); + + /* assert that the child died of SIGKILL */ + ATF_REQUIRE(WIFSIGNALED(status)); + ATF_REQUIRE_MSG(WTERMSIG(status) == SIGKILL, + "child caught SIG%s", sys_signame[WTERMSIG(status)]); +} + +ATF_TC_WITHOUT_HEAD(ppoll_race); +ATF_TC_BODY(ppoll_race, tc) +{ + prace(true); +} + +ATF_TC_WITHOUT_HEAD(pselect_race); +ATF_TC_BODY(pselect_race, tc) +{ + prace(false); +} + +ATF_TP_ADD_TCS(tp) +{ + ATF_TP_ADD_TC(tp, ppoll_race); + ATF_TP_ADD_TC(tp, pselect_race); + return (atf_no_error()); +}