git: 6748d4e0eb8a - main - tests: Add regression test for ppoll() / pselect() race.

From: Dag-Erling Smørgrav <des_at_FreeBSD.org>
Date: Tue, 26 Nov 2024 20:37:18 UTC
The branch main has been updated by des:

URL: https://cgit.FreeBSD.org/src/commit/?id=6748d4e0eb8ab918d55f69ae59a60ecbac8df85f

commit 6748d4e0eb8ab918d55f69ae59a60ecbac8df85f
Author:     Dag-Erling Smørgrav <des@FreeBSD.org>
AuthorDate: 2024-11-26 20:36:09 +0000
Commit:     Dag-Erling Smørgrav <des@FreeBSD.org>
CommitDate: 2024-11-26 20:36:09 +0000

    tests: Add regression test for ppoll() / pselect() race.
    
    These tests demonstrate the bug that was fixed in ccb973da1f1b.
    
    Sponsored by:   Klara, Inc.
    Sponsored by:   NetApp, Inc.
    Reviewed by:    markj
    Differential Revision:  https://reviews.freebsd.org/D47738
---
 tests/sys/kern/Makefile |   1 +
 tests/sys/kern/prace.c  | 144 ++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 145 insertions(+)

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
@@ -23,6 +23,7 @@ ATF_TESTS_C+=	ktls_test
 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
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 <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());
+}