An unhandled error could kill the process and still report success. The default handler ended in exit(errctx->status), and an exit status is one byte wide: the kernel keeps the low 8 bits of the argument and discards the rest. Consumer statuses start at AKERR_FIRST_CONSUMER_STATUS (256), so the first status any consumer can reserve exited 0 and a shell saw a clean run. Status 300 exited 44, an unrelated error's code. There is no wider exit() to reach for. _exit(), _Exit(), quick_exit() and the raw exit_group syscall all truncate identically, and even waitid(), whose si_status is a full int, reports the truncated value -- the truncation happened before the parent looked. akerr_exit() now owns that mapping and the default handler calls it: 0 exits 0, 1 through 255 exit the status, and anything else exits AKERR_EXIT_STATUS_UNREPRESENTABLE (125) rather than a low byte that is either a lie or a claim of success. Only values that were already being delivered wrong behave differently. Call it instead of exit() anywhere you leave the process on a status; it is declared AKERR_NORETURN. akerr_exit(0) exits 0, because 0 is this library's success status. That is not a hole in the rule: PROCESS opens with case 0, which marks a zero status handled, so a successful context never reaches FINISH_NORETURN's call to the handler at all. tests/err_exit_status.c drives one table through akerr_exit() and through the default handler in forked children and requires identical exit codes, so the handler cannot grow a mapping of its own. With the clamp removed it fails with "akerr_exit(256) exited 0, want 125". The full-width status was already reaching the log and still does, which the same test asserts against the captured stack trace. 2.0.1. No ABI break: the soname stays libakerror.so.2 and nothing that already existed changed shape. akerr_exit() is a new exported symbol, so a consumer that starts calling it needs 2.0.1 at link time. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
65 lines
2.1 KiB
C
65 lines
2.1 KiB
C
#include "akerror.h"
|
|
#include "err_capture.h"
|
|
#include <unistd.h>
|
|
#include <sys/wait.h>
|
|
|
|
/*
|
|
* The default unhandled-error handler is the library's last stop: it exits the
|
|
* process. Both of its exits were untested -- exit(1) for a NULL context (a
|
|
* handler invoked with no error at all) and, for a real one, the status handed
|
|
* to akerr_exit(). tests/err_exit_status.c covers what akerr_exit() does with a
|
|
* status; this covers that the handler reaches it, and the NULL case, which
|
|
* never gets that far.
|
|
*
|
|
* The handler never returns, so each case runs in a forked child and the test
|
|
* asserts the exact exit status. That is stricter than a WILL_FAIL test, which
|
|
* would pass on any non-zero exit, including one caused by an unrelated bug.
|
|
*/
|
|
|
|
/*
|
|
* Run the default handler on ctx in a child process and return the child's exit
|
|
* status, or -1 if it did not exit normally. The _exit() sentinel catches a
|
|
* handler that returns instead of terminating.
|
|
*/
|
|
static int handler_exit_status(akerr_ErrorContext *ctx)
|
|
{
|
|
pid_t pid = fork();
|
|
if ( pid == 0 ) {
|
|
akerr_default_handler_unhandled_error(ctx);
|
|
_exit(99);
|
|
}
|
|
int status = 0;
|
|
if ( pid < 0 || waitpid(pid, &status, 0) != pid || !WIFEXITED(status) ) {
|
|
return -1;
|
|
}
|
|
return WEXITSTATUS(status);
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
akerr_capture_install();
|
|
akerr_init();
|
|
|
|
/* No context to report: the handler has nothing to exit with but failure. */
|
|
AKERR_CHECK(handler_exit_status(NULL) == 1);
|
|
|
|
/*
|
|
* With a context, the status becomes the exit code. waitpid only reports
|
|
* its low 8 bits, which is where AKERR_VALUE (144) lands, and it is neither
|
|
* 0 nor the 1 used for the NULL case, so the two exits stay distinguishable.
|
|
*/
|
|
akerr_ErrorContext *slot = akerr_next_error();
|
|
AKERR_CHECK(slot != NULL);
|
|
slot->refcount = 1;
|
|
slot->status = AKERR_VALUE;
|
|
AKERR_CHECK((AKERR_VALUE & 0xff) != 0 && (AKERR_VALUE & 0xff) != 1);
|
|
AKERR_CHECK(handler_exit_status(slot) == (AKERR_VALUE & 0xff));
|
|
|
|
slot = akerr_release_error(slot);
|
|
AKERR_CHECK(slot == NULL);
|
|
AKERR_CHECK(akerr_slots_in_use() == 0);
|
|
|
|
fprintf(stderr, "err_unhandled_null ok\n");
|
|
return 0;
|
|
}
|