1 /*-
2 * Copyright (c) 2025 Kyle Evans <kevans@FreeBSD.org>
3 *
4 * SPDX-License-Identifier: BSD-2-Clause
5 */
6
7 #include <sys/wait.h>
8
9 #include <dlfcn.h>
10 #include <stdbool.h>
11 #include <stdio.h>
12 #include <stdlib.h>
13
14 #include <atf-c.h>
15
16 #define ARBITRARY_EXIT_CODE 42
17
18 static char *
get_shlib(const char * srcdir)19 get_shlib(const char *srcdir)
20 {
21 char *shlib;
22
23 shlib = NULL;
24 if (asprintf(&shlib, "%s/libatexit.so", srcdir) < 0)
25 atf_tc_fail("failed to construct path to libatexit.so");
26 return (shlib);
27 }
28
29 static void
run_test(const atf_tc_t * tc,bool with_fatal_atexit,bool with_exit)30 run_test(const atf_tc_t *tc, bool with_fatal_atexit, bool with_exit)
31 {
32 pid_t p;
33 void (*set_fatal_atexit)(bool);
34 void (*set_exit_code)(int);
35 void *hdl;
36 char *shlib;
37
38 shlib = get_shlib(atf_tc_get_config_var(tc, "srcdir"));
39
40 hdl = dlopen(shlib, RTLD_LAZY);
41 ATF_REQUIRE_MSG(hdl != NULL, "dlopen: %s", dlerror());
42
43 free(shlib);
44
45 if (with_fatal_atexit) {
46 set_fatal_atexit = dlsym(hdl, "set_fatal_atexit");
47 ATF_REQUIRE_MSG(set_fatal_atexit != NULL,
48 "set_fatal_atexit: %s", dlerror());
49 }
50 if (with_exit) {
51 set_exit_code = dlsym(hdl, "set_exit_code");
52 ATF_REQUIRE_MSG(set_exit_code != NULL, "set_exit_code: %s",
53 dlerror());
54 }
55
56 p = atf_utils_fork();
57 if (p == 0) {
58 /*
59 * Don't let the child clobber the results file; stderr/stdout
60 * have been replaced by atf_utils_fork() to capture it. We're
61 * intentionally using exit() instead of _exit() here to run
62 * __cxa_finalize at exit, otherwise we'd just leave it be.
63 */
64 closefrom(3);
65
66 if (with_fatal_atexit)
67 set_fatal_atexit(true);
68 if (with_exit)
69 set_exit_code(ARBITRARY_EXIT_CODE);
70
71 dlclose(hdl);
72
73 /*
74 * If the dtor was supposed to exit (most cases), then we should
75 * not have made it to this point. If it's not supposed to
76 * exit, then we just exit with success here because we might
77 * be expecting either a clean exit or a signal on our way out
78 * as the final __cxa_finalize tries to run a callback in the
79 * unloaded DSO.
80 */
81 if (with_exit)
82 exit(1);
83 exit(0);
84 }
85
86 dlclose(hdl);
87 atf_utils_wait(p, with_exit ? ARBITRARY_EXIT_CODE : 0, "", "");
88 }
89
90 ATF_TC_WITHOUT_HEAD(simple_cxa_atexit);
ATF_TC_BODY(simple_cxa_atexit,tc)91 ATF_TC_BODY(simple_cxa_atexit, tc)
92 {
93 /*
94 * This test exits in a global object's dtor so that we check for our
95 * dtor being run at dlclose() time. If it isn't, then the forked child
96 * will have a chance to exit(1) after dlclose() to raise a failure.
97 */
98 run_test(tc, false, true);
99 }
100
101 ATF_TC_WITHOUT_HEAD(late_cxa_atexit);
ATF_TC_BODY(late_cxa_atexit,tc)102 ATF_TC_BODY(late_cxa_atexit, tc)
103 {
104 /*
105 * This test creates another global object during a __cxa_atexit handler
106 * invocation. It's been observed in the wild that we weren't executing
107 * it, then the DSO gets torn down and it was executed at application
108 * exit time instead. In the best case scenario we would crash if
109 * something else hadn't been mapped there.
110 */
111 run_test(tc, true, false);
112 }
113
114 ATF_TC_WITHOUT_HEAD(late_cxa_atexit_ran);
ATF_TC_BODY(late_cxa_atexit_ran,tc)115 ATF_TC_BODY(late_cxa_atexit_ran, tc)
116 {
117 /*
118 * This is a slight variation of the previous test where we trigger an
119 * exit() in our late-registered __cxa_atexit handler so that we can
120 * ensure it was ran *before* dlclose() finished and not through some
121 * weird chain of events afterwards.
122 */
123 run_test(tc, true, true);
124 }
125
ATF_TP_ADD_TCS(tp)126 ATF_TP_ADD_TCS(tp)
127 {
128 ATF_TP_ADD_TC(tp, simple_cxa_atexit);
129 ATF_TP_ADD_TC(tp, late_cxa_atexit);
130 ATF_TP_ADD_TC(tp, late_cxa_atexit_ran);
131 return (atf_no_error());
132 }
133