git: 7e6ac503ffeb - main - libthr: add some tests for pthread_atfork() handling

From: Kyle Evans <kevans_at_FreeBSD.org>
Date: Thu, 14 Nov 2024 01:34:25 UTC
The branch main has been updated by kevans:

URL: https://cgit.FreeBSD.org/src/commit/?id=7e6ac503ffeb81733272d54af367db58e45e57ca

commit 7e6ac503ffeb81733272d54af367db58e45e57ca
Author:     Kyle Evans <kevans@FreeBSD.org>
AuthorDate: 2024-11-14 01:33:36 +0000
Commit:     Kyle Evans <kevans@FreeBSD.org>
CommitDate: 2024-11-14 01:33:37 +0000

    libthr: add some tests for pthread_atfork() handling
    
    Test that it generally functions, and also that registering multiple
    times calls each handler in the order that it's documented to call them
    in.
    
    Reviewed by:    kib, markj
    Differential Revision:  https://reviews.freebsd.org/D47348
---
 lib/libthr/tests/Makefile      |   1 +
 lib/libthr/tests/atfork_test.c | 227 +++++++++++++++++++++++++++++++++++++++++
 2 files changed, 228 insertions(+)

diff --git a/lib/libthr/tests/Makefile b/lib/libthr/tests/Makefile
index aadf0a4d80ca..017b740157dc 100644
--- a/lib/libthr/tests/Makefile
+++ b/lib/libthr/tests/Makefile
@@ -33,6 +33,7 @@ NETBSD_ATF_TESTS_SH+=	cancel_test
 NETBSD_ATF_TESTS_SH+=	exit_test
 NETBSD_ATF_TESTS_SH+=	resolv_test
 
+ATF_TESTS_C+=		atfork_test
 ATF_TESTS_C+=		umtx_op_test
 ATF_TESTS_C+=		pthread_sigqueue_test
 
