git: df4b8eff7b19 - stable/14 - libc: tests: add some tests for __cxa_atexit handling

From: Kyle Evans <kevans_at_FreeBSD.org>
Date: Thu, 17 Apr 2025 01:05:57 UTC
The branch stable/14 has been updated by kevans:

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

commit df4b8eff7b19311f6cc5c339aebb41ecafd2e52c
Author:     Kyle Evans <kevans@FreeBSD.org>
AuthorDate: 2025-04-05 00:47:54 +0000
Commit:     Kyle Evans <kevans@FreeBSD.org>
CommitDate: 2025-04-17 01:01:46 +0000

    libc: tests: add some tests for __cxa_atexit handling
    
    This adds a basic test that __cxa_atexit works, and also adds some tests
    for __cxa_atexit handlers registered in the middle of __cxa_finalize.
    
    PR:             285870
    
    (cherry picked from commit ee9ce1078c596f5719f312feedd616ab0fb41dc9)
---
 lib/libc/tests/stdlib/Makefile               |   2 +
 lib/libc/tests/stdlib/cxa_atexit_test.c      | 132 +++++++++++++++++++++++++++
 lib/libc/tests/stdlib/libatexit/Makefile     |  11 +++
 lib/libc/tests/stdlib/libatexit/libatexit.cc |  67 ++++++++++++++
 4 files changed, 212 insertions(+)

diff --git a/lib/libc/tests/stdlib/Makefile b/lib/libc/tests/stdlib/Makefile
index 860e530389df..974bbf7c0704 100644
--- a/lib/libc/tests/stdlib/Makefile
+++ b/lib/libc/tests/stdlib/Makefile
@@ -2,6 +2,7 @@
 .include <src.opts.mk>
 
 ATF_TESTS_C+=		clearenv_test
+ATF_TESTS_C+=		cxa_atexit_test
 ATF_TESTS_C+=		dynthr_test
 ATF_TESTS_C+=		heapsort_test
 ATF_TESTS_C+=		mergesort_test
@@ -79,5 +80,6 @@ LIBADD.${t}+=	netbsd util
 LIBADD.strtod_test+=		m
 
 SUBDIR+=	dynthr_mod
+SUBDIR+=	libatexit
 
 .include <bsd.test.mk>
