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:
10
AGENTS.md
10
AGENTS.md
@@ -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
|
||||
(`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
|
||||
|
||||
Add tests as `tests/err_<behavior>.c`. Register each new test in the
|
||||
|
||||
Reference in New Issue
Block a user