diff --git a/lib/libthr/tests/atfork_test.c b/lib/libthr/tests/atfork_test.c
new file mode 100644
index 000000000000..5133330b1247
--- /dev/null
+++ b/lib/libthr/tests/atfork_test.c
@@ -0,0 +1,227 @@
+/*-
+ *
+ * Copyright (C) 2024 Kyle Evans <kevans@FreeBSD.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ */
+
+#include <sys/wait.h>
+#include <errno.h>
+#include <pthread.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include <atf-c.h>
+
+#define	EXIT_NOPREPARE		1
+#define	EXIT_CALLEDPARENT	2
+#define	EXIT_NOCHILD		3
+#define	EXIT_BADORDER		4
+
+static int child;
+static int forked;
+static int parent;
+
+static void
+basic_prepare(void)
+{
+	ATF_REQUIRE(parent == 0);
+	forked++;
+}
+
+static void
+basic_parent(void)
+{
+	ATF_REQUIRE(forked != 0);
+	parent++;
+}
+
+static void
+basic_child(void)
+{
+	if (!forked)
+		_exit(EXIT_NOPREPARE);
+	if (parent != 0)
+		_exit(EXIT_CALLEDPARENT);
+	child++;
+}
+
+/*
+ * In the basic test, we'll register just once and set some globals to confirm
+ * that the prepare/parent callbacks were executed as expected.  The child will
+ * use its exit status to communicate to us if the callback was not executed
+ * properly since we cannot assert there.  This is a subset of the
+ * multi-callback test, but separated out so that it's more obvious from running
+ * the atfork_test if pthread_atfork() is completely broken or just
+ * out-of-order.
+ */
+ATF_TC(basic_atfork);
+ATF_TC_HEAD(basic_atfork, tc)
+{
+	atf_tc_set_md_var(tc, "descr",
+	    "Checks invocation of all three atfork callbacks");
+}
+ATF_TC_BODY(basic_atfork, tc)
+{
+	pid_t p, wpid;
+	int status;
+
+	pthread_atfork(basic_prepare, basic_parent, basic_child);
+
+	p = fork();
+
+	ATF_REQUIRE(p >= 0);
+	if (p == 0)
+		_exit(child != 0 ? 0 : EXIT_NOCHILD);
+
+	/*
+	 * The child can't use any of our standard atf-c(3) macros, so we have
+	 * to rely on the exit status to convey any shenanigans.
+	 */
+	while ((wpid = waitpid(p, &status, 0)) != p) {
+		ATF_REQUIRE_ERRNO(EINTR, wpid == -1);
+		if (wpid == -1)
+			continue;
+	}
+
+	ATF_REQUIRE_MSG(WIFEXITED(status),
+	    "child did not exit cleanly, status %x", status);
+
+	status = WEXITSTATUS(status);
+	ATF_REQUIRE_MSG(status == 0, "atfork in child %s",
+	   status == EXIT_NOPREPARE ? "did not see `prepare` execute" :
+	   (status == EXIT_CALLEDPARENT ? "observed `parent` executing" :
+	   (status == EXIT_NOCHILD ? "did not see `child` execute" :
+	    "mystery")));
+
+	ATF_REQUIRE(forked != 0);
+	ATF_REQUIRE(parent != 0);
+	ATF_REQUIRE(child == 0);
+}
+
+static void
+multi_assert(bool cond, bool can_assert)
+{
+	if (can_assert)
+		ATF_REQUIRE((cond));
+	else if (!(cond))
+		_exit(EXIT_BADORDER);
+}
+
+static void
+multi_bump(int *var, int bit, bool can_assert)
+{
+	int mask, val;
+
+	mask = (1 << (bit - 1));
+	val = *var;
+
+	/*
+	 * Every bit below this one must be set, and none of the upper bits
+	 * should be set.
+	 */
+	multi_assert((val & mask) == 0, can_assert);
+	if (bit == 1)
+		multi_assert(val == 0, can_assert);
+	else
+		multi_assert((val & ~mask) == (mask - 1), can_assert);
+
+	*var |= mask;
+}
+
+static void
+multi_prepare1(void)
+{
+	/*
+	 * The bits are flipped for prepare because it's supposed to be called
+	 * in the reverse order of registration.
+	 */
+	multi_bump(&forked, 2, true);
+}
+static void
+multi_prepare2(void)
+{
+	multi_bump(&forked, 1, true);
+}
+
+static void
+multi_parent1(void)
+{
+	multi_bump(&parent, 1, true);
+}
+static void
+multi_parent2(void)
+{
+	multi_bump(&parent, 2, true);
+}
+
+static void
+multi_child1(void)
+{
+	multi_bump(&child, 1, false);
+}
+static void
+multi_child2(void)
+{
+	multi_bump(&child, 2, false);
+}
+
+/*
+ * The multi-atfork test works much like the basic one, but it registers
+ * multiple times and enforces an order.  The child still does just as strict
+ * of tests as the parent and continues to communicate the results of those
+ * tests back via its exit status.
+ */
+ATF_TC(multi_atfork);
+ATF_TC_HEAD(multi_atfork, tc)
+{
+	atf_tc_set_md_var(tc, "descr",
+	    "Checks that multiple callbacks are called in the documented order");
+}
+ATF_TC_BODY(multi_atfork, tc)
+{
+	pid_t p, wpid;
+	int status;
+
+	pthread_atfork(multi_prepare1, multi_parent1, multi_child1);
+	pthread_atfork(multi_prepare2, multi_parent2, multi_child2);
+
+	p = fork();
+
+	ATF_REQUIRE(p >= 0);
+	if (p == 0)
+		_exit(child != 0 ? 0 : EXIT_NOCHILD);
+
+	/*
+	 * The child can't use any of our standard atf-c(3) macros, so we have
+	 * to rely on the exit status to convey any shenanigans.
+	 */
+	while ((wpid = waitpid(p, &status, 0)) != p) {
+		ATF_REQUIRE_ERRNO(EINTR, wpid == -1);
+		if (wpid == -1)
+			continue;
+	}
+
+	ATF_REQUIRE_MSG(WIFEXITED(status),
+	    "child did not exit cleanly, status %x", status);
+
+	status = WEXITSTATUS(status);
+	ATF_REQUIRE_MSG(status == 0, "atfork in child %s",
+	   status == EXIT_BADORDER ? "called in wrong order" :
+	   (status == EXIT_NOCHILD ? "did not see `child` execute" :
+	    "mystery"));
+
+	ATF_REQUIRE(forked != 0);
+	ATF_REQUIRE(parent != 0);
+	ATF_REQUIRE(child == 0);
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+	ATF_TP_ADD_TC(tp, basic_atfork);
+	ATF_TP_ADD_TC(tp, multi_atfork);
+	return (atf_no_error());
+}