diff --git a/lib/libc/tests/stdlib/cxa_atexit_test.c b/lib/libc/tests/stdlib/cxa_atexit_test.c
new file mode 100644
index 000000000000..7e2cafbce850
--- /dev/null
+++ b/lib/libc/tests/stdlib/cxa_atexit_test.c
@@ -0,0 +1,132 @@
+/*-
+ * Copyright (c) 2025 Kyle Evans <kevans@FreeBSD.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <sys/wait.h>
+
+#include <dlfcn.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <atf-c.h>
+
+#define	ARBITRARY_EXIT_CODE	42
+
+static char *
+get_shlib(const char *srcdir)
+{
+	char *shlib;
+
+	shlib = NULL;
+	if (asprintf(&shlib, "%s/libatexit.so", srcdir) < 0)
+		atf_tc_fail("failed to construct path to libatexit.so");
+	return (shlib);
+}
+
+static void
+run_test(const atf_tc_t *tc, bool with_fatal_atexit, bool with_exit)
+{
+	pid_t p;
+	void (*set_fatal_atexit)(bool);
+	void (*set_exit_code)(int);
+	void *hdl;
+	char *shlib;
+
+	shlib = get_shlib(atf_tc_get_config_var(tc, "srcdir"));
+
+	hdl = dlopen(shlib, RTLD_LAZY);
+	ATF_REQUIRE_MSG(hdl != NULL, "dlopen: %s", dlerror());
+
+	free(shlib);
+
+	if (with_fatal_atexit) {
+		set_fatal_atexit = dlsym(hdl, "set_fatal_atexit");
+		ATF_REQUIRE_MSG(set_fatal_atexit != NULL,
+		    "set_fatal_atexit: %s", dlerror());
+	}
+	if (with_exit) {
+		set_exit_code = dlsym(hdl, "set_exit_code");
+		ATF_REQUIRE_MSG(set_exit_code != NULL, "set_exit_code: %s",
+		    dlerror());
+	}
+
+	p = atf_utils_fork();
+	if (p == 0) {
+		/*
+		 * Don't let the child clobber the results file; stderr/stdout
+		 * have been replaced by atf_utils_fork() to capture it.  We're
+		 * intentionally using exit() instead of _exit() here to run
+		 * __cxa_finalize at exit, otherwise we'd just leave it be.
+		 */
+		closefrom(3);
+
+		if (with_fatal_atexit)
+			set_fatal_atexit(true);
+		if (with_exit)
+			set_exit_code(ARBITRARY_EXIT_CODE);
+
+		dlclose(hdl);
+
+		/*
+		 * If the dtor was supposed to exit (most cases), then we should
+		 * not have made it to this point.  If it's not supposed to
+		 * exit, then we just exit with success here because we might
+		 * be expecting either a clean exit or a signal on our way out
+		 * as the final __cxa_finalize tries to run a callback in the
+		 * unloaded DSO.
+		 */
+		if (with_exit)
+			exit(1);
+		exit(0);
+	}
+
+	dlclose(hdl);
+	atf_utils_wait(p, with_exit ? ARBITRARY_EXIT_CODE : 0, "", "");
+}
+
+ATF_TC_WITHOUT_HEAD(simple_cxa_atexit);
+ATF_TC_BODY(simple_cxa_atexit, tc)
+{
+	/*
+	 * This test exits in a global object's dtor so that we check for our
+	 * dtor being run at dlclose() time.  If it isn't, then the forked child
+	 * will have a chance to exit(1) after dlclose() to raise a failure.
+	 */
+	run_test(tc, false, true);
+}
+
+ATF_TC_WITHOUT_HEAD(late_cxa_atexit);
+ATF_TC_BODY(late_cxa_atexit, tc)
+{
+	/*
+	 * This test creates another global object during a __cxa_atexit handler
+	 * invocation.  It's been observed in the wild that we weren't executing
+	 * it, then the DSO gets torn down and it was executed at application
+	 * exit time instead.  In the best case scenario we would crash if
+	 * something else hadn't been mapped there.
+	 */
+	run_test(tc, true, false);
+}
+
+ATF_TC_WITHOUT_HEAD(late_cxa_atexit_ran);
+ATF_TC_BODY(late_cxa_atexit_ran, tc)
+{
+	/*
+	 * This is a slight variation of the previous test where we trigger an
+	 * exit() in our late-registered __cxa_atexit handler so that we can
+	 * ensure it was ran *before* dlclose() finished and not through some
+	 * weird chain of events afterwards.
+	 */
+	run_test(tc, true, true);
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+	ATF_TP_ADD_TC(tp, simple_cxa_atexit);
+	ATF_TP_ADD_TC(tp, late_cxa_atexit);
+	ATF_TP_ADD_TC(tp, late_cxa_atexit_ran);
+	return (atf_no_error());
+}
diff --git a/lib/libc/tests/stdlib/libatexit/Makefile b/lib/libc/tests/stdlib/libatexit/Makefile
new file mode 100644
index 000000000000..9ba04c77af62
--- /dev/null
+++ b/lib/libc/tests/stdlib/libatexit/Makefile
@@ -0,0 +1,11 @@
+SHLIB_CXX=		libatexit
+SHLIB_NAME=		libatexit.so
+SHLIB_MAJOR=		1
+SHLIBDIR=	${TESTSDIR}
+PACKAGE=	tests
+SRCS=		libatexit.cc
+
+TESTSDIR:=	${TESTSBASE}/${RELDIR:C/libc\/tests/libc/:H}
+
+
+.include <bsd.lib.mk>
diff --git a/lib/libc/tests/stdlib/libatexit/libatexit.cc b/lib/libc/tests/stdlib/libatexit/libatexit.cc
new file mode 100644
index 000000000000..bb286c97e421
--- /dev/null
+++ b/lib/libc/tests/stdlib/libatexit/libatexit.cc
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2025 Kyle Evans <kevans@FreeBSD.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ */
+
+#include <unistd.h>
+
+static int exit_code = -1;
+static bool fatal_atexit;
+
+extern "C" {
+	void set_fatal_atexit(bool);
+	void set_exit_code(int);
+}
+
+void
+set_fatal_atexit(bool fexit)
+{
+	fatal_atexit = fexit;
+}
+
+void
+set_exit_code(int code)
+{
+	exit_code = code;
+}
+
+struct other_object {
+	~other_object() {
+
+		/*
+		 * In previous versions of our __cxa_atexit handling, we would
+		 * never actually execute this handler because it's added during
+		 * ~object() below; __cxa_finalize would never revisit it.  We
+		 * will allow the caller to configure us to exit with a certain
+		 * exit code so that it can run us twice: once to ensure we
+		 * don't crash at the end, and again to make sure the handler
+		 * actually ran.
+		 */
+		if (exit_code != -1)
+			_exit(exit_code);
+	}
+};
+
+void
+create_staticobj()
+{
+	static other_object obj;
+}
+
+struct object {
+	~object() {
+		/*
+		 * If we're doing the fatal_atexit behavior (i.e., create an
+		 * object that will add its own dtor for __cxa_finalize), then
+		 * we don't exit here.
+		 */
+		if (fatal_atexit)
+			create_staticobj();
+		else if (exit_code != -1)
+			_exit(exit_code);
+	}
+};
+
+static object obj;