Both found while auditing the pool for the cross-thread transfer guarantee. AGENTS.md said the two exit() calls that bypass akerr_exit() are "both in src/error.c". ENSURE_ERROR_READY's pool-exhaustion abort is in include/akerror.tmpl.h. The count of abort sites is load-bearing -- it is the argument against any API that could fail on pool exhaustion -- so pointing at the wrong file makes it hard to check. akerr_valid_error_address() returns 1 for NULL, which is right for VALID() (NULL is how a function reports success) and a trap for anything reading it as "is this a pool slot". Say so where the function is defined. Comments only; no behavior change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
715 lines
28 KiB
C
715 lines
28 KiB
C
#include "akerror.h"
|
|
#include "lock.h"
|
|
#if defined(AKERR_USE_STDLIB) && AKERR_USE_STDLIB == 1
|
|
#include <stdlib.h>
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
#endif // AKERR_USE_STDLIB
|
|
|
|
/*
|
|
* Per-thread state.
|
|
*
|
|
* The last-ditch context is where a failure gets reported when there is no pool
|
|
* slot to report it from -- akerr_release_error(NULL). One shared copy would
|
|
* have two threads formatting a message into the same buffer, so each thread
|
|
* gets its own. Thread-local storage is zero initialized, which is why
|
|
* akerr_last_ditch_context() below sets the stack-trace cursor lazily rather
|
|
* than akerr_init() setting it for everyone: akerr_init() runs on one thread
|
|
* and cannot reach the others' copies.
|
|
*
|
|
* It is not small (an akerr_ErrorContext is tens of kilobytes), but the storage
|
|
* is allocated per thread only when that thread first touches the library's
|
|
* thread-local block, and the alternative is a shared buffer that two threads
|
|
* can be writing at once.
|
|
*/
|
|
static AKERR_THREAD_LOCAL akerr_ErrorContext __akerr_last_ditch;
|
|
AKERR_THREAD_LOCAL akerr_ErrorContext *__akerr_last_ignored;
|
|
akerr_ErrorUnhandledErrorHandler akerr_handler_unhandled_error;
|
|
akerr_ErrorLogFunction akerr_log_method = NULL;
|
|
|
|
/*
|
|
* One recursive lock over both the error pool and the status registry. See
|
|
* src/lock.h for why it is one lock, and why it is recursive.
|
|
*
|
|
* Everything below that touches either table does so through a function whose
|
|
* name ends in _locked, called from a wrapper that takes the lock and releases
|
|
* it on the single return path. The wrappers exist because the locked bodies
|
|
* are written with the FAIL_*_RETURN macros, which return from the middle of a
|
|
* function -- so those bodies cannot be the ones holding the lock.
|
|
*/
|
|
static akerr_Mutex akerr_state_lock;
|
|
|
|
/*
|
|
* 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;
|
|
|
|
typedef struct
|
|
{
|
|
int first;
|
|
int last;
|
|
char owner[AKERR_MAX_STATUS_RANGE_OWNER_LENGTH];
|
|
} akerr_StatusRange;
|
|
|
|
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;
|
|
|
|
akerr_ErrorContext AKERR_ARRAY_ERROR[AKERR_MAX_ARRAY_ERROR];
|
|
|
|
/*
|
|
* Bounded copy into a fixed buffer. Every argument is checked: this writes
|
|
* through a caller-supplied pointer for a caller-supplied length, so a NULL or
|
|
* a non-positive capacity here is a memory error waiting to happen, not
|
|
* something to absorb and return from quietly.
|
|
*
|
|
* Exported under the internal __akerr_ prefix rather than kept static so that
|
|
* tests/err_copy_string.c can reach these guards. Both in-library callers
|
|
* validate their arguments first, so nothing else can drive them.
|
|
*/
|
|
akerr_ErrorContext *__akerr_copy_string(char *destination, int capacity,
|
|
const char *source)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
FAIL_NONZERO_RETURN(errctx, (destination == NULL || source == NULL),
|
|
AKERR_NULLPOINTER,
|
|
"__akerr_copy_string got a NULL %s",
|
|
destination == NULL ? "destination buffer" : "source string");
|
|
FAIL_NONZERO_RETURN(errctx, (capacity <= 0), AKERR_VALUE,
|
|
"__akerr_copy_string got a capacity of %d; a buffer must "
|
|
"have room for at least a terminator", capacity);
|
|
|
|
strncpy(destination, source, (size_t)capacity - 1);
|
|
destination[capacity - 1] = '\0';
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/*
|
|
* 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.
|
|
*
|
|
* Takes no lock: the addresses of the pool slots are fixed for the life of the
|
|
* process, and nothing here reads a slot's contents.
|
|
*
|
|
* NULL reads back as valid, because the caller is VALID() asking whether a
|
|
* function returned something it has no business returning, and NULL is how a
|
|
* function says it succeeded. Anything reading this as "is this a pool slot"
|
|
* must check for NULL itself.
|
|
*/
|
|
int akerr_valid_error_address(akerr_ErrorContext *ptr)
|
|
{
|
|
// Is this within the memory region occupied by AKERR_ARRAY_ERROR?
|
|
if ( ptr == NULL ) {
|
|
return 1;
|
|
}
|
|
for ( int i = 0; i < AKERR_MAX_ARRAY_ERROR; i++ ) {
|
|
if ( ptr == &AKERR_ARRAY_ERROR[i] ) {
|
|
return 1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
void akerr_default_logger(const char *fmt, ...)
|
|
{
|
|
#if defined(AKERR_USE_STDLIB) && AKERR_USE_STDLIB == 1
|
|
va_list ap;
|
|
|
|
va_start(ap, fmt);
|
|
vfprintf(stderr, fmt, ap);
|
|
va_end(ap);
|
|
#else
|
|
return;
|
|
#endif
|
|
}
|
|
|
|
/*
|
|
* The library naming its own codes.
|
|
*
|
|
* akerr_init() returns void and runs before any consumer frame exists, so there
|
|
* is nothing to PASS an error to: this *is* the top of the stack. FINISH_NORETURN
|
|
* is the library's idiom for that position -- the same one main() uses -- so an
|
|
* unhandled failure prints its stack trace and goes to
|
|
* akerr_handler_unhandled_error, which terminates.
|
|
*
|
|
* That is fatal on purpose. The library can only fail to name its own status
|
|
* codes if the build is misconfigured -- a name table too small to hold even
|
|
* the library's own entries, or a reservation that did not take -- and the
|
|
* consequence of continuing is every later stack trace in the process printing
|
|
* "Unknown Error" for a code the library defines. That is a startup defect, and
|
|
* it is far cheaper to see it at init than to debug it from a degraded trace.
|
|
*
|
|
* The generated errno table calls this rather than registering names directly,
|
|
* so that all of this control flow lives here in one place.
|
|
*/
|
|
void __akerr_name_library_status(int status, const char *name)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
ATTEMPT {
|
|
CATCH(errctx, akerr_register_status_name(AKERR_LIBRARY_OWNER, status, name));
|
|
} CLEANUP {
|
|
} PROCESS(errctx) {
|
|
} FINISH_NORETURN(errctx);
|
|
}
|
|
|
|
/*
|
|
* The calling thread's last-ditch context.
|
|
*
|
|
* Thread-local storage starts zeroed, so a NULL stack-trace cursor means this
|
|
* thread has not used its copy yet. Checking the cursor rather than a separate
|
|
* flag keeps the whole thing self-describing: the cursor is the one field that
|
|
* must not be zero for the context to be usable at all.
|
|
*/
|
|
static akerr_ErrorContext *akerr_last_ditch_context(void)
|
|
{
|
|
if ( __akerr_last_ditch.stacktracebufptr == NULL ) {
|
|
memset((void *)&__akerr_last_ditch, 0x00, sizeof(akerr_ErrorContext));
|
|
__akerr_last_ditch.stacktracebufptr = (char *)&__akerr_last_ditch.stacktracebuf;
|
|
}
|
|
return &__akerr_last_ditch;
|
|
}
|
|
|
|
static AKERR_THREAD_LOCAL int akerr_initializing;
|
|
static akerr_Once akerr_state_once = AKERR_ONCE_INIT;
|
|
|
|
/*
|
|
* Runs exactly once per process, under akerr_once(). Everything it calls --
|
|
* the registry entry points, and the pool underneath them -- calls akerr_init()
|
|
* itself, so the first thing it does is raise the re-entry flag; see
|
|
* akerr_init() below.
|
|
*
|
|
* The lock is initialized before anything that could take it. That ordering is
|
|
* the reason this work lives in a once-routine instead of a
|
|
* check-a-flag-and-go: a second thread arriving while this one is still
|
|
* populating the tables must block until they are complete, not walk them.
|
|
*/
|
|
static void akerr_init_state(void)
|
|
{
|
|
akerr_mutex_init(&akerr_state_lock);
|
|
akerr_initializing = 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;
|
|
AKERR_ARRAY_ERROR[i].stacktracebufptr = (char *)&AKERR_ARRAY_ERROR[i].stacktracebuf;
|
|
}
|
|
__akerr_last_ignored = NULL;
|
|
(void)akerr_last_ditch_context();
|
|
if ( akerr_log_method == NULL ) {
|
|
akerr_log_method = &akerr_default_logger;
|
|
}
|
|
akerr_handler_unhandled_error = &akerr_default_handler_unhandled_error;
|
|
memset((void *)&akerr_status_names[0], 0x00, sizeof(akerr_status_names));
|
|
memset((void *)&akerr_status_ranges[0], 0x00, sizeof(akerr_status_ranges));
|
|
akerr_status_name_count = 0;
|
|
akerr_status_range_count = 0;
|
|
|
|
/* 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. Terminal for the same reason as
|
|
* __akerr_name_library_status(), and handled the same way: without this
|
|
* band the library owns nothing, so none of the names below could
|
|
* register either. */
|
|
PREPARE_ERROR(errctx);
|
|
ATTEMPT {
|
|
CATCH(errctx, akerr_reserve_status_range(0, AKERR_RESERVED_STATUS_COUNT,
|
|
AKERR_LIBRARY_OWNER));
|
|
} CLEANUP {
|
|
} PROCESS(errctx) {
|
|
} FINISH_NORETURN(errctx);
|
|
|
|
/* 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_name_library_status(AKERR_NULLPOINTER, "Null Pointer Error");
|
|
__akerr_name_library_status(AKERR_OUTOFBOUNDS, "Out Of Bounds Error");
|
|
__akerr_name_library_status(AKERR_API, "API Error");
|
|
__akerr_name_library_status(AKERR_ATTRIBUTE, "Attribute Error");
|
|
__akerr_name_library_status(AKERR_TYPE, "Type Error");
|
|
__akerr_name_library_status(AKERR_KEY, "Key Error");
|
|
__akerr_name_library_status(AKERR_INDEX, "Index Error");
|
|
__akerr_name_library_status(AKERR_FORMAT, "Format Error");
|
|
__akerr_name_library_status(AKERR_IO, "Input Output Error");
|
|
__akerr_name_library_status(AKERR_VALUE, "Value Error");
|
|
__akerr_name_library_status(AKERR_RELATIONSHIP, "Relationship Error");
|
|
__akerr_name_library_status(AKERR_EOF, "End Of File");
|
|
__akerr_name_library_status(AKERR_CIRCULAR_REFERENCE, "Circular Reference Error");
|
|
__akerr_name_library_status(AKERR_ITERATOR_BREAK, "Iterator Break");
|
|
__akerr_name_library_status(AKERR_NOT_IMPLEMENTED, "Not Implemented");
|
|
__akerr_name_library_status(AKERR_BADEXC, "Invalid akerr_ErrorContext");
|
|
__akerr_name_library_status(AKERR_STATUS_RANGE_OVERLAP, "Status Range Overlap");
|
|
__akerr_name_library_status(AKERR_STATUS_RANGE_FULL, "Status Range Table Full");
|
|
__akerr_name_library_status(AKERR_STATUS_RANGE_INVALID, "Invalid Status Range");
|
|
__akerr_name_library_status(AKERR_STATUS_NAME_UNRESERVED, "Unreserved Status Name");
|
|
__akerr_name_library_status(AKERR_STATUS_NAME_FOREIGN, "Foreign Status Name");
|
|
__akerr_name_library_status(AKERR_STATUS_NAME_FULL, "Status Name Registry Full");
|
|
__akerr_name_library_status(AKERR_STATUS_NAME_INVALID, "Invalid Status Name");
|
|
#if (defined(AKERR_USE_STDLIB) && AKERR_USE_STDLIB == 1) || (!defined(AKERR_USE_STDLIB))
|
|
akerr_init_errno();
|
|
#endif
|
|
|
|
akerr_initializing = 0;
|
|
}
|
|
|
|
/*
|
|
* Idempotent, and safe to call from any thread at any time. Every public entry
|
|
* point calls it -- 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 -- which means it is also on the path of everything
|
|
* akerr_init_state() itself calls.
|
|
*
|
|
* The re-entry guard is thread local, and has to be: only the thread running
|
|
* the once-routine may skip past it. A second thread arriving mid-initialization
|
|
* must block inside akerr_once() until the tables are complete, which a shared
|
|
* flag set at the top of initialization would have let it walk right past.
|
|
*/
|
|
void akerr_init()
|
|
{
|
|
if ( akerr_initializing != 0 ) {
|
|
return;
|
|
}
|
|
akerr_once(&akerr_state_once, &akerr_init_state);
|
|
}
|
|
|
|
/*
|
|
* Every way out of the library's status space goes through here, so that a
|
|
* status becomes an exit code exactly one way no matter who is leaving.
|
|
*
|
|
* Only 0 through AKERR_EXIT_STATUS_MAX survive the trip -- see the note on
|
|
* AKERR_EXIT_STATUS_UNREPRESENTABLE in the header for why there is no wider
|
|
* exit() to reach for. Everything else exits with that sentinel instead of its
|
|
* low byte, because the low byte is either a lie (status 300 exiting 44, which
|
|
* is some other error's code) or a disaster (status 256, the first status a
|
|
* consumer can own, exiting 0 and telling the shell the program succeeded).
|
|
*
|
|
* Status 0 exits 0, because 0 is this library's success status and an exit code
|
|
* of 0 is what success is called out here. What keeps an *unhandled* error from
|
|
* exiting 0 is not this function: PROCESS opens with `case 0`, which marks a
|
|
* zero status handled, so a successful context can never reach
|
|
* FINISH_NORETURN's call to the handler in the first place.
|
|
*
|
|
* Nothing is logged here. Callers arrive from a position that has already
|
|
* reported -- FINISH_NORETURN logs the stack trace, carrying the status at full
|
|
* width, before it calls the handler -- and a second line naming a number the
|
|
* trace already gave would only invite the reader to trust the exit code.
|
|
*/
|
|
void akerr_exit(int status)
|
|
{
|
|
if ( status < 0 || status > AKERR_EXIT_STATUS_MAX ) {
|
|
exit(AKERR_EXIT_STATUS_UNREPRESENTABLE);
|
|
}
|
|
exit(status);
|
|
}
|
|
|
|
/*
|
|
* The last stop for an unhandled error. A handler invoked with no error at all
|
|
* has no status to report and nothing akerr_exit() could map, so it exits 1
|
|
* directly: a plain generic failure.
|
|
*/
|
|
void akerr_default_handler_unhandled_error(akerr_ErrorContext *errctx)
|
|
{
|
|
if ( errctx == NULL ) {
|
|
exit(1);
|
|
}
|
|
akerr_exit(errctx->status);
|
|
}
|
|
|
|
/*
|
|
* Claim the lowest free slot. Finding it and taking the reference are one
|
|
* operation under the lock: a scan that returned an unclaimed slot would hand
|
|
* the same one to every thread that scanned before the first of them got around
|
|
* to incrementing the count.
|
|
*/
|
|
akerr_ErrorContext *akerr_next_error()
|
|
{
|
|
akerr_ErrorContext *found = (akerr_ErrorContext *)NULL;
|
|
|
|
akerr_init();
|
|
akerr_mutex_lock(&akerr_state_lock);
|
|
for (int i = 0; i < AKERR_MAX_ARRAY_ERROR; i++ ) {
|
|
if ( AKERR_ARRAY_ERROR[i].refcount == 0 ) {
|
|
found = &AKERR_ARRAY_ERROR[i];
|
|
found->refcount = 1;
|
|
break;
|
|
}
|
|
}
|
|
akerr_mutex_unlock(&akerr_state_lock);
|
|
return found;
|
|
}
|
|
|
|
/*
|
|
* The wipe returns the slot to the pool, so it and the decrement that triggers
|
|
* it are one operation under the lock. Otherwise a thread that saw the count
|
|
* reach zero could be handed the slot by akerr_next_error() and start writing
|
|
* its error into it while the releasing thread was still memsetting it.
|
|
*/
|
|
akerr_ErrorContext *akerr_release_error(akerr_ErrorContext *err)
|
|
{
|
|
int oldid = 0;
|
|
akerr_ErrorContext *remaining = err;
|
|
|
|
akerr_init();
|
|
if ( err == NULL ) {
|
|
akerr_ErrorContext *errctx = akerr_last_ditch_context();
|
|
FAIL_RETURN(errctx, AKERR_NULLPOINTER, "akerr_release_error got NULL context pointer");
|
|
}
|
|
akerr_mutex_lock(&akerr_state_lock);
|
|
if ( err->refcount > 0 ) {
|
|
err->refcount -= 1;
|
|
}
|
|
if ( err->refcount == 0 ) {
|
|
oldid = err->arrayid;
|
|
memset(err, 0x00, sizeof(akerr_ErrorContext));
|
|
err->stacktracebufptr = (char *)&err->stacktracebuf;
|
|
err->arrayid = oldid;
|
|
remaining = NULL;
|
|
}
|
|
akerr_mutex_unlock(&akerr_state_lock);
|
|
return remaining;
|
|
}
|
|
|
|
|
|
/*
|
|
* 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. Caller holds akerr_state_lock.
|
|
*
|
|
* 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. Caller
|
|
* holds akerr_state_lock. */
|
|
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 raises an error -- 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 -- so the
|
|
* message carries everything a caller needs to see in a stack trace.
|
|
*
|
|
* Caller holds akerr_state_lock. The FAIL_* macros below re-enter the library
|
|
* to build their error -- a pool slot from akerr_next_error(), and a status
|
|
* name for the stack trace -- and that re-entry is why the lock is recursive.
|
|
*/
|
|
static akerr_ErrorContext AKERR_NOIGNORE *akerr_store_status_name_locked(const char *owner,
|
|
int status,
|
|
const char *name)
|
|
{
|
|
akerr_StatusRange *range;
|
|
akerr_StatusName *entry;
|
|
PREPARE_ERROR(errctx);
|
|
|
|
FAIL_NONZERO_RETURN(errctx, (name == NULL), AKERR_STATUS_NAME_INVALID,
|
|
"Refusing to name status %d for %s: the name is NULL",
|
|
status, owner == NULL ? "an unnamed caller" : owner);
|
|
FAIL_NONZERO_RETURN(errctx,
|
|
(owner != NULL && ( owner[0] == '\0' ||
|
|
strlen(owner) >= AKERR_MAX_STATUS_RANGE_OWNER_LENGTH )),
|
|
AKERR_STATUS_NAME_INVALID,
|
|
"Refusing to name status %d (\"%s\"): the owner string "
|
|
"is empty or longer than %d characters",
|
|
status, name, AKERR_MAX_STATUS_RANGE_OWNER_LENGTH - 1);
|
|
|
|
range = akerr_range_for_status(status);
|
|
FAIL_ZERO_RETURN(errctx, range, AKERR_STATUS_NAME_UNRESERVED,
|
|
"Refusing to name status %d (\"%s\") for %s: no reserved "
|
|
"range contains it. Call akerr_reserve_status_range() first.",
|
|
status, name, owner == NULL ? "an unnamed caller" : owner);
|
|
FAIL_NONZERO_RETURN(errctx,
|
|
(owner != NULL && strcmp(owner, range->owner) != 0),
|
|
AKERR_STATUS_NAME_FOREIGN,
|
|
"Refusing to name status %d (\"%s\") for %s: that status "
|
|
"is in range %d..%d owned by %s.",
|
|
status, name, owner,
|
|
range->first, range->last, range->owner);
|
|
|
|
entry = akerr_status_slot(status, 1);
|
|
FAIL_ZERO_RETURN(errctx, entry, AKERR_STATUS_NAME_FULL,
|
|
"Status name registry is full (%d entries); dropping name "
|
|
"\"%s\" for status %d. Rebuild libakerror with a larger "
|
|
"AKERR_STATUS_NAME_SLOTS.",
|
|
AKERR_MAX_REGISTERED_STATUS_NAMES, name, status);
|
|
|
|
PASS(errctx, __akerr_copy_string(entry->name, AKERR_MAX_ERROR_NAME_LENGTH, name));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/*
|
|
* Register a name for a status inside a range the caller reserved. Both strings
|
|
* are checked here rather than only inside akerr_store_status_name_locked(): the
|
|
* store accepts a NULL owner for the legacy akerr_name_for_status() path, so a
|
|
* NULL arriving through *this* entry point would be read as "caller did not
|
|
* identify itself" and skip the ownership check entirely.
|
|
*
|
|
* Caller holds akerr_state_lock.
|
|
*/
|
|
static akerr_ErrorContext AKERR_NOIGNORE *akerr_register_status_name_locked(const char *owner,
|
|
int status,
|
|
const char *name)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
FAIL_NONZERO_RETURN(errctx, (owner == NULL), AKERR_STATUS_NAME_INVALID,
|
|
"Refusing to name status %d: the owner string is NULL. "
|
|
"Pass the same owner you reserved the range with.",
|
|
status);
|
|
FAIL_NONZERO_RETURN(errctx, (name == NULL), AKERR_STATUS_NAME_INVALID,
|
|
"Refusing to name status %d for %s: the name is NULL",
|
|
status, owner);
|
|
PASS(errctx, akerr_store_status_name_locked(owner, status, name));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akerr_register_status_name(const char *owner, int status, const char *name)
|
|
{
|
|
akerr_ErrorContext *errctx;
|
|
|
|
akerr_init();
|
|
akerr_mutex_lock(&akerr_state_lock);
|
|
errctx = akerr_register_status_name_locked(owner, status, name);
|
|
akerr_mutex_unlock(&akerr_state_lock);
|
|
return errctx;
|
|
}
|
|
|
|
/*
|
|
* Return or set a name. Status magnitude is unrelated to storage size.
|
|
*
|
|
* The set path is the legacy two-argument form. It returns a name, so it cannot
|
|
* hand an error back to its caller and cannot raise: it handles the refusal
|
|
* here, converting it to the "Unknown Error" sentinel the way any function that
|
|
* must return a value converts a caught error into one.
|
|
* akerr_register_status_name() is the form that raises.
|
|
*
|
|
* The lookup path (name == NULL) deliberately stays clear of all of this. FAIL
|
|
* calls it to render a status into a stack trace, so it must not itself need an
|
|
* error context.
|
|
*
|
|
* The name is returned by pointer into the registry, which never resizes and
|
|
* never removes an entry, so the pointer is good for the life of the process.
|
|
* Its *contents* are stable as long as nobody registers a second name for the
|
|
* same status: a rename overwrites the buffer in place, and a lookup on another
|
|
* thread can be reading it. Register names during initialization -- renaming a
|
|
* live status while other threads run is the one registry operation the lock
|
|
* cannot make safe, because the reader is outside it by then.
|
|
*/
|
|
static akerr_ErrorContext AKERR_NOIGNORE *akerr_store_status_name(const char *owner,
|
|
int status,
|
|
const char *name)
|
|
{
|
|
akerr_ErrorContext *errctx;
|
|
|
|
akerr_mutex_lock(&akerr_state_lock);
|
|
errctx = akerr_store_status_name_locked(owner, status, name);
|
|
akerr_mutex_unlock(&akerr_state_lock);
|
|
return errctx;
|
|
}
|
|
|
|
char *akerr_name_for_status(int status, char *name)
|
|
{
|
|
akerr_StatusName *entry;
|
|
char *found = "Unknown Error";
|
|
|
|
akerr_init();
|
|
if ( name != NULL ) {
|
|
PREPARE_ERROR(errctx);
|
|
int refused = 0;
|
|
|
|
/* The store takes and releases the lock itself, so the handler below
|
|
* calls akerr_log_method -- consumer code, which may do anything at all
|
|
* including calling back into this library -- without holding it. */
|
|
ATTEMPT {
|
|
CATCH(errctx, akerr_store_status_name(NULL, status, name));
|
|
} CLEANUP {
|
|
} PROCESS(errctx) {
|
|
} HANDLE_DEFAULT(errctx) {
|
|
LOG_ERROR_WITH_MESSAGE(errctx, "** REFUSED STATUS NAME **");
|
|
refused = 1;
|
|
} FINISH_NORETURN(errctx);
|
|
|
|
if ( refused != 0 ) {
|
|
return "Unknown Error";
|
|
}
|
|
}
|
|
akerr_mutex_lock(&akerr_state_lock);
|
|
entry = akerr_status_slot(status, 0);
|
|
if ( entry != NULL ) {
|
|
found = entry->name;
|
|
}
|
|
akerr_mutex_unlock(&akerr_state_lock);
|
|
return found;
|
|
}
|
|
|
|
/* Reserve an inclusive status interval and reject collisions. Caller holds
|
|
* akerr_state_lock: the overlap scan and the entry that follows it are one
|
|
* decision, so two threads claiming overlapping ranges at once must not be able
|
|
* to both find the table clear. */
|
|
static akerr_ErrorContext AKERR_NOIGNORE *akerr_reserve_status_range_locked(int first_status,
|
|
int count,
|
|
const char *owner)
|
|
{
|
|
int last_status;
|
|
PREPARE_ERROR(errctx);
|
|
|
|
FAIL_NONZERO_RETURN(errctx,
|
|
(count <= 0 || owner == NULL || owner[0] == '\0' ||
|
|
strlen(owner) >= AKERR_MAX_STATUS_RANGE_OWNER_LENGTH ||
|
|
first_status > INT_MAX - (count - 1)),
|
|
AKERR_STATUS_RANGE_INVALID,
|
|
"Invalid status range reservation: %d status values from "
|
|
"%d for %s (count must be positive, the owner string "
|
|
"non-empty and shorter than %d characters, and the range "
|
|
"must not overflow int)",
|
|
count, first_status, owner == NULL ? "(null)" : owner,
|
|
AKERR_MAX_STATUS_RANGE_OWNER_LENGTH);
|
|
last_status = first_status + count - 1;
|
|
|
|
for ( int i = 0; i < akerr_status_range_count; i++ ) {
|
|
if ( first_status <= akerr_status_ranges[i].last &&
|
|
last_status >= akerr_status_ranges[i].first ) {
|
|
if ( first_status == akerr_status_ranges[i].first &&
|
|
last_status == akerr_status_ranges[i].last &&
|
|
strcmp(owner, akerr_status_ranges[i].owner) == 0 ) {
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
FAIL_RETURN(errctx, AKERR_STATUS_RANGE_OVERLAP,
|
|
"Status range %d..%d requested by %s overlaps %d..%d "
|
|
"owned by %s",
|
|
first_status, last_status, owner,
|
|
akerr_status_ranges[i].first,
|
|
akerr_status_ranges[i].last,
|
|
akerr_status_ranges[i].owner);
|
|
}
|
|
}
|
|
FAIL_NONZERO_RETURN(errctx,
|
|
(akerr_status_range_count == AKERR_MAX_RESERVED_STATUS_RANGES),
|
|
AKERR_STATUS_RANGE_FULL,
|
|
"Status range table is full (%d ranges); refusing %d..%d "
|
|
"for %s.",
|
|
AKERR_MAX_RESERVED_STATUS_RANGES,
|
|
first_status, last_status, owner);
|
|
|
|
/* The owner copy is what commits the entry, so the count only advances
|
|
* once it has succeeded -- a half-written reservation would claim the
|
|
* range under an empty owner nobody could ever match. */
|
|
akerr_status_ranges[akerr_status_range_count].first = first_status;
|
|
akerr_status_ranges[akerr_status_range_count].last = last_status;
|
|
PASS(errctx, __akerr_copy_string(akerr_status_ranges[akerr_status_range_count].owner,
|
|
AKERR_MAX_STATUS_RANGE_OWNER_LENGTH, owner));
|
|
akerr_status_range_count++;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akerr_reserve_status_range(int first_status, int count, const char *owner)
|
|
{
|
|
akerr_ErrorContext *errctx;
|
|
|
|
akerr_init();
|
|
akerr_mutex_lock(&akerr_state_lock);
|
|
errctx = akerr_reserve_status_range_locked(first_status, count, owner);
|
|
akerr_mutex_unlock(&akerr_state_lock);
|
|
return errctx;
|
|
}
|