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:
91
README.md
91
README.md
@@ -6,12 +6,17 @@ This library provides a TRY/CATCH style exception handling mechanism for C.
|
||||
|
||||
## 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`
|
||||
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
|
||||
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
|
||||
capacity limits and how to raise them, and the thread-safety rules.
|
||||
See [UPGRADING.md](UPGRADING.md) for all three, what was removed, how to migrate,
|
||||
the capacity limits and how to raise them, and the thread-safety rules.
|
||||
|
||||
|
||||
# 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);
|
||||
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
|
||||
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
|
||||
initialization. `akerr_reserve_status_range()` and
|
||||
@@ -151,7 +157,7 @@ What it does not cover, and cannot:
|
||||
Registering a *new* status concurrently is fine.
|
||||
* **Which unhandled error terminates the process.** An error that reaches
|
||||
`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
|
||||
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.
|
||||
@@ -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, 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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user