9 Commits

Author SHA1 Message Date
5a269ea01b Expose ignored error snapshot
All checks were successful
libakerror CI Build / coverage (push) Successful in 3m18s
libakerror CI Build / cmake_build (push) Successful in 10m11s
libakerror CI Build / mutation_test (push) Successful in 45m33s
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-08-04 11:24:30 -04:00
55090d2419 Preserve ignored error snapshots
Some checks failed
libakerror CI Build / coverage (push) Successful in 3m39s
libakerror CI Build / cmake_build (push) Successful in 6m3s
libakerror CI Build / mutation_test (push) Has been cancelled
2026-08-04 10:44:46 -04:00
4fe7571329 Release ignored error contexts
All checks were successful
libakerror CI Build / cmake_build (push) Successful in 2m52s
libakerror CI Build / coverage (push) Successful in 2m53s
libakerror CI Build / mutation_test (push) Successful in 44m12s
2026-08-03 13:46:00 -04:00
fdb8ffaad0 Merge pull request 'Annotate intentional handler fallthrough' (#19) from 18 into main
All checks were successful
libakerror CI Build / coverage (push) Successful in 2m47s
libakerror CI Build / cmake_build (push) Successful in 2m53s
libakerror CI Build / mutation_test (push) Successful in 39m42s
Reviewed-on: #19
2026-08-03 12:14:30 -04:00
7d0e467181 Disable ThreadSanitizer CI pending runner fix
All checks were successful
libakerror CI Build / cmake_build (push) Successful in 2m53s
libakerror CI Build / coverage (push) Successful in 2m50s
libakerror CI Build / mutation_test (push) Successful in 37m30s
Co-authored-by: Andrew Kesterson <andrew@aklabs.net>
2026-08-03 09:31:03 -04:00
997828116b Install the ThreadSanitizer ASLR runner
Some checks failed
libakerror CI Build / cmake_build (push) Successful in 2m53s
libakerror CI Build / coverage (push) Successful in 2m52s
libakerror CI Build / thread_sanitizer (push) Failing after 2m52s
libakerror CI Build / mutation_test (push) Successful in 43m58s
Co-authored-by: Andrew Kesterson <andrew@aklabs.net>
2026-08-03 07:49:28 -04:00
0a6cb303f5 Run ThreadSanitizer tests without ASLR
Some checks failed
libakerror CI Build / coverage (push) Successful in 2m49s
libakerror CI Build / thread_sanitizer (push) Failing after 2m54s
libakerror CI Build / cmake_build (push) Successful in 3m3s
libakerror CI Build / mutation_test (push) Has been cancelled
Co-authored-by: Andrew Kesterson <andrew@aklabs.net>
2026-08-03 07:28:40 -04:00
a902c95155 Annotate intentional handler fallthrough
Some checks failed
libakerror CI Build / coverage (push) Successful in 2m51s
libakerror CI Build / cmake_build (push) Successful in 2m53s
libakerror CI Build / thread_sanitizer (push) Failing after 5m53s
libakerror CI Build / mutation_test (push) Successful in 37m49s
Co-authored-by: Andrew Kesterson <andrew@aklabs.net>
2026-08-02 23:52:04 -04:00
11cc5578fe Repoint the last TODO.md citations at the tracker
Some checks failed
libakerror CI Build / coverage (push) Successful in 2m48s
libakerror CI Build / cmake_build (push) Successful in 2m50s
libakerror CI Build / thread_sanitizer (push) Failing after 2m47s
libakerror CI Build / mutation_test (push) Successful in 39m57s
docs/building.md sent a reader to TODO.md for what a stdlib replacement must
provide; the AKERR_USE_STDLIB=OFF build does not compile at all, which is issue
#12, so it says that instead.

Found while sweeping the consumer repositories: the handler macros trip
-Wimplicit-fallthrough, which is why libakgl cannot adopt -Wextra. That was
recorded only in libakgl and is now issue #18 -- the fourth defect in this
library found written down in a consumer rather than here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-08-02 22:01:26 -04:00
11 changed files with 102 additions and 66 deletions

View File

@@ -67,34 +67,6 @@ jobs:
fail_on_failure: 'false' fail_on_failure: 'false'
- run: echo "🍏 This job's status is ${{ job.status }}." - run: echo "🍏 This job's status is ${{ job.status }}."
thread_sanitizer:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: dependencies
run: |
sudo apt-get update -y
sudo apt-get install -y cmake gcc moreutils
# The thread tests assert exclusive ownership of pool slots and reserved
# ranges, which is checkable without tooling and runs in the job above.
# This is the run that proves there is no data race underneath them.
# libtsan arrives with gcc (libgcc-N-dev depends on it); the script
# disables ASLR because TSan aborts on kernels with vm.mmap_rnd_bits > 28.
- name: thread sanitizer
run: |
scripts/thread_test.sh build/tsan --output-junit "$(pwd)/tsan-junit.xml"
- name: publish thread sanitizer results
if: always()
uses: mikepenz/action-junit-report@v4
with:
report_paths: 'tsan-junit.xml'
annotate_only: true
detailed_summary: true
include_passed: true
fail_on_failure: 'true'
- run: echo "🍏 This job's status is ${{ job.status }}."
mutation_test: mutation_test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:

View File

@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.10) cmake_minimum_required(VERSION 3.10)
# 1.0.0 replaced the consumer-sized __AKERR_ERROR_NAMES array with private # 1.0.0 replaced the consumer-sized __AKERR_ERROR_NAMES array with private
# storage. 2.0.0 makes the library thread safe, which is a second ABI break in # storage. 2.0.0 makes the library thread safe, which is a second ABI break in
# the same places: __akerr_last_ignored became thread-local storage, and # the same places: akerr_last_ignored became thread-local storage, and
# ENSURE_ERROR_READY no longer takes the pool reference that akerr_next_error() # ENSURE_ERROR_READY no longer takes the pool reference that akerr_next_error()
# now takes for it. Consumer code compiled against a 1.x header would # now takes for it. Consumer code compiled against a 1.x header would
# double-count every reference. Hence the major bump and the SOVERSION, so a # double-count every reference. Hence the major bump and the SOVERSION, so a
@@ -107,6 +107,19 @@ if(AKERR_SANITIZE AND NOT CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
"AKERR_SANITIZE requires GCC or Clang, not ${CMAKE_C_COMPILER_ID}") "AKERR_SANITIZE requires GCC or Clang, not ${CMAKE_C_COMPILER_ID}")
endif() endif()
if(AKERR_SANITIZE STREQUAL "thread" AND CMAKE_SYSTEM_NAME STREQUAL "Linux")
# ThreadSanitizer reserves a fixed, very large virtual-address range. Linux
# ASLR can put a loader mapping inside it before the runtime starts, which
# makes every test fail with "unexpected memory mapping" before main().
# Run the instrumented tests under the normal util-linux ASLR wrapper.
find_program(AKERR_SETARCH_EXECUTABLE setarch)
if(NOT AKERR_SETARCH_EXECUTABLE)
message(FATAL_ERROR
"AKERR_SANITIZE=thread on Linux requires setarch (util-linux) to "
"start tests with ASLR disabled.")
endif()
endif()
function(akerr_instrument_for_sanitizers _target) function(akerr_instrument_for_sanitizers _target)
if(AKERR_SANITIZE) if(AKERR_SANITIZE)
target_compile_options(${_target} PRIVATE target_compile_options(${_target} PRIVATE
@@ -252,7 +265,13 @@ foreach(_test IN LISTS AKERR_TESTS)
target_link_libraries(test_${_test} PRIVATE Threads::Threads) target_link_libraries(test_${_test} PRIVATE Threads::Threads)
endif() endif()
akerr_instrument_for_sanitizers(test_${_test}) akerr_instrument_for_sanitizers(test_${_test})
add_test(NAME ${_test} COMMAND test_${_test}) if(AKERR_SANITIZE STREQUAL "thread" AND CMAKE_SYSTEM_NAME STREQUAL "Linux")
add_test(NAME ${_test}
COMMAND ${AKERR_SETARCH_EXECUTABLE} ${CMAKE_SYSTEM_PROCESSOR}
-R $<TARGET_FILE:test_${_test}>)
else()
add_test(NAME ${_test} COMMAND test_${_test})
endif()
# A sanitizer report is a test failure. Without halt_on_error the runtime # A sanitizer report is a test failure. Without halt_on_error the runtime
# prints and continues, which leaves a race to be noticed in the log by # prints and continues, which leaves a race to be noticed in the log by
# somebody reading it -- and under a race storm the reporting itself is slow # somebody reading it -- and under a race storm the reporting itself is slow
@@ -263,6 +282,14 @@ foreach(_test IN LISTS AKERR_TESTS)
endif() endif()
endforeach() endforeach()
# HANDLE_GROUP deliberately enters the next case label. Keep that public macro
# compiling with the warning enabled, so a future macro edit cannot restore the
# warning for consumers which adopt -Wextra.
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(test_err_handle_group PRIVATE
-Werror=implicit-fallthrough)
endif()
set_tests_properties( set_tests_properties(
${AKERR_WILL_FAIL_TESTS} ${AKERR_WILL_FAIL_TESTS}
PROPERTIES WILL_FAIL TRUE PROPERTIES WILL_FAIL TRUE

View File

@@ -170,7 +170,7 @@ the exit code was the status truncated to a byte, and every consumer status
starts at 256. Use `akerr_exit()` instead of `exit()` — see starts at 256. Use `akerr_exit()` instead of `exit()` — see
[docs/exit-status.md](docs/exit-status.md). No ABI break. [docs/exit-status.md](docs/exit-status.md). No ABI break.
2.0.0 makes the library thread safe. That is an ABI break — `__akerr_last_ignored` 2.0.0 makes the library thread safe. That is an ABI break — `akerr_last_ignored`
became thread-local storage and the pool now takes its own reference — so became thread-local storage and the pool now takes its own reference — so
everything built against a 1.x header must be rebuilt. 1.0.0 replaced the everything built against a 1.x header must be rebuilt. 1.0.0 replaced the
consumer-sized status-name array with a private, ownership-enforced registry. consumer-sized status-name array with a private, ownership-enforced registry.

View File

@@ -52,7 +52,7 @@ accident.
What moved at the ABI: What moved at the ABI:
* `__akerr_last_ignored` is thread-local storage. An ignored error is a fact * `akerr_last_ignored` is thread-local storage. An ignored error is a fact
about the thread that ignored it, and one shared slot had two threads about the thread that ignored it, and one shared slot had two threads
overwriting each other's. The `IGNORE` macro expands at *your* call site, so overwriting each other's. The `IGNORE` macro expands at *your* call site, so
your objects reference the symbol under whichever storage model your header your objects reference the symbol under whichever storage model your header

View File

@@ -58,4 +58,6 @@ cmake --install build
`PATH_MAX` and `NULL` are used unconditionally but only included under the `PATH_MAX` and `NULL` are used unconditionally but only included under the
stdlib branch, so the header's includes need untangling before stdlib branch, so the header's includes need untangling before
`-DAKERR_USE_STDLIB=OFF` builds. The list above still states what a replacement `-DAKERR_USE_STDLIB=OFF` builds. The list above still states what a replacement
must provide. See [TODO.md](../TODO.md). must provide. That configuration does not currently compile -- `bool`, `PATH_MAX` and `NULL`
are used unconditionally but included only under the stdlib branch. See
[issue #12](https://source.starfort.tech/andrew/libakerror/issues/12).

View File

@@ -14,9 +14,12 @@ What that covers:
against each other and against lookups. Two threads reserving the same range against each other and against lookups. Two threads reserving the same range
cannot both win — exactly one gets `NULL` and the other gets cannot both win — exactly one gets `NULL` and the other gets
`AKERR_STATUS_RANGE_OVERLAP` naming the winner. `AKERR_STATUS_RANGE_OVERLAP` naming the winner.
* **Per-thread state.** The context behind `IGNORE` (`__akerr_last_ignored`) and * **Per-thread state.** `IGNORE` copies the swallowed context into its
the last-ditch context used to report `akerr_release_error(NULL)` are thread-local `akerr_last_ignored` snapshot before releasing the pool slot.
thread-local, so one thread's ignored error is never another's. The snapshot remains valid until that thread ignores another error, so a
later pool checkout cannot overwrite it. The snapshot and the last-ditch
context used to report `akerr_release_error(NULL)` are thread-local, so
concurrent calls cannot overwrite each other's state.
* **Handing a context from one thread to another.** A context is not thread * **Handing a context from one thread to another.** A context is not thread
state — it lives in `AKERR_ARRAY_ERROR`, which is process-global — so it state — it lives in `AKERR_ARRAY_ERROR`, which is process-global — so it
outlives the thread that raised it. The reference count is the only field the outlives the thread that raised it. The reference count is the only field the

View File

@@ -15,7 +15,7 @@
* scripts/generrno.sh stamps this value in at build time from the AKERR_THREADS * scripts/generrno.sh stamps this value in at build time from the AKERR_THREADS
* build option, the same way it stamps AKERR_LAST_ERRNO_VALUE. It is generated * build option, the same way it stamps AKERR_LAST_ERRNO_VALUE. It is generated
* rather than defined by the consumer on purpose: whether the library * rather than defined by the consumer on purpose: whether the library
* serializes its global state and whether __akerr_last_ignored is a * serializes its global state and whether akerr_last_ignored is a
* thread-local are the same decision, and a consumer that disagreed with the * thread-local are the same decision, and a consumer that disagreed with the
* library about it would link against a differently shaped symbol. * library about it would link against a differently shaped symbol.
* *
@@ -173,11 +173,12 @@ extern akerr_ErrorContext AKERR_ARRAY_ERROR[AKERR_MAX_ARRAY_ERROR];
extern akerr_ErrorUnhandledErrorHandler akerr_handler_unhandled_error; extern akerr_ErrorUnhandledErrorHandler akerr_handler_unhandled_error;
extern akerr_ErrorLogFunction akerr_log_method; extern akerr_ErrorLogFunction akerr_log_method;
/* /*
* The error IGNORE() last swallowed, per thread: an ignored error is a fact * IGNORE()'s public per-thread snapshot. IGNORE() copies the swallowed error here
* about the thread that ignored it, and one shared slot would have two threads * before releasing its pool context, so this remains a useful debugging aid
* overwriting each other's. Thread local only when AKERR_THREAD_SAFE is 1. * after the pool slot is reused. The snapshot is read-only and is replaced by
* the next ignored error. Thread local only when AKERR_THREAD_SAFE is 1.
*/ */
extern AKERR_THREAD_LOCAL akerr_ErrorContext *__akerr_last_ignored; static AKERR_THREAD_LOCAL akerr_ErrorContext akerr_last_ignored;
/* /*
* Drop one reference, returning NULL once the last one is gone so the caller can * Drop one reference, returning NULL once the last one is gone so the caller can
@@ -391,6 +392,18 @@ akerr_ErrorContext AKERR_NOIGNORE *__akerr_copy_string(char *destination, int ca
* Defines for the ATTEMPT/CATCH/CLEANUP/PROCESS/HANDLE/FINISH process * Defines for the ATTEMPT/CATCH/CLEANUP/PROCESS/HANDLE/FINISH process
*/ */
/*
* HANDLE_GROUP deliberately enters the next case label. GCC and clang both
* understand this spelling in the C modes supported by libakerror. Other
* compilers keep the established control flow without receiving an attribute
* they do not implement.
*/
#if defined(__GNUC__) || defined(__clang__)
#define AKERR_FALLTHROUGH __attribute__((fallthrough));
#else
#define AKERR_FALLTHROUGH
#endif
#define ATTEMPT \ #define ATTEMPT \
switch ( 0 ) { \ switch ( 0 ) { \
case 0: \ case 0: \
@@ -422,10 +435,20 @@ akerr_ErrorContext AKERR_NOIGNORE *__akerr_copy_string(char *destination, int ca
FINISH_LOGIC(__err_context, true); FINISH_LOGIC(__err_context, true);
#define IGNORE(__stmt) \ #define IGNORE(__stmt) \
__akerr_last_ignored = __stmt; \ do { \
if ( __akerr_last_ignored != NULL ) { \ akerr_ErrorContext *__akerr_ignored = __stmt; \
LOG_ERROR_WITH_MESSAGE(__akerr_last_ignored, "** IGNORED ERROR **"); \ if ( __akerr_ignored != NULL ) { \
} memcpy(&akerr_last_ignored, __akerr_ignored, \
sizeof(akerr_last_ignored)); \
akerr_last_ignored.stacktracebufptr = \
(char *)&akerr_last_ignored.stacktracebuf; \
akerr_ErrorContext *__akerr_ignored_snapshot = \
&akerr_last_ignored; \
LOG_ERROR_WITH_MESSAGE(__akerr_ignored_snapshot, \
"** IGNORED ERROR **"); \
RELEASE_ERROR(__akerr_ignored); \
} \
} while ( 0 )
#define CLEANUP \ #define CLEANUP \
}; };
@@ -443,6 +466,7 @@ akerr_ErrorContext AKERR_NOIGNORE *__akerr_copy_string(char *destination, int ca
__err_context->handled = true; __err_context->handled = true;
#define HANDLE_GROUP(__err_context, __err_status) \ #define HANDLE_GROUP(__err_context, __err_status) \
AKERR_FALLTHROUGH \
case __err_status: \ case __err_status: \
__err_context->stacktracebufptr = (char *)&__err_context->stacktracebuf; \ __err_context->stacktracebufptr = (char *)&__err_context->stacktracebuf; \
__err_context->handled = true; __err_context->handled = true;

View File

@@ -20,10 +20,10 @@
* It is not small (an akerr_ErrorContext is tens of kilobytes), but the storage * 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 * 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 * thread-local block, and the alternative is a shared buffer that two threads
* can be writing at once. * can be writing at once. The per-thread IGNORE() snapshot lives in the public
* template header because the macro copies into it at the call site.
*/ */
static AKERR_THREAD_LOCAL akerr_ErrorContext __akerr_last_ditch; static AKERR_THREAD_LOCAL akerr_ErrorContext __akerr_last_ditch;
AKERR_THREAD_LOCAL akerr_ErrorContext *__akerr_last_ignored;
akerr_ErrorUnhandledErrorHandler akerr_handler_unhandled_error; akerr_ErrorUnhandledErrorHandler akerr_handler_unhandled_error;
akerr_ErrorLogFunction akerr_log_method = NULL; akerr_ErrorLogFunction akerr_log_method = NULL;
@@ -233,7 +233,6 @@ static void akerr_init_state(void)
AKERR_ARRAY_ERROR[i].arrayid = i; AKERR_ARRAY_ERROR[i].arrayid = i;
AKERR_ARRAY_ERROR[i].stacktracebufptr = (char *)&AKERR_ARRAY_ERROR[i].stacktracebuf; AKERR_ARRAY_ERROR[i].stacktracebufptr = (char *)&AKERR_ARRAY_ERROR[i].stacktracebuf;
} }
__akerr_last_ignored = NULL;
(void)akerr_last_ditch_context(); (void)akerr_last_ditch_context();
if ( akerr_log_method == NULL ) { if ( akerr_log_method == NULL ) {
akerr_log_method = &akerr_default_logger; akerr_log_method = &akerr_default_logger;

View File

@@ -107,7 +107,7 @@ The remaining survivors are dominated by:
* **Equivalent mutants** in `akerr_init`: deleting the `memset`/`NULL` setup of * **Equivalent mutants** in `akerr_init`: deleting the `memset`/`NULL` setup of
file-scope statics (`AKERR_ARRAY_ERROR`, `__akerr_last_ditch`, file-scope statics (`AKERR_ARRAY_ERROR`, `__akerr_last_ditch`,
`__akerr_last_ignored`) changes nothing, because C already zero-initializes `akerr_last_ignored`) changes nothing, because C already zero-initializes
objects with static storage duration. `int oldid = 0;``1` is likewise objects with static storage duration. `int oldid = 0;``1` is likewise
dead: it is overwritten before use, and so is clearing `akerr_initializing` dead: it is overwritten before use, and so is clearing `akerr_initializing`
at the end of initialization — nothing reads that flag once the once-routine at the end of initialization — nothing reads that flag once the once-routine

View File

@@ -1,11 +1,8 @@
#include "akerror.h" #include "akerror.h"
#include "err_capture.h" #include "err_capture.h"
#include <string.h>
/* /* IGNORE snapshots and logs an error, releases its pool slot, then continues. */
* IGNORE deliberately swallows an error: it records the context in
* __akerr_last_ignored, logs it with an "IGNORED ERROR" marker, and lets
* execution continue.
*/
akerr_ErrorContext *boom(void) akerr_ErrorContext *boom(void)
{ {
@@ -21,11 +18,19 @@ int main(void)
PREPARE_ERROR(e); PREPARE_ERROR(e);
(void)e; (void)e;
IGNORE(boom()); /* More failures than the pool has slots must remain safe: a leaking
* IGNORE used to exhaust the pool and terminate the process here. The
* copied snapshot must also survive the slot being reused on the next
* iteration. */
for ( int i = 0; i < AKERR_MAX_ARRAY_ERROR + 1; i++ ) {
IGNORE(boom());
AKERR_CHECK(akerr_last_ignored.status == AKERR_VALUE);
AKERR_CHECK(strcmp(akerr_last_ignored.message,
"this error is ignored on purpose") == 0);
AKERR_CHECK(akerr_slots_in_use() == 0);
}
reached_after_ignore = 1; reached_after_ignore = 1;
AKERR_CHECK(__akerr_last_ignored != NULL);
AKERR_CHECK(__akerr_last_ignored->status == AKERR_VALUE);
AKERR_CHECK(reached_after_ignore == 1); AKERR_CHECK(reached_after_ignore == 1);
AKERR_CHECK_CONTAINS("IGNORED ERROR"); AKERR_CHECK_CONTAINS("IGNORED ERROR");
AKERR_CHECK_CONTAINS("this error is ignored on purpose"); AKERR_CHECK_CONTAINS("this error is ignored on purpose");

View File

@@ -100,17 +100,21 @@ static void *pool_body(void *raw)
one_checkout(arg); one_checkout(arg);
} }
/* An ignored error is a fact about the thread that ignored it: each thread /* IGNORE's snapshot is thread-local while logging and remains valid after
* must see its own, not the last one any thread swallowed. */ * release. Concurrent ignored errors must all return their pool slots. */
IGNORE(ignorable(arg)); IGNORE(ignorable(arg));
AKERR_TCHECK(arg, __akerr_last_ignored != NULL); AKERR_TCHECK(arg, akerr_last_ignored.status == AKERR_IO);
if ( __akerr_last_ignored != NULL ) { AKERR_TCHECK(arg, strcmp(akerr_last_ignored.message, expected) == 0);
AKERR_TCHECK(arg, __akerr_last_ignored->status == AKERR_IO);
AKERR_TCHECK(arg, strcmp(__akerr_last_ignored->message, expected) == 0); /* Reuse a slot after IGNORE and prove that the copied snapshot did not
* become an alias for the newly acquired context. */
akerr_ErrorContext *reused = akerr_next_error();
AKERR_TCHECK(arg, reused != NULL);
if ( reused != NULL ) {
RELEASE_ERROR(reused);
} }
/* IGNORE keeps the reference by design; hand it back so the pool is empty AKERR_TCHECK(arg, akerr_last_ignored.status == AKERR_IO);
* at the end of the test. */ AKERR_TCHECK(arg, strcmp(akerr_last_ignored.message, expected) == 0);
RELEASE_ERROR(__akerr_last_ignored);
return NULL; return NULL;
} }