Stop an unhandled error from exiting zero
Some checks failed
libakerror CI Build / cmake_build (push) Successful in 2m47s
libakerror CI Build / coverage (push) Successful in 2m48s
libakerror CI Build / thread_sanitizer (push) Failing after 2m49s
libakerror CI Build / mutation_test (push) Successful in 39m15s

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:
2026-08-01 07:25:37 -04:00
parent 756933c600
commit 5eaa956f50
8 changed files with 435 additions and 10 deletions

View File

@@ -88,6 +88,16 @@ locked body does the work, and a thin wrapper takes the lock, calls it, and
releases it on the single return path. Do not call consumer code releases it on the single return path. Do not call consumer code
(`akerr_log_method`, `akerr_handler_unhandled_error`) while holding it. (`akerr_log_method`, `akerr_handler_unhandled_error`) while holding it.
**Exiting.** Never call `exit()` with a status value. Call `akerr_exit()`, which
owns the one mapping from an akerr status to an exit code: an exit status is a
byte, and every consumer status starts at 256, so passing a status through
`exit()` silently truncates it — status 256 exits 0 and reports success. The
only exits that bypass it are the two that have no status to map, both in
`src/error.c`: `ENSURE_ERROR_READY`'s pool-exhaustion abort and the NULL-context
case in `akerr_default_handler_unhandled_error()`. This rule applies to test
programs too, except where the test's whole point is to observe the raw
truncation.
## Testing Guidelines ## Testing Guidelines
Add tests as `tests/err_<behavior>.c`. Register each new test in the Add tests as `tests/err_<behavior>.c`. Register each new test in the

View File

@@ -6,7 +6,10 @@ cmake_minimum_required(VERSION 3.10)
# now takes for it. Consumer code compiled against a 1.x header would # now takes for it. Consumer code compiled against a 1.x header would
# double-count every reference. Hence the major bump and the SOVERSION, so a # double-count every reference. Hence the major bump and the SOVERSION, so a
# stale installed libakerror.so cannot be silently paired with new headers. # stale installed libakerror.so cannot be silently paired with new headers.
project(akerror VERSION 2.0.0 LANGUAGES C) # 2.0.1 fixes the unhandled-error exit code, which reported success for any
# status whose low byte was zero. It adds akerr_exit() but breaks nothing: the
# soname is unchanged and no existing entry point changed shape.
project(akerror VERSION 2.0.1 LANGUAGES C)
include(GNUInstallDirs) include(GNUInstallDirs)
include(CMakePackageConfigHelpers) include(CMakePackageConfigHelpers)
@@ -217,6 +220,7 @@ set(AKERR_TESTS
err_name_bounds err_name_bounds
err_format_string err_format_string
err_unhandled_null err_unhandled_null
err_exit_status
err_release_null err_release_null
err_release_refcount err_release_refcount
) )

View File

