Enforce status-code ownership and harden the name registry
Reservations were advisory bookkeeping: any component could name any status, so the registry only detected declared-range overlap between components that both opted in. Naming a status now requires a reservation. akerr_register_status_name() checks that the range belongs to the caller, and the legacy two-argument akerr_name_for_status() set path, which cannot identify its caller, requires that some reservation covers the status. Every refusal is logged and names the real owner, because a name that fails to register degrades that code to "Unknown Error" in every later stack trace. Fix a reservation made before the first PREPARE_ERROR being silently discarded. akerr_init() clears the tables, so whichever component first triggered it wiped an earlier reservation and the next component to claim the same range was told it was free, producing exactly the undetected aliasing the registry exists to prevent. Every registry entry point now calls akerr_init(), which sets its guard before doing any work so those calls do not recurse. Replace the linear-scan name array with an open-addressed hash table, taking lookup from O(n) to O(1) and raising usable capacity from 512 entries (366 free to consumers after errno registration) to 3072 (~2900 free). Both table sizes are build-time overridable and applied PRIVATE: they live entirely in src/error.c, so raising them cannot desynchronize a library from its consumers the way AKERR_MAX_ERR_VALUE could. Exhausting either table is now logged and returned to the caller rather than silently dropping the entry. No dynamic allocation is introduced; both tables remain file-scope arrays, and the library's undefined-symbol set gains only strcmp and strlen. Register names for AKERR_EOF, AKERR_ITERATOR_BREAK and AKERR_NOT_IMPLEMENTED, which had none and rendered as "Unknown Error" in every stack trace carrying them. err_error_names.c now sweeps the whole AKERR_* offset span so a code added without a name fails there instead of in production traces. Add static assertions that the slot count is a power of two and that AKERR_BADEXC stays inside the library's own 0-255 band, the latter guarding against a host errno space large enough to push library codes into the range consumers are told to allocate from. Set a project version and soname (1.0.0 / libakerror.so.1) so a stale installed library can no longer be silently paired with newer headers, and so akerror.pc ships a real Version field instead of an empty one. Mutation testing surfaced an out-of-bounds probe in the new table that the suite did not catch: masking with SLOTS rather than SLOTS-1 indexes past the array, and err_maxval.c asserted only that some names registered before the table filled, which a collapsed probe sequence still satisfies. It now requires a substantial entry count and reads every entry back by its own distinct name. Tests: 28/28 pass. Coverage 99.4% line / 86.8% branch. Mutation score for src/error.c 74% -> 77.3%. Compatibility: source and ABI break. AKERR_MAX_ERR_VALUE and the __AKERR_ERROR_NAMES data symbol are gone, custom codes must move out of 0-255, and names must be registered against a reserved range. README.md carries the migration steps. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,26 +3,46 @@
|
||||
#include <limits.h>
|
||||
#include <string.h>
|
||||
|
||||
/* Status magnitude is no longer coupled to a public array bound. */
|
||||
/*
|
||||
* Status magnitude is no longer coupled to a public array bound: any int is a
|
||||
* legal status, and storage is a private sparse registry. What bounds the
|
||||
* registry now is its *capacity*, not the value of the largest code.
|
||||
*
|
||||
* Covers: arbitrary int status values, name truncation, range reservation
|
||||
* semantics (overlap, idempotency, endpoints, validation, overflow), and both
|
||||
* capacity limits -- the range table and the name table -- each of which must
|
||||
* report the failure rather than dropping the registration quietly.
|
||||
*/
|
||||
|
||||
int main(void)
|
||||
{
|
||||
akerr_capture_install();
|
||||
akerr_init();
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(INT_MAX, "Maximum Status"),
|
||||
"Maximum Status") == 0);
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(INT_MIN, "Minimum Status"),
|
||||
"Minimum Status") == 0);
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(INT_MAX, NULL),
|
||||
"Maximum Status") == 0);
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(INT_MIN, NULL),
|
||||
"Minimum Status") == 0);
|
||||
|
||||
/* Any int is a legal status, at either extreme of the range. */
|
||||
AKERR_CHECK(akerr_reserve_status_range(INT_MIN, 1, "min-owner") ==
|
||||
AKERR_STATUS_RANGE_OK);
|
||||
AKERR_CHECK(akerr_reserve_status_range(INT_MAX, 1, "max-owner") ==
|
||||
AKERR_STATUS_RANGE_OK);
|
||||
AKERR_CHECK(akerr_register_status_name("max-owner", INT_MAX, "Maximum Status") ==
|
||||
AKERR_STATUS_NAME_OK);
|
||||
AKERR_CHECK(akerr_register_status_name("min-owner", INT_MIN, "Minimum Status") ==
|
||||
AKERR_STATUS_NAME_OK);
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(INT_MAX, NULL), "Maximum Status") == 0);
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(INT_MIN, NULL), "Minimum Status") == 0);
|
||||
|
||||
/* A name longer than the buffer is truncated and always terminated. */
|
||||
AKERR_CHECK(akerr_reserve_status_range(1000000, 1, "trunc") ==
|
||||
AKERR_STATUS_RANGE_OK);
|
||||
const char *long_name =
|
||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-extra";
|
||||
char *stored = akerr_name_for_status(1000000, (char *)long_name);
|
||||
AKERR_CHECK(akerr_register_status_name("trunc", 1000000, long_name) ==
|
||||
AKERR_STATUS_NAME_OK);
|
||||
char *stored = akerr_name_for_status(1000000, NULL);
|
||||
AKERR_CHECK(strlen(stored) == AKERR_MAX_ERROR_NAME_LENGTH - 1);
|
||||
AKERR_CHECK(stored[AKERR_MAX_ERROR_NAME_LENGTH - 1] == '\0');
|
||||
|
||||
/* Reservation: overlap detection, and idempotency for an exact repeat. */
|
||||
AKERR_CHECK(akerr_reserve_status_range(256, 16, "component-a") ==
|
||||
AKERR_STATUS_RANGE_OK);
|
||||
AKERR_CHECK(akerr_reserve_status_range(256, 16, "component-a") ==
|
||||
@@ -30,8 +50,15 @@ int main(void)
|
||||
AKERR_CHECK(akerr_reserve_status_range(260, 2, "component-b") ==
|
||||
AKERR_STATUS_RANGE_OVERLAP);
|
||||
AKERR_CHECK_CONTAINS("component-a");
|
||||
|
||||
/* The library's own 0..255 band is reserved and cannot be encroached on. */
|
||||
AKERR_CHECK(akerr_reserve_status_range(255, 1, "component-b") ==
|
||||
AKERR_STATUS_RANGE_OVERLAP);
|
||||
AKERR_CHECK(akerr_reserve_status_range(0, AKERR_RESERVED_STATUS_COUNT,
|
||||
AKERR_LIBRARY_OWNER) ==
|
||||
AKERR_STATUS_RANGE_OK); /* exact repeat by the owner */
|
||||
|
||||
/* Argument validation. */
|
||||
AKERR_CHECK(akerr_reserve_status_range(INT_MAX, 2, "overflow") ==
|
||||
AKERR_STATUS_RANGE_INVALID);
|
||||
AKERR_CHECK(akerr_reserve_status_range(300, 0, "empty") ==
|
||||
@@ -43,6 +70,7 @@ int main(void)
|
||||
AKERR_CHECK(akerr_reserve_status_range(300, 1, "") ==
|
||||
AKERR_STATUS_RANGE_INVALID);
|
||||
|
||||
/* Owner strings: 63 chars fit, 64 do not. */
|
||||
char owner63[AKERR_MAX_ERROR_NAME_LENGTH];
|
||||
char owner64[AKERR_MAX_ERROR_NAME_LENGTH + 1];
|
||||
memset(owner63, 'a', sizeof(owner63) - 1);
|
||||
@@ -53,37 +81,79 @@ int main(void)
|
||||
AKERR_STATUS_RANGE_OK);
|
||||
AKERR_CHECK(akerr_reserve_status_range(401, 1, owner64) ==
|
||||
AKERR_STATUS_RANGE_INVALID);
|
||||
AKERR_CHECK(akerr_reserve_status_range(INT_MAX, 1, "int-max") ==
|
||||
AKERR_STATUS_RANGE_OK);
|
||||
|
||||
/* Partial overlaps at either endpoint, and a same-range different owner. */
|
||||
AKERR_CHECK(akerr_reserve_status_range(500, 2, "endpoint") ==
|
||||
AKERR_STATUS_RANGE_OK);
|
||||
AKERR_CHECK(akerr_reserve_status_range(499, 2, "left") ==
|
||||
AKERR_STATUS_RANGE_OVERLAP);
|
||||
AKERR_CHECK(akerr_reserve_status_range(500, 1, "endpoint") ==
|
||||
AKERR_STATUS_RANGE_OVERLAP);
|
||||
AKERR_STATUS_RANGE_OVERLAP); /* subset, not an exact repeat */
|
||||
AKERR_CHECK(akerr_reserve_status_range(501, 1, "endpoint") ==
|
||||
AKERR_STATUS_RANGE_OVERLAP);
|
||||
AKERR_CHECK(akerr_reserve_status_range(500, 2, "other") ==
|
||||
AKERR_STATUS_RANGE_OVERLAP);
|
||||
AKERR_STATUS_RANGE_OVERLAP); /* same range, wrong owner */
|
||||
|
||||
/* Five ranges exist: library, component-a, owner63, INT_MAX, endpoint. */
|
||||
for ( int i = 0; i < 59; i++ ) {
|
||||
AKERR_CHECK(akerr_reserve_status_range(1000 + (i * 2), 1, "fill") ==
|
||||
AKERR_STATUS_RANGE_OK);
|
||||
/* Claim room for the name-exhaustion sweep before filling the range table. */
|
||||
AKERR_CHECK(akerr_reserve_status_range(2000000, 100000, "fill") ==
|
||||
AKERR_STATUS_RANGE_OK);
|
||||
|
||||
/*
|
||||
* Range table capacity. The limit is private to src/error.c on purpose, so
|
||||
* discover it by filling rather than by hardcoding it here.
|
||||
*/
|
||||
akerr_capture_reset();
|
||||
int ranges_added = 0;
|
||||
int range_rc = AKERR_STATUS_RANGE_OK;
|
||||
for ( int i = 0; i < 100000; i++ ) {
|
||||
range_rc = akerr_reserve_status_range(1000 + (i * 2), 1, "pad");
|
||||
if ( range_rc != AKERR_STATUS_RANGE_OK ) {
|
||||
break;
|
||||
}
|
||||
ranges_added++;
|
||||
}
|
||||
AKERR_CHECK(akerr_reserve_status_range(2000, 1, "too-many") ==
|
||||
AKERR_STATUS_RANGE_FULL);
|
||||
AKERR_CHECK(ranges_added > 0);
|
||||
AKERR_CHECK(range_rc == AKERR_STATUS_RANGE_FULL);
|
||||
AKERR_CHECK_CONTAINS("range table is full");
|
||||
|
||||
int filled = 0;
|
||||
for ( int status = 2000000; status < 2000600; status++ ) {
|
||||
if ( strcmp(akerr_name_for_status(status, "Filled"), "Filled") != 0 ) {
|
||||
filled = 1;
|
||||
/*
|
||||
* Name table capacity. Exhaustion must be reported, not silent: a dropped
|
||||
* name degrades every future stack trace for that code to "Unknown Error".
|
||||
*/
|
||||
akerr_capture_reset();
|
||||
int full_at = -1;
|
||||
for ( int i = 0; i < 100000; i++ ) {
|
||||
char name[32];
|
||||
snprintf(name, sizeof(name), "Filled %d", i);
|
||||
int rc = akerr_register_status_name("fill", 2000000 + i, name);
|
||||
if ( rc != AKERR_STATUS_NAME_OK ) {
|
||||
AKERR_CHECK(rc == AKERR_STATUS_NAME_FULL);
|
||||
full_at = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
AKERR_CHECK(filled == 1);
|
||||
AKERR_CHECK_CONTAINS("registry is full");
|
||||
AKERR_CHECK_CONTAINS("AKERR_STATUS_NAME_SLOTS");
|
||||
|
||||
fprintf(stderr, "err_maxval ok\n");
|
||||
/*
|
||||
* The table must actually hold everything it accepted. A probe sequence
|
||||
* that revisits slots instead of walking the table -- e.g. masking with
|
||||
* SLOTS rather than SLOTS-1 -- both collapses the usable capacity and
|
||||
* loses earlier entries, and each check below catches it independently.
|
||||
* The floor assumes at least the default table size (4096 slots).
|
||||
*/
|
||||
AKERR_CHECK(full_at > 256);
|
||||
for ( int i = 0; i < full_at; i++ ) {
|
||||
char expected[32];
|
||||
snprintf(expected, sizeof(expected), "Filled %d", i);
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(2000000 + i, NULL), expected) == 0);
|
||||
}
|
||||
|
||||
/* A dropped name reads back as the sentinel, and earlier ones survive. */
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(2000000 + full_at, NULL),
|
||||
"Unknown Error") == 0);
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(INT_MIN, NULL), "Minimum Status") == 0);
|
||||
|
||||
fprintf(stderr, "err_maxval ok (%d consumer names before full)\n", full_at);
|
||||
return 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user