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:
@@ -39,10 +39,37 @@
|
||||
#define AKERR_NOT_IMPLEMENTED (AKERR_LAST_ERRNO_VALUE + 16) /** A method was called that is defined but not currently implemented */
|
||||
#define AKERR_BADEXC (AKERR_LAST_ERRNO_VALUE + 17) /** The libakerr library was given an akerr_ErrorContext to parse that did not come from AKERR_ARRAY_ERROR (likely an uninitialized pointer) */
|
||||
|
||||
#define AKERR_STATUS_RANGE_OK 0
|
||||
#define AKERR_STATUS_RANGE_OVERLAP 1
|
||||
#define AKERR_STATUS_RANGE_FULL 2
|
||||
#define AKERR_STATUS_RANGE_INVALID 3
|
||||
/*
|
||||
* Status values 0 through 255 are reserved by libakerror at akerr_init() time:
|
||||
* the host's errno values plus the AKERR_* codes above. Consumers allocate from
|
||||
* 256 upwards. Reserving any part of this band fails with
|
||||
* AKERR_STATUS_RANGE_OVERLAP naming AKERR_LIBRARY_OWNER.
|
||||
*/
|
||||
#define AKERR_LIBRARY_OWNER "libakerror"
|
||||
#define AKERR_RESERVED_STATUS_COUNT 256
|
||||
#define AKERR_FIRST_CONSUMER_STATUS AKERR_RESERVED_STATUS_COUNT
|
||||
|
||||
/* Results of akerr_reserve_status_range(). */
|
||||
#define AKERR_STATUS_RANGE_OK 0 /** The range is now reserved by the caller */
|
||||
#define AKERR_STATUS_RANGE_OVERLAP 1 /** Some part of the range is already owned by someone else */
|
||||
#define AKERR_STATUS_RANGE_FULL 2 /** No reservation slots remain (see AKERR_MAX_RESERVED_STATUS_RANGES) */
|
||||
#define AKERR_STATUS_RANGE_INVALID 3 /** Bad count, bad owner string, or the range overflows int */
|
||||
|
||||
/* Results of akerr_register_status_name(). */
|
||||
#define AKERR_STATUS_NAME_OK 0 /** The name is registered */
|
||||
#define AKERR_STATUS_NAME_UNRESERVED 1 /** No owner has reserved a range containing this status */
|
||||
#define AKERR_STATUS_NAME_FOREIGN 2 /** The status lies in a range reserved by a different owner */
|
||||
#define AKERR_STATUS_NAME_FULL 3 /** The name registry is full (raise AKERR_STATUS_NAME_SLOTS) */
|
||||
#define AKERR_STATUS_NAME_INVALID 4 /** NULL/empty/over-long owner, or a NULL name */
|
||||
|
||||
/*
|
||||
* The library reserves status values 0 through 255 for itself (see akerr_init),
|
||||
* which must contain every AKERR_* code above. AKERR_LAST_ERRNO_VALUE is
|
||||
* derived from the host's errno list at build time, so on a platform with an
|
||||
* unusually large errno space these codes could escape the band and collide
|
||||
* with consumer codes allocated at 256. Fail the build instead.
|
||||
*/
|
||||
typedef char akerr_assert_codes_within_reserved_band[(AKERR_BADEXC < 256) ? 1 : -1];
|
||||
|
||||
#define AKERR_MAX_ARRAY_ERROR 128
|
||||
|
||||
@@ -74,7 +101,29 @@ extern akerr_ErrorContext *__akerr_last_ignored;
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akerr_release_error(akerr_ErrorContext *ptr);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akerr_next_error();
|
||||
/*
|
||||
* Look up (name == NULL) or register (name != NULL) the display name for a
|
||||
* status. Registration succeeds only if some owner has reserved a range
|
||||
* containing `status`; prefer akerr_register_status_name(), which also checks
|
||||
* that the range belongs to you and reports why a registration was refused.
|
||||
* Never returns NULL -- an unregistered status reads back as "Unknown Error".
|
||||
*/
|
||||
char *akerr_name_for_status(int status, char *name);
|
||||
|
||||
/*
|
||||
* Register a display name for a status you own. Returns AKERR_STATUS_NAME_OK,
|
||||
* or one of the AKERR_STATUS_NAME_* codes above; every refusal is also logged
|
||||
* through akerr_log_method. `owner` must match the owner string passed to
|
||||
* akerr_reserve_status_range() for the range containing `status`.
|
||||
*/
|
||||
int akerr_register_status_name(const char *owner, int status, const char *name);
|
||||
|
||||
/*
|
||||
* Claim `count` status values starting at `first_status` for `owner`. Repeating
|
||||
* an identical reservation for the same owner is a no-op; any other collision
|
||||
* is refused and logged. Returns one of the AKERR_STATUS_RANGE_* codes; treat
|
||||
* anything other than AKERR_STATUS_RANGE_OK as an initialization failure.
|
||||
*/
|
||||
int akerr_reserve_status_range(int first_status, int count, const char *owner);
|
||||
void akerr_init();
|
||||
void akerr_default_handler_unhandled_error(akerr_ErrorContext *ptr);
|
||||
|
||||
Reference in New Issue
Block a user