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>
This commit is contained in:
@@ -113,6 +113,26 @@
|
||||
*/
|
||||
typedef char akerr_assert_codes_within_reserved_band[(AKERR_LAST_LIBRARY_STATUS < 256) ? 1 : -1];
|
||||
|
||||
/*
|
||||
* A process exit status is one byte wide. exit() takes an int, but the kernel
|
||||
* keeps only the low 8 bits of it and throws the rest away, so 0 through 255
|
||||
* are the only statuses that can also be exit codes -- and every consumer status
|
||||
* begins at AKERR_FIRST_CONSUMER_STATUS (256). No wider variant exists to reach
|
||||
* for: _exit(), _Exit(), quick_exit() and the raw exit_group syscall all
|
||||
* truncate identically, and even waitid()'s int-wide si_status reports the
|
||||
* truncated value, because the truncation happened before the parent looked.
|
||||
*
|
||||
* akerr_exit() therefore substitutes AKERR_EXIT_STATUS_UNREPRESENTABLE for any
|
||||
* status it cannot deliver intact, rather than passing the low byte -- status
|
||||
* 256 would exit 0 and report success. 125 is the conventional "the tool itself
|
||||
* failed" code (126, 127 and 128+n belong to the shell). It is inside the
|
||||
* library's reserved band, so it is also some host's errno: the exit code says
|
||||
* only that the process died of an error, and the stack trace carries the real
|
||||
* status.
|
||||
*/
|
||||
#define AKERR_EXIT_STATUS_MAX 255
|
||||
#define AKERR_EXIT_STATUS_UNREPRESENTABLE 125
|
||||
|
||||
#define AKERR_MAX_ARRAY_ERROR 128
|
||||
|
||||
|
||||
@@ -132,6 +152,9 @@ typedef struct
|
||||
} akerr_ErrorContext;
|
||||
|
||||
#define AKERR_NOIGNORE __attribute__((warn_unused_result))
|
||||
/* akerr_exit() does not come back, and the compiler should know it: a handler
|
||||
* whose last statement is a call to it is complete, not falling off the end. */
|
||||
#define AKERR_NORETURN __attribute__((noreturn))
|
||||
|
||||
typedef void (*akerr_ErrorUnhandledErrorHandler)(akerr_ErrorContext *errctx);
|
||||
typedef void (*akerr_ErrorLogFunction)(const char *f, ...);
|
||||
@@ -190,6 +213,25 @@ akerr_ErrorContext AKERR_NOIGNORE *akerr_register_status_name(const char *owner,
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akerr_reserve_status_range(int first_status, int count, const char *owner);
|
||||
void akerr_init();
|
||||
/*
|
||||
* Terminate the process, reporting `status`. Use this instead of exit()
|
||||
* anywhere you are leaving on account of an akerr status -- an unhandled-error
|
||||
* handler of your own, a CLI's top-level HANDLE block, an init routine that
|
||||
* cannot continue -- so that every exit out of the library's status space maps
|
||||
* the same way.
|
||||
*
|
||||
* Exits with `status` when 0 <= status <= AKERR_EXIT_STATUS_MAX, and with
|
||||
* AKERR_EXIT_STATUS_UNREPRESENTABLE otherwise (see above). Status 0 exits 0:
|
||||
* zero is this library's success status, and passing it here says the program
|
||||
* finished, not that it failed with a code that got lost.
|
||||
*/
|
||||
void AKERR_NORETURN akerr_exit(int status);
|
||||
/*
|
||||
* The default akerr_handler_unhandled_error: logs nothing further -- the stack
|
||||
* trace has already been printed by the time it runs -- and hands `ptr->status`
|
||||
* to akerr_exit(), or exits 1 when `ptr` is NULL. Replace it if you need a
|
||||
* different mapping, and call akerr_exit() from your replacement.
|
||||
*/
|
||||
void akerr_default_handler_unhandled_error(akerr_ErrorContext *ptr);
|
||||
void akerr_default_logger(const char *f, ...);
|
||||
int akerr_valid_error_address(akerr_ErrorContext *ptr);
|
||||
|
||||
Reference in New Issue
Block a user