Files
libakerror/tests/err_exit_status.c

207 lines
6.9 KiB
C
Raw Normal View History

Stop an unhandled error from exiting zero 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>
2026-08-01 07:25:37 -04:00
#include "akerror.h"
#include "err_capture.h"
#include <unistd.h>
#include <sys/wait.h>
/*
* An unhandled error must never leave the process looking like a success.
*
* The default handler used to exit(errctx->status) unconditionally, and an exit
* status is one byte wide: status 256 -- AKERR_FIRST_CONSUMER_STATUS, the very
* first code any consumer can reserve -- exited 0 and told the shell the
* program succeeded. Status 300 exited 44, which is some unrelated error's code.
*
* akerr_exit() now owns that mapping, and the default handler is one of its
* callers. The same table therefore drives both: a status must produce the same
* exit code whether a consumer calls akerr_exit() from their own handler or
* lets the library's handler run.
*
* akerr_exit(0) exits 0 -- zero is the library's success status. The thing that
* keeps an unhandled error off that path is PROCESS's `case 0`, which marks a
* zero status handled before FINISH_NORETURN can reach the handler, and the
* last three cases assert that, plus that the status an exit code could not
* carry is still recoverable from the stack trace.
*
* Neither exit path returns, so those cases run in forked children.
*/
#define TEST_OWNER "err_exit_status"
#define TEST_STATUS AKERR_FIRST_CONSUMER_STATUS
#define TEST_STATUS_NAME "Consumer Status Two Fifty Six"
static const struct {
int status;
int expect;
} exit_cases[] = {
/* status expected exit */
{ 0, 0 }, /* Zero is the library's success status, and exit code 0 is what that is called out here */
{ 1, 1 }, /* Lowest status an exit code can carry */
{ AKERR_VALUE, AKERR_VALUE }, /* An ordinary library status, delivered intact */
{ AKERR_EXIT_STATUS_MAX, AKERR_EXIT_STATUS_MAX }, /* Highest status an exit code can carry */
{ AKERR_FIRST_CONSUMER_STATUS, AKERR_EXIT_STATUS_UNREPRESENTABLE }, /* 256: low byte 0, the case that used to exit success */
{ 300, AKERR_EXIT_STATUS_UNREPRESENTABLE }, /* Low byte 44 would alias an unrelated status */
{ 65536, AKERR_EXIT_STATUS_UNREPRESENTABLE }, /* Low byte 0 again, further out */
{ -1, AKERR_EXIT_STATUS_UNREPRESENTABLE }, /* Negative: low byte 255 */
};
/*
* Run body in a child and return its exit status, or -1 if it did not exit
* normally. The 99 sentinel catches a body that returns instead of terminating.
*/
static int child_exit_status(void (*body)(void))
{
pid_t pid = fork();
if ( pid == 0 ) {
body();
_exit(99);
}
int status = 0;
if ( pid < 0 || waitpid(pid, &status, 0) != pid || !WIFEXITED(status) ) {
return -1;
}
return WEXITSTATUS(status);
}
/* The status the next child leaves with. Set before each fork. */
static int pending_status;
/* The way a consumer's own handler is expected to leave. */
static void run_akerr_exit(void)
{
akerr_exit(pending_status);
}
/* The way the library leaves when nothing handled the error. */
static void run_default_handler(void)
{
akerr_ErrorContext *slot = akerr_next_error();
if ( slot == NULL ) {
_exit(98);
}
slot->status = pending_status;
akerr_default_handler_unhandled_error(slot);
}
static akerr_ErrorContext AKERR_NOIGNORE *raise_consumer_error(void)
{
PREPARE_ERROR(errctx);
FAIL_RETURN(errctx, TEST_STATUS, "consumer status, deliberately unhandled");
}
static akerr_ErrorContext AKERR_NOIGNORE *raise_nothing(void)
{
PREPARE_ERROR(errctx);
SUCCEED_RETURN(errctx);
}
/* A full propagation to the top of the stack, with nothing handling it. */
static void unhandled_consumer_error(void)
{
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, raise_consumer_error());
} CLEANUP {
} PROCESS(errctx) {
/* no HANDLE for TEST_STATUS -> stays unhandled */
} FINISH_NORETURN(errctx);
}
/*
* Reached through FINISH_NORETURN rather than by calling the handler directly,
* so the child exercises the path a consumer actually takes. The handler is set
* explicitly because an earlier case in this process may have replaced it.
*/
static void unhandled_consumer_error_fatal(void)
{
akerr_handler_unhandled_error = &akerr_default_handler_unhandled_error;
unhandled_consumer_error();
}
static int trace_fired = -2;
static void nonfatal_handler(akerr_ErrorContext *e)
{
trace_fired = (e != NULL) ? e->status : -1;
}
/*
* The same shape as unhandled_consumer_error(), except nothing fails. PROCESS
* opens with `case 0`, so a zero status is handled and the handler must not
* run -- which is what keeps akerr_exit(0) exiting 0 from being a hole in the
* "an unhandled error never exits 0" rule.
*/
static void successful_operation(void)
{
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, raise_nothing());
} CLEANUP {
} PROCESS(errctx) {
} FINISH_NORETURN(errctx);
}
int main(void)
{
akerr_capture_install();
akerr_init();
/* The three outcomes have to stay distinguishable from each other. */
AKERR_CHECK(AKERR_EXIT_STATUS_UNREPRESENTABLE != 0);
AKERR_CHECK(AKERR_EXIT_STATUS_UNREPRESENTABLE != 1);
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(TEST_STATUS, 1, TEST_OWNER));
AKERR_CHECK_SUCCEEDS(akerr_register_status_name(TEST_OWNER, TEST_STATUS,
TEST_STATUS_NAME));
for ( size_t i = 0; i < sizeof(exit_cases) / sizeof(exit_cases[0]); i++ ) {
pending_status = exit_cases[i].status;
int direct = child_exit_status(&run_akerr_exit);
if ( direct != exit_cases[i].expect ) {
fprintf(stderr, "akerr_exit(%d) exited %d, want %d\n",
exit_cases[i].status, direct, exit_cases[i].expect);
return 1;
}
/* The handler must not carry a mapping of its own. */
int handled = child_exit_status(&run_default_handler);
if ( handled != direct ) {
fprintf(stderr, "default handler on status %d exited %d,"
" but akerr_exit(%d) exited %d\n",
exit_cases[i].status, handled, exit_cases[i].status, direct);
return 1;
}
}
/* The NULL-context exit is asserted by tests/err_unhandled_null.c. */
/* End to end: an unhandled consumer error kills the process non-zero. */
AKERR_CHECK(child_exit_status(&unhandled_consumer_error_fatal)
== AKERR_EXIT_STATUS_UNREPRESENTABLE);
/*
* And the status the exit code could not carry is in the trace. Run with a
* handler that returns so the assertions happen in this process, where the
* captured log lives.
*/
akerr_handler_unhandled_error = &nonfatal_handler;
akerr_capture_reset();
unhandled_consumer_error();
AKERR_CHECK(trace_fired == TEST_STATUS);
AKERR_CHECK_CONTAINS("Unhandled Error");
AKERR_CHECK_CONTAINS("256");
AKERR_CHECK_CONTAINS(TEST_STATUS_NAME);
/* A zero status is handled by PROCESS and never reaches the handler. */
trace_fired = -2;
akerr_capture_reset();
successful_operation();
AKERR_CHECK(trace_fired == -2);
AKERR_CHECK_NOT_CONTAINS("Unhandled Error");
AKERR_CHECK(akerr_slots_in_use() == 0);
fprintf(stderr, "err_exit_status ok\n");
return 0;
}