@@ -6,12 +6,17 @@ This library provides a TRY/CATCH style exception handling mechanism for C.
## Upgrading ## Upgrading
2.0.1 fixes an unhandled error killing the process and still reporting success:
the exit code was the status truncated to a byte, and every consumer status
starts at 256. Use `akerr_exit()` instead of `exit()` — see
[Exit status](#exit-status). No ABI break.
2.0.0 makes the library thread safe. That is an ABI break — `__akerr_last_ignored` 2.0.0 makes the library thread safe. That is an ABI break — `__akerr_last_ignored`
became thread-local storage and the pool now takes its own reference — so became thread-local storage and the pool now takes its own reference — so
everything built against a 1.x header must be rebuilt. 1.0.0 replaced the everything built against a 1.x header must be rebuilt. 1.0.0 replaced the
consumer-sized status-name array with a private, ownership-enforced registry. consumer-sized status-name array with a private, ownership-enforced registry.
See [UPGRADING.md](UPGRADING.md) for both, what was removed, how to migrate, the See [UPGRADING.md](UPGRADING.md) for all three, what was removed, how to migrate,
capacity limits and how to raise them, and the thread-safety rules. the capacity limits and how to raise them, and the thread-safety rules.
# Why? # Why?
@@ -78,7 +83,8 @@ You can define additional error types as integer constants. Values 0 through 255
are reserved by libakerror (the host's errno values plus the `AKERR_*` codes); are reserved by libakerror (the host's errno values plus the `AKERR_*` codes);
consumers allocate codes starting at `AKERR_FIRST_CONSUMER_STATUS` (256). Status consumers allocate codes starting at `AKERR_FIRST_CONSUMER_STATUS` (256). Status
names are stored sparsely, so any `int` is a legal status and no compile names are stored sparsely, so any `int` is a legal status and no compile
definition is needed to use large values. definition is needed to use large values. Note that no consumer status can be a
process exit code — see [Exit status](#exit-status).
Every library that may coexist in one process must reserve its range during Every library that may coexist in one process must reserve its range during
initialization. `akerr_reserve_status_range()` and initialization. `akerr_reserve_status_range()` and
@@ -151,7 +157,7 @@ What it does not cover, and cannot:
Registering a *new* status concurrently is fine. Registering a *new* status concurrently is fine.
* **Which unhandled error terminates the process.** An error that reaches * **Which unhandled error terminates the process.** An error that reaches
`FINISH_NORETURN` unhandled prints its stack trace and calls `FINISH_NORETURN` unhandled prints its stack trace and calls
`akerr_handler_unhandled_error`, which by default calls `exit()`. Each `akerr_handler_unhandled_error`, which by default calls `akerr_exit()`. Each
thread's trace is whole — the buffer belongs to its context, and each line is thread's trace is whole — the buffer belongs to its context, and each line is
one call to `akerr_log_method` — but if two threads get there at the same one call to `akerr_log_method` — but if two threads get there at the same
instant, both traces print and the exit status is whichever one won. instant, both traces print and the exit status is whichever one won.
@@ -567,3 +573,80 @@ From bottom to top, we have:
* Above that, the `FINISH()` macro in `func2()` which detected an unhandled error and passed it out of the function * Above that, the `FINISH()` macro in `func2()` which detected an unhandled error and passed it out of the function
* Above that, a reference to the line where the `FAIL()` macro set the error code and provided the message which is printed here * Above that, a reference to the line where the `FAIL()` macro set the error code and provided the message which is printed here
## Exit status
**Never call `exit()` with an akerr status. Call `akerr_exit()`.**
```c
void akerr_exit(int status);
```
This applies everywhere you are leaving the process on account of a status, not
just in an unhandled-error handler: a CLI's top-level `HANDLE` block, an
initialization routine that cannot continue, a `main()` that ends by reporting
the status it finished with. One function owns the mapping so that a given
status produces the same exit code no matter which of your exits it left by.
The mapping exists because a process exit status is one byte wide. `exit()`
accepts an `int` and the kernel keeps the low 8 bits of it — `_exit()`,
`_Exit()`, `quick_exit()` and the raw `exit_group` syscall all behave
identically, and even `waitid()`, whose `si_status` is a full `int`, sees the
truncated value because the truncation happened before the parent looked. There
is no wider `exit()` to reach for.
That leaves 0 through 255 as the only statuses an exit code can carry, and
consumer statuses start at `AKERR_FIRST_CONSUMER_STATUS` (256) — so *no* consumer
status can be an exit code:
```
status exit code
0 0 (success)
1 .. 255 the status
negative, or > 255 AKERR_EXIT_STATUS_UNREPRESENTABLE (125)
```
Passing the low byte instead would have made status 256 exit 0 and report
success to the shell, and status 300 exit 44 — an unrelated error's code. 125 is
the conventional "the tool itself failed" status; 126, 127 and 128+*n* already
belong to the shell.
Status 0 exits 0, because 0 is this library's success status. That is not a hole
in the rule that an unhandled error never exits 0: `PROCESS` opens with `case
0`, which marks a zero status handled, so a successful context cannot reach
`FINISH_NORETURN`'s call to the handler at all.
Above 255, the exit code tells you the process died of an error, not which one.
125 is inside the library's reserved band and so is also some host's `errno`, and
every status above 255 collapses onto it. **The stack trace is what identifies
the error** — it is printed before the handler runs, carrying the status at full
width along with its registered name.
### Replacing the handler
After the trace is printed, `FINISH_NORETURN` calls
`akerr_handler_unhandled_error`. The default implementation,
`akerr_default_handler_unhandled_error()`, hands `errctx->status` to
`akerr_exit()` (a NULL context exits 1). Replace it if you need something else
to happen first — a core dump, a crash reporter, a flush — and finish by calling
`akerr_exit()` so the exit code still means what it means everywhere else:
```c
static void mylib_handler(akerr_ErrorContext *e)
{
mylib_flush_telemetry();
if ( e == NULL ) {
akerr_exit(AKERR_API);
}
akerr_exit(e->status);
}
akerr_handler_unhandled_error = &mylib_handler;
```
`akerr_exit()` is declared `AKERR_NORETURN`, so the compiler knows a handler
ending in one of those calls is complete rather than falling off the end.
Set the handler once, before you start any threads. `tests/err_custom_handler.c`
installs one that does not exit at all, which is how the test suite asserts on
unhandled errors without dying.

View File

@@ -1,3 +1,45 @@
# Bug fix: unhandled-error exit status (2.0.1)
An unhandled error could kill the process and still report success.
`akerr_default_handler_unhandled_error()` ended in `exit(errctx->status)`, and a
process 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 or supervisor watching `$?` saw a clean run.
Status 300 exited 44, which is some unrelated error's code. No status a consumer
owns could ever come out of `$?` intact, and 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 mapping now lives in one place, `akerr_exit()`, which the default handler
calls:
```
status exit code
0 0 (success)
1 .. 255 the status
negative, or > 255 AKERR_EXIT_STATUS_UNREPRESENTABLE (125)
```
Statuses 0 through 255 are unchanged, which covers every `errno` and every
`AKERR_*` code. Only the values that were already being delivered wrong behave
differently, and they now exit 125 instead of a truncated byte.
**What you should change.** Call `akerr_exit()` instead of `exit()` anywhere you
leave the process on an akerr status — your own unhandled-error handler, a
top-level `HANDLE` block, an init routine that cannot continue — so one mapping
covers every exit. It is declared `AKERR_NORETURN`. If you were reading a
consumer status out of `$?`, you were never getting it: read the stack trace,
which carries the status at full width along with its registered name, or
install a handler that maps your own statuses into a byte. See "Exit status" in
[README.md](README.md).
No ABI break. The soname stays `libakerror.so.2` and nothing you already call
changed shape. `akerr_exit()` is a new exported symbol, so a consumer that
starts calling it needs 2.0.1 or later at link time.
# Upgrade notice: thread safety (2.0.0) # Upgrade notice: thread safety (2.0.0)
2.0.0 makes the library thread safe. Every entry point may be called from any 2.0.0 makes the library thread safe. Every entry point may be called from any

View File

@@ -113,6 +113,26 @@
*/ */
typedef char akerr_assert_codes_within_reserved_band[(AKERR_LAST_LIBRARY_STATUS < 256) ? 1 : -1]; 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 #define AKERR_MAX_ARRAY_ERROR 128
@@ -132,6 +152,9 @@ typedef struct
} akerr_ErrorContext; } akerr_ErrorContext;
#define AKERR_NOIGNORE __attribute__((warn_unused_result)) #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_ErrorUnhandledErrorHandler)(akerr_ErrorContext *errctx);
typedef void (*akerr_ErrorLogFunction)(const char *f, ...); 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); akerr_ErrorContext AKERR_NOIGNORE *akerr_reserve_status_range(int first_status, int count, const char *owner);
void akerr_init(); 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_handler_unhandled_error(akerr_ErrorContext *ptr);
void akerr_default_logger(const char *f, ...); void akerr_default_logger(const char *f, ...);
int akerr_valid_error_address(akerr_ErrorContext *ptr); int akerr_valid_error_address(akerr_ErrorContext *ptr);

