opnsense-src/tests/sys/kern/prace.c
Dag-Erling Smørgrav 6748d4e0eb tests: Add regression test for ppoll() / pselect() race.
These tests demonstrate the bug that was fixed in ccb973da1f.

Sponsored by:	Klara, Inc.
Sponsored by:	NetApp, Inc.
Reviewed by:	markj
Differential Revision:	https://reviews.freebsd.org/D47738
2024-11-26 21:36:09 +01:00

144 lines
3.3 KiB
C

/*-
* 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 <sys/select.h>
#include <sys/wait.h>
#include <err.h>
#include <errno.h>
#include <poll.h>
#include <signal.h>
#include <stdbool.h>
#include <stdlib.h>
#include <unistd.h>
#include <atf-c.h>
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());
}