git: 0c9cec8b66e7 - main - tests: kqueue: add a basic test for CPONFORK

From: Kyle Evans <kevans_at_FreeBSD.org>
Date: Thu, 09 Apr 2026 02:37:33 UTC
The branch main has been updated by kevans:

URL: https://cgit.FreeBSD.org/src/commit/?id=0c9cec8b66e7033f50059329704515d5222b9ff4

commit 0c9cec8b66e7033f50059329704515d5222b9ff4
Author:     Kyle Evans <kevans@FreeBSD.org>
AuthorDate: 2026-04-09 02:37:00 +0000
Commit:     Kyle Evans <kevans@FreeBSD.org>
CommitDate: 2026-04-09 02:37:11 +0000

    tests: kqueue: add a basic test for CPONFORK
    
    Just copy over a timer and a write-filter, be sure that we can observe
    both in the child.  Maybe the timer should check for a minimum time
    passed, but I don't know that we'd be likely to get that wrong.
    
    This also adds a negative test with a kqueue that is *not* set for
    CPONFORK being added to the first one, made readable, and confirming
    that we don't see a knote for it in the child.
    
    Some other improvements to the test noted in the review are planned in
    the short term, but they're not particularly worth blocking adding this
    as a basic sanity check.
    
    Reviewed by:    kib, markj
    Differential Revision:  https://reviews.freebsd.org/D56223
---
 tests/sys/kqueue/kqueue_fork.c | 140 +++++++++++++++++++++++++++++++++++++++++
 1 file changed, 140 insertions(+)

diff --git a/tests/sys/kqueue/kqueue_fork.c b/tests/sys/kqueue/kqueue_fork.c
index e4c0412c1980..6f517a2e0e29 100644
--- a/tests/sys/kqueue/kqueue_fork.c
+++ b/tests/sys/kqueue/kqueue_fork.c
@@ -27,9 +27,13 @@
  */
 
 #include <sys/event.h>
+#include <sys/procdesc.h>
+#include <sys/stat.h>
+#include <sys/user.h>
 #include <sys/wait.h>
 
 #include <err.h>
+#include <fcntl.h>
 #include <signal.h>
 #include <unistd.h>
 
@@ -81,9 +85,145 @@ ATF_TC_BODY(shared_table_filt_sig, tc)
 	ATF_REQUIRE_EQ(WEXITSTATUS(status), 0);
 }
 