View File

@@ -305,12 +305,47 @@ void akerr_init()
akerr_once(&akerr_state_once, &akerr_init_state); akerr_once(&akerr_state_once, &akerr_init_state);
} }
/*
* Every way out of the library's status space goes through here, so that a
* status becomes an exit code exactly one way no matter who is leaving.
*
* Only 0 through AKERR_EXIT_STATUS_MAX survive the trip -- see the note on
* AKERR_EXIT_STATUS_UNREPRESENTABLE in the header for why there is no wider
* exit() to reach for. Everything else exits with that sentinel instead of its
* low byte, because the low byte is either a lie (status 300 exiting 44, which
* is some other error's code) or a disaster (status 256, the first status a
* consumer can own, exiting 0 and telling the shell the program succeeded).
*
* Status 0 exits 0, because 0 is this library's success status and an exit code
* of 0 is what success is called out here. What keeps an *unhandled* error from
* exiting 0 is not this function: PROCESS opens with `case 0`, which marks a
* zero status handled, so a successful context can never reach
* FINISH_NORETURN's call to the handler in the first place.
*
* Nothing is logged here. Callers arrive from a position that has already
* reported -- FINISH_NORETURN logs the stack trace, carrying the status at full
* width, before it calls the handler -- and a second line naming a number the
* trace already gave would only invite the reader to trust the exit code.
*/
void akerr_exit(int status)
{
if ( status < 0 || status > AKERR_EXIT_STATUS_MAX ) {
exit(AKERR_EXIT_STATUS_UNREPRESENTABLE);
}
exit(status);
}
/*
* The last stop for an unhandled error. A handler invoked with no error at all
* has no status to report and nothing akerr_exit() could map, so it exits 1
* directly: a plain generic failure.
*/
void akerr_default_handler_unhandled_error(akerr_ErrorContext *errctx) void akerr_default_handler_unhandled_error(akerr_ErrorContext *errctx)
{ {
if ( errctx == NULL ) { if ( errctx == NULL ) {
exit(1); exit(1);
} }
exit(errctx->status); akerr_exit(errctx->status);
} }
/* /*

206
tests/err_exit_status.c Normal file
View File

@@ -0,0 +1,206 @@
#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;
}

View File

@@ -6,7 +6,10 @@
/* /*
* The default unhandled-error handler is the library's last stop: it exits the * 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 * process. Both of its exits were untested -- exit(1) for a NULL context (a
* handler invoked with no error at all) and exit(errctx->status) for a real one. * 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 * 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 * asserts the exact exit status. That is stricter than a WILL_FAIL test, which