tests: ASSERT_SIGNAL: Do not allow parent to hallucinate it is the child

assert_signal_internal() returns 0 in two distinct cases:

1. In the child process (immediately after fork returns 0).
2. In the parent process, if the child exited normally (no signal).

ASSERT_SIGNAL fails to distinguish these cases. When a child exited
normally (case 2), the parent process receives 0, incorrectly interprets
it as meaning it is the child, and re-executes the test expression
inside the parent process. Goodness gracious!

This causes two severe test integrity issues:

1. False positives. The parent can run the expression, succeed, and call
   _exit(EXIT_SUCCESS), causing the test to pass even though no signal
   was raised.
2. Silent truncation. The _exit() call in the parent terminates the test
   runner prematurely, preventing subsequent tests in the same file from
   running.

Example of the bug in action, from #39674:

    ASSERT_SIGNAL(fd_is_writable(closed_fd), SIGABRT)

This test should fail (fd_is_writable does not SIGABRT here), but with
the bug, the parent hallucinated being the child, re-ran the expression
successfully, and exited with success.

Fix this by refactoring assert_signal_internal() to be much more strict
about separating control flow from data.

The signal status is now returned via a strictly typed output parameter,
guaranteeing that determining whether we are the child is never
conflated with whether the child exited cleanly.
This commit is contained in:
Chris Down
2025-11-20 02:40:07 +08:00
parent d759ed527c
commit e21a431ec4
3 changed files with 73 additions and 11 deletions
+13 -5
View File
@@ -425,10 +425,17 @@ void test_prepare(int argc, char *argv[], int log_level) {
test_setup_logging(log_level);
}
int assert_signal_internal(void) {
/* Returns:
* ASSERT_SIGNAL_FORK_CHILD = We are in the child process
* ASSERT_SIGNAL_FORK_PARENT = We are in the parent process (signal/status stored in *ret_signal)
* <0 = Error (negative errno)
*/
int assert_signal_internal(int *ret_signal) {
siginfo_t siginfo = {};
int r;
assert(ret_signal);
r = fork();
if (r < 0)
return -errno;
@@ -439,7 +446,7 @@ int assert_signal_internal(void) {
/* But still set an rlimit just in case */
(void) setrlimit(RLIMIT_CORE, &RLIMIT_MAKE_CONST(0));
return 0;
return ASSERT_SIGNAL_FORK_CHILD;
}
r = wait_for_terminate(r, &siginfo);
@@ -451,10 +458,11 @@ int assert_signal_internal(void) {
* - CLD_KILLED/CLD_DUMPED: si_status is the signal number that killed the process
* We need to return the signal number only if the child was killed by a signal. */
if (IN_SET(siginfo.si_code, CLD_KILLED, CLD_DUMPED))
return siginfo.si_status;
*ret_signal = siginfo.si_status;
else
*ret_signal = 0;
/* Child exited normally, return 0 to indicate no signal was received, regardless of actual exit */
return 0;
return ASSERT_SIGNAL_FORK_PARENT;
}
+14 -6
View File
@@ -592,7 +592,12 @@ _noreturn_ void log_test_failed_internal(const char *file, int line, const char
})
#endif
int assert_signal_internal(void);
enum {
ASSERT_SIGNAL_FORK_CHILD = 0, /* We are in the child process */
ASSERT_SIGNAL_FORK_PARENT = 1, /* We are in the parent process */
};
int assert_signal_internal(int *ret_status);
#ifdef __COVERITY__
# define ASSERT_SIGNAL(expr, signal) __coverity_check__(((expr), false))
@@ -601,16 +606,19 @@ int assert_signal_internal(void);
# define __ASSERT_SIGNAL(uniq, expr, sgnl) \
({ \
ASSERT_TRUE(SIGNAL_VALID(sgnl)); \
int UNIQ_T(_r, uniq) = assert_signal_internal(); \
ASSERT_OK_ERRNO(UNIQ_T(_r, uniq)); \
if (UNIQ_T(_r, uniq) == 0) { \
int UNIQ_T(_status, uniq); \
int UNIQ_T(_path, uniq) = assert_signal_internal(&UNIQ_T(_status, uniq)); \
ASSERT_OK_ERRNO(UNIQ_T(_path, uniq)); \
if (UNIQ_T(_path, uniq) == ASSERT_SIGNAL_FORK_CHILD) { \
(void) signal(sgnl, SIG_DFL); \
expr; \
_exit(EXIT_SUCCESS); \
} \
if (UNIQ_T(_r, uniq) != sgnl) \
ASSERT_EQ(UNIQ_T(_path, uniq), ASSERT_SIGNAL_FORK_PARENT); \
if (UNIQ_T(_status, uniq) != sgnl) \
log_test_failed("\"%s\" died with signal %s, but %s was expected", \
#expr, signal_to_string(UNIQ_T(_r, uniq)), signal_to_string(sgnl)); \
#expr, signal_to_string(UNIQ_T(_status, uniq)), \
signal_to_string(sgnl)); \
})
#endif