+#define	TIMER_FORKED	0
+#define	TIMER_TIMEOUT	1
+
+#define	RECV_TIMER	0x01
+#define	RECV_VNODE	0x02
+#define	RECV_CLOREAD	0x04
+#define	RECV_ERROR	0x80
+#define	RECV_ALL	(RECV_TIMER | RECV_VNODE)
+
+static int
+cponfork_notes_check(int kq, int clofd)
+{
+	struct kevent ev;
+	int error, received = 0;
+
+	EV_SET(&ev, TIMER_TIMEOUT, EVFILT_TIMER,
+	    EV_ADD | EV_ENABLE | EV_ONESHOT, NOTE_SECONDS, 4, NULL);
+	error = kevent(kq, &ev, 1, NULL, 0, NULL);
+	if (error == -1)
+		return (RECV_ERROR);
+
+	while ((received & RECV_ALL) != RECV_ALL) {
+		error = kevent(kq, NULL, 0, &ev, 1, NULL);
+		if (error < 0)
+			return (RECV_ERROR);
+		else if (error == 0)
+			break;
+
+		switch (ev.filter) {
+		case EVFILT_TIMER:
+			if (ev.ident == TIMER_TIMEOUT)
+				return (received | RECV_ERROR);
+
+			received |= RECV_TIMER;
+			break;
+		case EVFILT_VNODE:
+			received |= RECV_VNODE;
+			break;
+		case EVFILT_READ:
+			if ((int)ev.ident != clofd)
+				return (received | RECV_ERROR);
+			received |= RECV_CLOREAD;
+			break;
+		}
+	}
+
+	return (received);
+}
+
+ATF_TC_WITHOUT_HEAD(cponfork_notes);
+ATF_TC_BODY(cponfork_notes, tc)
+{
+	struct kevent ev[3];
+	int clofd, dfd, error, kq, pdfd, pmask, status;
+	pid_t pid;
+
+	kq = kqueuex(KQUEUE_CPONFORK);
+	ATF_REQUIRE(kq >= 0);
+
+	dfd = open(".", O_DIRECTORY);
+	ATF_REQUIRE(dfd >= 0);
+
+	clofd = kqueue();
+	ATF_REQUIRE(clofd >= 0);
+
+	/*
+	 * Setup an event on clofd that we can trigger to make it readable,
+	 * as we'll want this ready to go when we fork to be sure that if we
+	 * *were* going to receive an event from it, it would have occurred
+	 * before the three-second timer that would normally close out the child
+	 * fires.
+	 */
+	EV_SET(&ev[0], 0, EVFILT_USER, EV_ADD | EV_ENABLE, 0, 0, NULL);
+	error = kevent(clofd, &ev[0], 1, NULL, 0, NULL);
+	ATF_REQUIRE(error != -1);
+
+	/*
+	 * Every event we setup here we should expect to observe in both the
+	 * child and the parent, with exception to the EVFILT_READ of clofd.  We
+	 * except that one to be dropped in the child when the kqueue it's
+	 * attached to goes away, thus its exclusion from the RECV_ALL mask.
+	 */
+	EV_SET(&ev[0], dfd, EVFILT_VNODE, EV_ADD | EV_ENABLE | EV_ONESHOT,
+	    NOTE_WRITE, 0, NULL);
+	EV_SET(&ev[1], TIMER_FORKED, EVFILT_TIMER, EV_ADD | EV_ENABLE | EV_ONESHOT,
+	    NOTE_SECONDS, 3, NULL);
+	EV_SET(&ev[2], clofd, EVFILT_READ, EV_ADD | EV_ENABLE | EV_ONESHOT, 0,
+	    0, NULL);
+	error = kevent(kq, &ev[0], 3, NULL, 0, NULL);
+	ATF_REQUIRE(error != -1);
+
+	/* Fire off an event to make clofd readable. */
+	EV_SET(&ev[0], 0, EVFILT_USER, 0, NOTE_TRIGGER, 0, NULL);
+	error = kevent(clofd, &ev[0], 1, NULL, 0, NULL);
+
+	/*
+	 * We're only using pdfork here for the kill-on-exit semantics, in case
+	 * the parent fails to setup some context needed for one of our events
+	 * to fire.
+	 */
+	pid = pdfork(&pdfd, 0);
+	ATF_REQUIRE(pid != -1);
+	if (pid == 0) {
+		struct kinfo_file kf = { .kf_structsize = sizeof(kf) };
+
+		if (fcntl(kq, F_KINFO, &kf) != 0)
+			_exit(RECV_ERROR);
+		else if (kf.kf_type != KF_TYPE_KQUEUE)
+			_exit(RECV_ERROR);
+
+		_exit(cponfork_notes_check(kq, clofd));
+	}
+
+	/* Setup anything we need to fire off any of our events above. */
+	error = mkdir("canary", 0755);
+	ATF_REQUIRE(error == 0);
+
+	/*
+	 * We'll simultaneously do the same exercise of polling the kqueue in
+	 * the parent, to demonstrate that forking doesn't "steal" any of the
+	 * knotes from us -- all of the events we've added are one-shot and
+	 * still fire twice (once in parent, once in child).
+	 */
+	pmask = cponfork_notes_check(kq, clofd);
+	ATF_REQUIRE_EQ(pmask, RECV_ALL | RECV_CLOREAD);
+
+	/* Wait for the child to timeout or observe the timer. */
+	_Static_assert(RECV_ALL <= UCHAR_MAX,
+	    "Too many events to observe -- switch from waitpid -> waitid");
+	error = waitpid(pid, &status, 0);
+	ATF_REQUIRE(error != -1);
+	ATF_REQUIRE(WIFEXITED(status));
+	ATF_REQUIRE_EQ(WEXITSTATUS(status), RECV_ALL);
+}
+
 ATF_TP_ADD_TCS(tp)
 {
 	ATF_TP_ADD_TC(tp, shared_table_filt_sig);
+	ATF_TP_ADD_TC(tp, cponfork_notes);
 
 	return (atf_no_error());
 }