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:
262
src/error.c
262
src/error.c
@@ -10,13 +10,43 @@ akerr_ErrorContext *__akerr_last_ignored;
|
||||
akerr_ErrorUnhandledErrorHandler akerr_handler_unhandled_error;
|
||||
akerr_ErrorLogFunction akerr_log_method = NULL;
|
||||
|
||||
#define AKERR_MAX_REGISTERED_STATUS_NAMES 512
|
||||
/*
|
||||
* Status-name registry.
|
||||
*
|
||||
* Storage is an open-addressed hash table keyed by status value. Both sizes are
|
||||
* private to this translation unit -- they are deliberately NOT in the public
|
||||
* header, because a consumer-visible table bound is exactly the ABI hazard this
|
||||
* registry replaced. Overriding them changes only this file, so a library and
|
||||
* its consumers can never disagree about the layout.
|
||||
*
|
||||
* The table is never resized or rehashed, so a pointer handed out by
|
||||
* akerr_name_for_status() stays valid for the life of the process. Entries are
|
||||
* never removed, so probing needs no tombstones.
|
||||
*/
|
||||
#ifndef AKERR_STATUS_NAME_SLOTS
|
||||
#define AKERR_STATUS_NAME_SLOTS 4096
|
||||
#endif
|
||||
|
||||
#ifndef AKERR_MAX_RESERVED_STATUS_RANGES
|
||||
#define AKERR_MAX_RESERVED_STATUS_RANGES 64
|
||||
#endif
|
||||
|
||||
/* Probing masks with SLOTS-1, so the slot count must be a power of two. This is
|
||||
* the C99-portable spelling of a static assertion (a negative array bound). */
|
||||
typedef char akerr_assert_name_slots_pow2[
|
||||
(AKERR_STATUS_NAME_SLOTS > 0 &&
|
||||
(AKERR_STATUS_NAME_SLOTS & (AKERR_STATUS_NAME_SLOTS - 1)) == 0) ? 1 : -1];
|
||||
|
||||
/* Cap occupancy at 75% so linear probing always meets an empty slot. */
|
||||
#define AKERR_MAX_REGISTERED_STATUS_NAMES \
|
||||
(AKERR_STATUS_NAME_SLOTS - (AKERR_STATUS_NAME_SLOTS / 4))
|
||||
|
||||
#define AKERR_MAX_STATUS_RANGE_OWNER_LENGTH 64
|
||||
|
||||
typedef struct
|
||||
{
|
||||
int status;
|
||||
int used;
|
||||
char name[AKERR_MAX_ERROR_NAME_LENGTH];
|
||||
} akerr_StatusName;
|
||||
|
||||
@@ -27,7 +57,7 @@ typedef struct
|
||||
char owner[AKERR_MAX_STATUS_RANGE_OWNER_LENGTH];
|
||||
} akerr_StatusRange;
|
||||
|
||||
static akerr_StatusName akerr_status_names[AKERR_MAX_REGISTERED_STATUS_NAMES];
|
||||
static akerr_StatusName akerr_status_names[AKERR_STATUS_NAME_SLOTS];
|
||||
static int akerr_status_name_count;
|
||||
static akerr_StatusRange akerr_status_ranges[AKERR_MAX_RESERVED_STATUS_RANGES];
|
||||
static int akerr_status_range_count;
|
||||
@@ -36,10 +66,19 @@ akerr_ErrorContext AKERR_ARRAY_ERROR[AKERR_MAX_ARRAY_ERROR];
|
||||
|
||||
static void akerr_copy_string(char *destination, int capacity, const char *source)
|
||||
{
|
||||
if ( capacity <= 0 ) {
|
||||
return;
|
||||
}
|
||||
strncpy(destination, source, (size_t)capacity - 1);
|
||||
destination[capacity - 1] = '\0';
|
||||
}
|
||||
|
||||
/*
|
||||
* Compare against each element address rather than testing the address range.
|
||||
* A range test also accepts pointers into the *interior* of an element, which
|
||||
* would then be treated as the head of an akerr_ErrorContext and written
|
||||
* through. Keep this an element-wise scan; it is not a missed optimization.
|
||||
*/
|
||||
int akerr_valid_error_address(akerr_ErrorContext *ptr)
|
||||
{
|
||||
// Is this within the memory region occupied by AKERR_ARRAY_ERROR?
|
||||
@@ -67,10 +106,18 @@ void akerr_default_logger(const char *fmt, ...)
|
||||
#endif
|
||||
}
|
||||
|
||||
/*
|
||||
* Idempotent. `inited` is set before any work so that the registry calls below
|
||||
* -- and the public registry entry points, which all call akerr_init() so that
|
||||
* a consumer reserving its range before anything else touches the library
|
||||
* cannot have that reservation wiped by a later first-use of the pool -- see
|
||||
* themselves as already initialized instead of recursing.
|
||||
*/
|
||||
void akerr_init()
|
||||
{
|
||||
static int inited = 0;
|
||||
if ( inited == 0 ) {
|
||||
inited = 1;
|
||||
for (int i = 0; i < AKERR_MAX_ARRAY_ERROR; i++ ) {
|
||||
memset((void *)&AKERR_ARRAY_ERROR[i], 0x00, sizeof(akerr_ErrorContext));
|
||||
AKERR_ARRAY_ERROR[i].arrayid = i;
|
||||
@@ -88,26 +135,33 @@ void akerr_init()
|
||||
akerr_status_name_count = 0;
|
||||
akerr_status_range_count = 0;
|
||||
|
||||
/* errno and AKERR_* values are the library-owned compatibility band. */
|
||||
(void)akerr_reserve_status_range(0, 256, "libakerror");
|
||||
/* errno and AKERR_* values are the library-owned compatibility band.
|
||||
* This must precede every registration below: naming a status is only
|
||||
* permitted inside a reserved range. */
|
||||
(void)akerr_reserve_status_range(0, AKERR_RESERVED_STATUS_COUNT,
|
||||
AKERR_LIBRARY_OWNER);
|
||||
|
||||
akerr_name_for_status(AKERR_NULLPOINTER, "Null Pointer Error");
|
||||
akerr_name_for_status(AKERR_OUTOFBOUNDS, "Out Of Bounds Error");
|
||||
akerr_name_for_status(AKERR_API, "API Error");
|
||||
akerr_name_for_status(AKERR_ATTRIBUTE, "Attribute Error");
|
||||
akerr_name_for_status(AKERR_TYPE, "Type Error");
|
||||
akerr_name_for_status(AKERR_KEY, "Key Error");
|
||||
akerr_name_for_status(AKERR_INDEX, "Index Error");
|
||||
akerr_name_for_status(AKERR_FORMAT, "Format Error");
|
||||
akerr_name_for_status(AKERR_IO, "Input Output Error");
|
||||
akerr_name_for_status(AKERR_VALUE, "Value Error");
|
||||
akerr_name_for_status(AKERR_RELATIONSHIP, "Relationship Error");
|
||||
akerr_name_for_status(AKERR_CIRCULAR_REFERENCE, "Circular Reference Error");
|
||||
akerr_name_for_status(AKERR_BADEXC, "Invalid akerr_ErrorContext");
|
||||
/* Every AKERR_* code gets a name; tests/err_error_names.c asserts the
|
||||
* list is exhaustive so a new code cannot be added without one. */
|
||||
akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_NULLPOINTER, "Null Pointer Error");
|
||||
akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_OUTOFBOUNDS, "Out Of Bounds Error");
|
||||
akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_API, "API Error");
|
||||
akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_ATTRIBUTE, "Attribute Error");
|
||||
akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_TYPE, "Type Error");
|
||||
akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_KEY, "Key Error");
|
||||
akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_INDEX, "Index Error");
|
||||
akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_FORMAT, "Format Error");
|
||||
akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_IO, "Input Output Error");
|
||||
akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_VALUE, "Value Error");
|
||||
akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_RELATIONSHIP, "Relationship Error");
|
||||
akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_EOF, "End Of File");
|
||||
akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_CIRCULAR_REFERENCE, "Circular Reference Error");
|
||||
akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_ITERATOR_BREAK, "Iterator Break");
|
||||
akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_NOT_IMPLEMENTED, "Not Implemented");
|
||||
akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_BADEXC, "Invalid akerr_ErrorContext");
|
||||
#if (defined(AKERR_USE_STDLIB) && AKERR_USE_STDLIB == 1) || (!defined(AKERR_USE_STDLIB))
|
||||
akerr_init_errno();
|
||||
#endif
|
||||
inited = 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,31 +204,158 @@ akerr_ErrorContext *akerr_release_error(akerr_ErrorContext *err)
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Scatter the status across the table. Status values are typically dense runs
|
||||
* (errno 1..N, then a library's block at its base), which linear probing on the
|
||||
* raw value would pile into one cluster, so mix the bits first.
|
||||
*/
|
||||
static unsigned akerr_status_hash(int status)
|
||||
{
|
||||
unsigned h = (unsigned)status;
|
||||
|
||||
h ^= h >> 16;
|
||||
h *= 0x85ebca6bu;
|
||||
h ^= h >> 13;
|
||||
h *= 0xc2b2ae35u;
|
||||
h ^= h >> 16;
|
||||
return h & (unsigned)(AKERR_STATUS_NAME_SLOTS - 1);
|
||||
}
|
||||
|
||||
/*
|
||||
* Find the slot holding `status`. With create != 0, claim a free slot for it if
|
||||
* it is not present yet. Returns NULL when the status is absent and either no
|
||||
* slot was requested or the registry is full.
|
||||
*
|
||||
* The `& (AKERR_STATUS_NAME_SLOTS - 1)` below is load-bearing and fails
|
||||
* silently: off by one in either direction and the probe indexes past
|
||||
* akerr_status_names, writing into whatever BSS follows rather than crashing.
|
||||
* A test that only counts how many names registered before the table filled
|
||||
* cannot see that -- a probe sequence collapsed to two slots still registers
|
||||
* "some" names. Any change to the probe sequence, the occupancy cap, or the
|
||||
* power-of-two assumption needs a test that reads every entry back by its own
|
||||
* distinct value; tests/err_maxval.c does.
|
||||
*/
|
||||
static akerr_StatusName *akerr_status_slot(int status, int create)
|
||||
{
|
||||
unsigned slot = akerr_status_hash(status);
|
||||
|
||||
for ( int probe = 0; probe < AKERR_STATUS_NAME_SLOTS; probe++ ) {
|
||||
akerr_StatusName *entry = &akerr_status_names[slot];
|
||||
|
||||
if ( entry->used == 0 ) {
|
||||
if ( create == 0 ||
|
||||
akerr_status_name_count >= AKERR_MAX_REGISTERED_STATUS_NAMES ) {
|
||||
return NULL;
|
||||
}
|
||||
entry->used = 1;
|
||||
entry->status = status;
|
||||
entry->name[0] = '\0';
|
||||
akerr_status_name_count++;
|
||||
return entry;
|
||||
}
|
||||
if ( entry->status == status ) {
|
||||
return entry;
|
||||
}
|
||||
slot = (slot + 1u) & (unsigned)(AKERR_STATUS_NAME_SLOTS - 1);
|
||||
}
|
||||
/* Unreachable: occupancy is capped below the slot count, so the probe above
|
||||
* always meets a free slot. Present so a future change to that cap cannot
|
||||
* turn this into a runaway loop. */
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* The reservation covering `status`, or NULL if nobody has claimed it. */
|
||||
static akerr_StatusRange *akerr_range_for_status(int status)
|
||||
{
|
||||
for ( int i = 0; i < akerr_status_range_count; i++ ) {
|
||||
if ( status >= akerr_status_ranges[i].first &&
|
||||
status <= akerr_status_ranges[i].last ) {
|
||||
return &akerr_status_ranges[i];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/*
|
||||
* Shared body of both registration entry points. A NULL owner means the caller
|
||||
* did not identify itself (the legacy two-argument akerr_name_for_status path):
|
||||
* the status must still lie inside *some* reservation, but we cannot check that
|
||||
* it is the caller's. Every refusal is logged -- a name that silently fails to
|
||||
* register degrades into "Unknown Error" in stack traces, which is exactly the
|
||||
* kind of quiet loss this registry exists to prevent.
|
||||
*/
|
||||
static int akerr_store_status_name(const char *owner, int status, const char *name)
|
||||
{
|
||||
akerr_StatusRange *range;
|
||||
akerr_StatusName *entry;
|
||||
|
||||
if ( name == NULL ) {
|
||||
return AKERR_STATUS_NAME_INVALID;
|
||||
}
|
||||
if ( owner != NULL && ( owner[0] == '\0' ||
|
||||
strlen(owner) >= AKERR_MAX_STATUS_RANGE_OWNER_LENGTH ) ) {
|
||||
return AKERR_STATUS_NAME_INVALID;
|
||||
}
|
||||
|
||||
range = akerr_range_for_status(status);
|
||||
if ( range == NULL ) {
|
||||
if ( akerr_log_method != NULL ) {
|
||||
akerr_log_method("Refusing to name status %d (\"%s\") for %s: no "
|
||||
"reserved range contains it. Call "
|
||||
"akerr_reserve_status_range() first.\n",
|
||||
status, name, owner == NULL ? "an unnamed caller" : owner);
|
||||
}
|
||||
return AKERR_STATUS_NAME_UNRESERVED;
|
||||
}
|
||||
if ( owner != NULL && strcmp(owner, range->owner) != 0 ) {
|
||||
if ( akerr_log_method != NULL ) {
|
||||
akerr_log_method("Refusing to name status %d (\"%s\") for %s: that "
|
||||
"status is in range %d..%d owned by %s.\n",
|
||||
status, name, owner,
|
||||
range->first, range->last, range->owner);
|
||||
}
|
||||
return AKERR_STATUS_NAME_FOREIGN;
|
||||
}
|
||||
|
||||
entry = akerr_status_slot(status, 1);
|
||||
if ( entry == NULL ) {
|
||||
if ( akerr_log_method != NULL ) {
|
||||
akerr_log_method("Status name registry is full (%d entries); "
|
||||
"dropping name \"%s\" for status %d. Rebuild "
|
||||
"libakerror with a larger AKERR_STATUS_NAME_SLOTS.\n",
|
||||
AKERR_MAX_REGISTERED_STATUS_NAMES, name, status);
|
||||
}
|
||||
return AKERR_STATUS_NAME_FULL;
|
||||
}
|
||||
akerr_copy_string(entry->name, AKERR_MAX_ERROR_NAME_LENGTH, name);
|
||||
return AKERR_STATUS_NAME_OK;
|
||||
}
|
||||
|
||||
/* Register a name for a status inside a range the caller reserved. */
|
||||
int akerr_register_status_name(const char *owner, int status, const char *name)
|
||||
{
|
||||
akerr_init();
|
||||
if ( owner == NULL ) {
|
||||
return AKERR_STATUS_NAME_INVALID;
|
||||
}
|
||||
return akerr_store_status_name(owner, status, name);
|
||||
}
|
||||
|
||||
/* Return or set a name. Status magnitude is unrelated to storage size. */
|
||||
char *akerr_name_for_status(int status, char *name)
|
||||
{
|
||||
int i;
|
||||
akerr_StatusName *entry;
|
||||
|
||||
for ( i = 0; i < akerr_status_name_count; i++ ) {
|
||||
if ( akerr_status_names[i].status == status ) {
|
||||
if ( name != NULL ) {
|
||||
akerr_copy_string(akerr_status_names[i].name,
|
||||
AKERR_MAX_ERROR_NAME_LENGTH, name);
|
||||
}
|
||||
return akerr_status_names[i].name;
|
||||
}
|
||||
akerr_init();
|
||||
if ( name != NULL &&
|
||||
akerr_store_status_name(NULL, status, name) != AKERR_STATUS_NAME_OK ) {
|
||||
return "Unknown Error";
|
||||
}
|
||||
if ( name != NULL ) {
|
||||
if ( akerr_status_name_count == AKERR_MAX_REGISTERED_STATUS_NAMES ) {
|
||||
return "Unknown Error";
|
||||
}
|
||||
i = akerr_status_name_count++;
|
||||
akerr_status_names[i].status = status;
|
||||
akerr_copy_string(akerr_status_names[i].name,
|
||||
AKERR_MAX_ERROR_NAME_LENGTH, name);
|
||||
return akerr_status_names[i].name;
|
||||
entry = akerr_status_slot(status, 0);
|
||||
if ( entry == NULL ) {
|
||||
return "Unknown Error";
|
||||
}
|
||||
return "Unknown Error";
|
||||
return entry->name;
|
||||
}
|
||||
|
||||
/* Reserve an inclusive status interval and reject collisions. */
|
||||
@@ -182,6 +363,7 @@ int akerr_reserve_status_range(int first_status, int count, const char *owner)
|
||||
{
|
||||
int last_status;
|
||||
|
||||
akerr_init();
|
||||
if ( count <= 0 || owner == NULL || owner[0] == '\0' ||
|
||||
strlen(owner) >= AKERR_MAX_STATUS_RANGE_OWNER_LENGTH ||
|
||||
first_status > INT_MAX - (count - 1) ) {
|
||||
@@ -209,6 +391,12 @@ int akerr_reserve_status_range(int first_status, int count, const char *owner)
|
||||
}
|
||||
}
|
||||
if ( akerr_status_range_count == AKERR_MAX_RESERVED_STATUS_RANGES ) {
|
||||
if ( akerr_log_method != NULL ) {
|
||||
akerr_log_method("Status range table is full (%d ranges); "
|
||||
"refusing %d..%d for %s.\n",
|
||||
AKERR_MAX_RESERVED_STATUS_RANGES,
|
||||
first_status, last_status, owner);
|
||||
}
|
||||
return AKERR_STATUS_RANGE_FULL;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user