6 Commits

Author SHA1 Message Date
0c0d81249f Avoid mutation target collisions when embedded
All checks were successful
libakerror CI Build / cmake_build (push) Successful in 2m44s
libakerror CI Build / mutation_test (push) Successful in 7m38s
2026-07-29 18:01:58 -04:00
426efbb2d4 Add CLAUDE.md
Some checks failed
libakerror CI Build / cmake_build (push) Successful in 2m44s
libakerror CI Build / mutation_test (push) Has been cancelled
2026-07-29 17:42:56 -04:00
4ae1decde2 Expand error status test coverage
All checks were successful
libakerror CI Build / cmake_build (push) Successful in 2m44s
libakerror CI Build / mutation_test (push) Successful in 7m46s
- Added explicit status validation across error handling tests.
- Added lifecycle and slot-leak checks to older tests.
- Improved unhandled-error propagation coverage.
- Added repository guidance and a test runner script.
- Verified all 23 CTest tests pass.

Co-Authored by Codex GPT 5.4
2026-07-29 17:12:52 -04:00
4212ff0b28 Fix format-string use of __FILE__/__func__ and name_for_status lower bound
All checks were successful
libakerror CI Build / cmake_build (push) Successful in 2m48s
libakerror CI Build / mutation_test (push) Successful in 7m40s
Two hardening fixes flagged by the earlier review:

1. FAIL passed __FILE__ and __func__ directly as the snprintf format string.
   __FILE__ expands to a string literal that could contain a '%' (a build path
   under a directory with a percent sign), and __func__ is not a literal at all;
   either way snprintf would read nonexistent varargs. Pass them as "%s"
   arguments instead.

2. akerr_name_for_status guarded the upper bound but not the lower one, so a
   negative status indexed __AKERR_ERROR_NAMES[negative] -- an out-of-bounds
   read, or an out-of-bounds write when a name was supplied. Reject status < 0.

Regression tests err_format_string (uses #line to put a conversion specifier in
__FILE__) and err_name_bounds fail against the old code (verified) and pass now.
Full suite: 23/23, no warnings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-28 10:52:29 -04:00
de13b290d4 Fix refcount leak and stack-trace buffer overflow
Two memory-safety bugs in the macro core:

1. Refcount leak. ENSURE_ERROR_READY incremented refcount on every FAIL/SUCCEED
   rather than only when it acquired a fresh context from the pool. A function
   that FAILed a context more than once and then propagated arrived at its
   caller with refcount 2; the caller released once, leaking the slot. After
   AKERR_MAX_ARRAY_ERROR leaks the pool is exhausted and the library exit(1)s.
   Move the increment inside the acquisition branch.

2. Stack-trace overflow. Each appended frame passed the full buffer length to
   snprintf instead of the space remaining, and advanced the cursor by
   snprintf's would-be return value, so a trace that filled the buffer wrote
   past the end of stacktracebuf and ran the cursor out of bounds. Add
   AKERR_STACKTRACE_APPEND, which bounds the write to the remaining space and
   clamps the cursor advance.

Regression tests err_refcount_double_fail and err_stacktrace_bounds fail against
the old code (verified) and pass now. Full suite: 21/21, no warnings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-28 09:32:55 -04:00
3e24356f07 Document that CATCH/FAIL_*_BREAK must not be used inside a loop
All checks were successful
libakerror CI Build / cmake_build (push) Successful in 2m47s
libakerror CI Build / mutation_test (push) Successful in 6m59s
These macros leave the ATTEMPT block with a C break, which only escapes the
innermost loop/switch. Nesting them in a loop inside an ATTEMPT lets the rest of
the block run with an error already pending. Document the correct patterns:
iterate with PASS / FAIL_*_RETURN (which return, not break), or move the loop
into a helper returning akerr_ErrorContext * and CATCH the single call. Also
note that merely extracting the loop into a function does not fix it if the
helper still wraps the loop in ATTEMPT.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-28 08:49:29 -04:00
24 changed files with 393 additions and 18 deletions

66
AGENTS.md Normal file
View File

@@ -0,0 +1,66 @@
# Repository Guidelines
## Project Structure & Module Organization
This is a small C library built with CMake. Core implementation lives in
`src/error.c`. The public header is generated at build time from
`include/akerror.tmpl.h` by `scripts/generrno.sh`, which also generates
`src/errno.c` under the build directory. CMake package and pkg-config templates
are in `cmake/` and `akerror.pc.in`. Tests are one-file C programs in `tests/`;
shared test helpers live beside them, such as `tests/err_capture.h`.
## Build, Test, and Development Commands
Use an out-of-tree build:
```sh
cmake -S . -B build
cmake --build build
ctest --test-dir build --output-on-failure
```
`cmake -S . -B build` configures the build and generates `akerror.h`.
`cmake --build build` compiles `libakerror` and test executables. `ctest`
runs the registered unit tests. To emit CI-style results, use:
```sh
ctest --test-dir build --output-on-failure --output-junit "$(pwd)/ctest-junit.xml"
```
Mutation testing is available through:
```sh
cmake --build build --target mutation
scripts/mutation_test.py --target src/error.c --threshold 65
```
## Coding Style & Naming Conventions
Use C99-compatible C and follow the surrounding style. Functions and types use
the `akerr_` prefix; macros and constants use `AKERR_` or all-caps macro names
such as `PREPARE_ERROR`. Keep generated-code changes in templates or generator
scripts, not in build outputs. Preserve concise comments for invariants,
macro constraints, and non-obvious error lifecycle behavior.
## Testing Guidelines
Add tests as `tests/err_<behavior>.c`. Register each new test in the
`AKERR_TESTS` list in `CMakeLists.txt`; tests that intentionally abort must
also be listed in `AKERR_WILL_FAIL_TESTS`. Prefer focused executable tests that
return zero on success and use existing helpers such as `AKERR_CHECK`. Run the
CTest suite before submitting changes, and run mutation testing when changing
core control-flow, reference counting, stack-trace, or handler behavior.
## Commit & Pull Request Guidelines
Recent commits use short, imperative, sentence-case subjects, for example
`Fix refcount leak and stack-trace buffer overflow`. Keep commits focused and
describe the observable behavior changed. Pull requests should include a brief
summary, tests run, and any compatibility impact for public macros, generated
headers, installation paths, or CMake/pkg-config consumers.
## Agent-Specific Instructions
Do not overwrite uncommitted user changes. Avoid editing generated files in
`build/`; update `include/akerror.tmpl.h`, `src/error.c`, CMake files, tests,
or scripts instead.

1
CLAUDE.md Normal file
View File

@@ -0,0 +1 @@
See @AGENTS.md

View File

@@ -67,6 +67,10 @@ set(AKERR_TESTS
err_release_clears err_release_clears
err_pool_exhaust err_pool_exhaust
err_maxval err_maxval
err_refcount_double_fail
err_stacktrace_bounds
err_name_bounds
err_format_string
) )
set(AKERR_WILL_FAIL_TESTS set(AKERR_WILL_FAIL_TESTS
@@ -95,9 +99,16 @@ set_tests_properties(
# notices. This is a meta-check on the tests themselves, so it is a manual # notices. This is a meta-check on the tests themselves, so it is a manual
# target (it rebuilds and re-runs the whole suite many times), not a CTest test. # target (it rebuilds and re-runs the whole suite many times), not a CTest test.
# cmake --build build --target mutation # cmake --build build --target mutation
# When embedded in another project, use a namespaced target to avoid collisions
# with mutation targets provided by sibling dependencies.
find_package(Python3 COMPONENTS Interpreter) find_package(Python3 COMPONENTS Interpreter)
if(Python3_FOUND) if(Python3_FOUND)
add_custom_target(mutation if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
set(AKERR_MUTATION_TARGET mutation)
else()
set(AKERR_MUTATION_TARGET akerror_mutation)
endif()
add_custom_target(${AKERR_MUTATION_TARGET}
COMMAND ${Python3_EXECUTABLE} COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/scripts/mutation_test.py ${CMAKE_CURRENT_SOURCE_DIR}/scripts/mutation_test.py
--source-root ${CMAKE_CURRENT_SOURCE_DIR} --source-root ${CMAKE_CURRENT_SOURCE_DIR}

View File

@@ -210,6 +210,8 @@ ATTEMPT {
This will assign the return value of the function in question to the akerr_ErrorContext previously prepared in the current scope. If the function returns an akerr_ErrorContext that indicates any type of error, the `ATTEMPT` block is immediately exited, and the `CLEANUP` block begins. This will assign the return value of the function in question to the akerr_ErrorContext previously prepared in the current scope. If the function returns an akerr_ErrorContext that indicates any type of error, the `ATTEMPT` block is immediately exited, and the `CLEANUP` block begins.
(One caveat: because this exit is implemented with a C `break`, `CATCH` must not be used inside a loop within the `ATTEMPT` block — see the section "Important: do not use CATCH or FAIL_*_BREAK inside a loop" below.)
## Setting errors from functions or expressions returning integer ## Setting errors from functions or expressions returning integer
For functions that return integer, such as logical comparisons or most standard library functions, use the `FAIL_ZERO_BREAK` and `FAIL_NONZERO_BREAK` macros. These macros allow you to capture an integer return code from an expression or function and set an error code in the current context based off that return. For functions that return integer, such as logical comparisons or most standard library functions, use the `FAIL_ZERO_BREAK` and `FAIL_NONZERO_BREAK` macros. These macros allow you to capture an integer return code from an expression or function and set an error code in the current context based off that return.
@@ -232,6 +234,49 @@ ATTEMPT {
When either of these two macros are used, the `ATTEMPT` block is immediately exited, and the `CLEANUP` block begins. When either of these two macros are used, the `ATTEMPT` block is immediately exited, and the `CLEANUP` block begins.
## Important: do not use CATCH or FAIL_*_BREAK inside a loop
`CATCH`, `FAIL_ZERO_BREAK`, `FAIL_NONZERO_BREAK`, and `FAIL_BREAK` leave the `ATTEMPT` block by executing a C `break` statement. In C, `break` only exits the *innermost* enclosing `for`, `while`, `do`, or `switch`. Therefore **these macros must not be used inside a loop (or a nested `switch`) that is itself inside an `ATTEMPT` block.** If you do, the `break` escapes only the loop — not the `ATTEMPT` — and the rest of the `ATTEMPT` body then runs with an error already pending:
```c
ATTEMPT {
for ( int i = 0; i < n; i++ ) {
CATCH(errctx, process(items[i])); // WRONG: break exits the for loop, not the ATTEMPT
}
// ... this code still executes, with errctx already in an error state ...
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
```
Note that moving the loop into a helper function does **not** fix this on its own — if the helper still wraps the loop in an `ATTEMPT` and uses `CATCH`/`FAIL_*_BREAK` inside it, it has the exact same problem. The fix is to iterate with `return`-based macros, which are unaffected by loop nesting. Use one of the two patterns below.
**Pattern 1 — use `PASS` (or a `FAIL_*_RETURN` macro) inside the loop.** These exit the *enclosing function* with a `return` rather than a `break`, so loop nesting is irrelevant. Use this when the loop should stop and propagate on the first error:
```c
akerr_ErrorContext AKERR_NOIGNORE *process_all(Item *items, int n)
{
PREPARE_ERROR(errctx);
for ( int i = 0; i < n; i++ ) {
PASS(errctx, process(items[i])); // returns from process_all on the first error
}
SUCCEED_RETURN(errctx);
}
```
**Pattern 2 — move the loop into a helper and `CATCH` the single call.** When you need a `CLEANUP` block or want to `HANDLE` the error locally, put the loop in its own `akerr_ErrorContext *`-returning function (written per Pattern 1) and `CATCH` that one call. The `CATCH` is then not inside a loop, so its `break` scopes to the `ATTEMPT` correctly:
```c
ATTEMPT {
CATCH(errctx, process_all(items, n)); // a single CATCH, not looped
} CLEANUP {
// ... always runs ...
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_VALUE) {
// ... handle a failure from any iteration ...
} FINISH(errctx, true);
```
# Passing errors # Passing errors
Sometimes you can't actually do anything about the errors that come out of a given method, but you want that error to be propagated back up the call chain, and to be properly reported. If this is your goal, you can avoid using a `ATTEMPT ... FINISH` block, and simply use the `PASS` macro. Sometimes you can't actually do anything about the errors that come out of a given method, but you want that error to be propagated back up the call chain, and to be properly reported. If this is your goal, you can avoid using a `ATTEMPT ... FINISH` block, and simply use the `PASS` macro.

View File

@@ -110,8 +110,30 @@ void akerr_init_errno(void);
akerr_log_method("%s:%s:%d: Unable to pull an error context from the array!", __FILE__, (char *)__func__, __LINE__); \ akerr_log_method("%s:%s:%d: Unable to pull an error context from the array!", __FILE__, (char *)__func__, __LINE__); \
exit(1); \ exit(1); \
} \ } \
__err_context->refcount += 1; \
}
/*
* Append a formatted line to the error's stack-trace buffer, bounded by the
* space that remains so a deep propagation chain cannot write past the end of
* stacktracebuf. snprintf reports the length it *would* have written, which on
* truncation exceeds what it actually wrote, so the cursor advance is clamped
* to the remaining space.
*/
#define AKERR_STACKTRACE_APPEND(__err_context, ...) \
do { \
char *__akerr_stb = (char *)__err_context->stacktracebuf; \
size_t __akerr_used = (size_t)(__err_context->stacktracebufptr - __akerr_stb); \
if ( __akerr_used < AKERR_MAX_ERROR_STACKTRACE_BUF_LENGTH ) { \
size_t __akerr_rem = AKERR_MAX_ERROR_STACKTRACE_BUF_LENGTH - __akerr_used; \
int __akerr_n = snprintf(__err_context->stacktracebufptr, __akerr_rem, __VA_ARGS__); \
if ( __akerr_n < 0 ) { \
__akerr_n = 0; \
} \ } \
__err_context->refcount += 1; __err_context->stacktracebufptr += ((size_t)__akerr_n < __akerr_rem) \
? (size_t)__akerr_n : (__akerr_rem - 1); \
} \
} while ( 0 )
/* /*
* Failure and success methods for functions that return akerr_ErrorContext * * Failure and success methods for functions that return akerr_ErrorContext *
@@ -168,11 +190,11 @@ void akerr_init_errno(void);
#define FAIL(__err_context, __err, __message, ...) \ #define FAIL(__err_context, __err, __message, ...) \
ENSURE_ERROR_READY(__err_context); \ ENSURE_ERROR_READY(__err_context); \
__err_context->status = __err; \ __err_context->status = __err; \
snprintf((char *)__err_context->fname, AKERR_MAX_ERROR_FNAME_LENGTH, __FILE__); \ snprintf((char *)__err_context->fname, AKERR_MAX_ERROR_FNAME_LENGTH, "%s", __FILE__); \
snprintf((char *)__err_context->function, AKERR_MAX_ERROR_FUNCTION_LENGTH, __func__); \ snprintf((char *)__err_context->function, AKERR_MAX_ERROR_FUNCTION_LENGTH, "%s", __func__); \
__err_context->lineno = __LINE__; \ __err_context->lineno = __LINE__; \
snprintf((char *)__err_context->message, AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH, __message, ## __VA_ARGS__); \ snprintf((char *)__err_context->message, AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH, __message, ## __VA_ARGS__); \
__err_context->stacktracebufptr += snprintf(__err_context->stacktracebufptr, AKERR_MAX_ERROR_STACKTRACE_BUF_LENGTH, "%s:%s:%d: %d (%s) : %s\n", (char *)__err_context->fname, (char *)__err_context->function, __err_context->lineno, __err_context->status, akerr_name_for_status(__err_context->status, NULL), (__err_context->message == NULL ? "" : __err_context->message)); AKERR_STACKTRACE_APPEND(__err_context, "%s:%s:%d: %d (%s) : %s\n", (char *)__err_context->fname, (char *)__err_context->function, __err_context->lineno, __err_context->status, akerr_name_for_status(__err_context->status, NULL), (__err_context->message == NULL ? "" : __err_context->message));
#define SUCCEED(__err_context) \ #define SUCCEED(__err_context) \
@@ -198,7 +220,7 @@ void akerr_init_errno(void);
VALID(__err_context, __stmt); \ VALID(__err_context, __stmt); \
if ( __err_context != NULL ) { \ if ( __err_context != NULL ) { \
if ( __err_context->status != 0 ) { \ if ( __err_context->status != 0 ) { \
__err_context->stacktracebufptr += snprintf(__err_context->stacktracebufptr, AKERR_MAX_ERROR_STACKTRACE_BUF_LENGTH, "%s:%s:%d\n", (char *)__FILE__, (char *)__func__, __LINE__); \ AKERR_STACKTRACE_APPEND(__err_context, "%s:%s:%d\n", (char *)__FILE__, (char *)__func__, __LINE__); \
break; \ break; \
} \ } \
} }

View File

@@ -118,7 +118,7 @@ akerr_ErrorContext *akerr_release_error(akerr_ErrorContext *err)
// Call with name = NULL to retrieve a status. // Call with name = NULL to retrieve a status.
char *akerr_name_for_status(int status, char *name) char *akerr_name_for_status(int status, char *name)
{ {
if ( status > AKERR_MAX_ERR_VALUE ) { if ( status < 0 || status > AKERR_MAX_ERR_VALUE ) {
return "Unknown Error"; return "Unknown Error";
} }
if ( name != NULL ) { if ( name != NULL ) {

4
test.sh Normal file
View File

@@ -0,0 +1,4 @@
cmake -S . -B build
cmake --build build
ctest --test-dir build --output-on-failure --output-junit "$(pwd)/ctest-junit.xml"
python3 scripts/mutation_test.py --target src/error.c --junit mutation-junit.xml --threshold 65

View File

@@ -63,26 +63,26 @@ int main(void)
akerr_ErrorContext *r; akerr_ErrorContext *r;
r = zero_break(0); r = zero_break(0);
AKERR_CHECK(r != NULL && r->status == AKERR_VALUE); AKERR_CHECK_STATUS(r, AKERR_VALUE);
r = akerr_release_error(r); r = akerr_release_error(r);
AKERR_CHECK(zero_break(7) == NULL); AKERR_CHECK(zero_break(7) == NULL);
r = nonzero_break(7); r = nonzero_break(7);
AKERR_CHECK(r != NULL && r->status == AKERR_INDEX); AKERR_CHECK_STATUS(r, AKERR_INDEX);
r = akerr_release_error(r); r = akerr_release_error(r);
AKERR_CHECK(nonzero_break(0) == NULL); AKERR_CHECK(nonzero_break(0) == NULL);
r = always_break(); r = always_break();
AKERR_CHECK(r != NULL && r->status == AKERR_IO); AKERR_CHECK_STATUS(r, AKERR_IO);
r = akerr_release_error(r); r = akerr_release_error(r);
r = zero_return(0); r = zero_return(0);
AKERR_CHECK(r != NULL && r->status == AKERR_KEY); AKERR_CHECK_STATUS(r, AKERR_KEY);
r = akerr_release_error(r); r = akerr_release_error(r);
AKERR_CHECK(zero_return(7) == NULL); AKERR_CHECK(zero_return(7) == NULL);
r = nonzero_return(7); r = nonzero_return(7);
AKERR_CHECK(r != NULL && r->status == AKERR_TYPE); AKERR_CHECK_STATUS(r, AKERR_TYPE);
r = akerr_release_error(r); r = akerr_release_error(r);
AKERR_CHECK(nonzero_return(0) == NULL); AKERR_CHECK(nonzero_return(0) == NULL);

View File

@@ -75,6 +75,12 @@ static int __attribute__((unused)) akerr_slots_in_use(void)
} \ } \
} while ( 0 ) } while ( 0 )
#define AKERR_CHECK_STATUS(errctx, expected_status) \
do { \
AKERR_CHECK((errctx) != NULL); \
AKERR_CHECK((errctx)->status == (expected_status)); \
} while ( 0 )
#define AKERR_CHECK_CONTAINS(needle) \ #define AKERR_CHECK_CONTAINS(needle) \
AKERR_CHECK(strstr(akerr_capture_buf, (needle)) != NULL) AKERR_CHECK(strstr(akerr_capture_buf, (needle)) != NULL)

View File

@@ -1,4 +1,5 @@
#include "akerror.h" #include "akerror.h"
#include "err_capture.h"
akerr_ErrorContext *func2(void) akerr_ErrorContext *func2(void)
{ {
@@ -31,6 +32,11 @@ int main(void)
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) { } HANDLE(errctx, AKERR_NULLPOINTER) {
AKERR_CHECK_STATUS(errctx, AKERR_NULLPOINTER);
akerr_log_method("Caught exception"); akerr_log_method("Caught exception");
} FINISH_NORETURN(errctx); } FINISH_NORETURN(errctx);
AKERR_CHECK(akerr_slots_in_use() == 0);
fprintf(stderr, "err_catch ok\n");
return 0;
} }

View File

@@ -1,4 +1,5 @@
#include "akerror.h" #include "akerror.h"
#include "err_capture.h"
int x; int x;
@@ -34,10 +35,15 @@ int main(void)
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) { } HANDLE(errctx, AKERR_NULLPOINTER) {
AKERR_CHECK_STATUS(errctx, AKERR_NULLPOINTER);
if ( x == 0 ) { if ( x == 0 ) {
fprintf(stderr, "Cleanup works\n"); akerr_log_method("Cleanup works\n");
return 0; } else {
}
return 1; return 1;
}
} FINISH_NORETURN(errctx); } FINISH_NORETURN(errctx);
AKERR_CHECK(akerr_slots_in_use() == 0);
fprintf(stderr, "err_cleanup ok\n");
return 0;
} }

View File

@@ -37,6 +37,7 @@ int main(void)
} CLEANUP { } CLEANUP {
} PROCESS(e) { } PROCESS(e) {
} HANDLE(e, EACCES) { } HANDLE(e, EACCES) {
AKERR_CHECK_STATUS(e, EACCES);
handled = 1; handled = 1;
} FINISH_NORETURN(e); } FINISH_NORETURN(e);

34
tests/err_format_string.c Normal file
View File

@@ -0,0 +1,34 @@
#include "akerror.h"
#include "err_capture.h"
#include <string.h>
/*
* FAIL records the source file and function names with snprintf. Those names
* must be passed as %s ARGUMENTS, not used as the format string -- otherwise a
* path containing a printf conversion (say a build directory with a '%') is
* interpreted as a format and reads nonexistent varargs (undefined behavior).
*
* #line lets us make __FILE__ contain a conversion specifier; the stored name
* must come back verbatim.
*/
#line 1 "pct%dname.c"
akerr_ErrorContext *raise_with_percent_in_filename(void)
{
PREPARE_ERROR(e);
FAIL_RETURN(e, AKERR_VALUE, "boom");
}
#line 22 "tests/err_format_string.c"
int main(void)
{
akerr_init();
akerr_ErrorContext *e = raise_with_percent_in_filename();
AKERR_CHECK(e != NULL);
AKERR_CHECK(strcmp(e->fname, "pct%dname.c") == 0);
e = akerr_release_error(e);
fprintf(stderr, "err_format_string ok\n");
return 0;
}

View File

@@ -8,6 +8,7 @@
static int specific_fired = 0; static int specific_fired = 0;
static int default_fired = 0; static int default_fired = 0;
static int default_status = 0;
akerr_ErrorContext *boom(void) akerr_ErrorContext *boom(void)
{ {
@@ -28,10 +29,12 @@ int main(void)
specific_fired = 1; /* must NOT run: error is AKERR_TYPE */ specific_fired = 1; /* must NOT run: error is AKERR_TYPE */
} HANDLE_DEFAULT(e) { } HANDLE_DEFAULT(e) {
default_fired = 1; default_fired = 1;
default_status = e->status;
} FINISH_NORETURN(e); } FINISH_NORETURN(e);
AKERR_CHECK(specific_fired == 0); AKERR_CHECK(specific_fired == 0);
AKERR_CHECK(default_fired == 1); AKERR_CHECK(default_fired == 1);
AKERR_CHECK(default_status == AKERR_TYPE);
AKERR_CHECK(akerr_slots_in_use() == 0); AKERR_CHECK(akerr_slots_in_use() == 0);
fprintf(stderr, "err_handle_default ok\n"); fprintf(stderr, "err_handle_default ok\n");
return 0; return 0;

View File

@@ -9,6 +9,7 @@
static int a_fired = 0; static int a_fired = 0;
static int b_fired = 0; static int b_fired = 0;
static int c_fired = 0; static int c_fired = 0;
static int b_status = 0;
akerr_ErrorContext *boom(void) akerr_ErrorContext *boom(void)
{ {
@@ -29,12 +30,14 @@ int main(void)
a_fired = 1; a_fired = 1;
} HANDLE(e, AKERR_TYPE) { } HANDLE(e, AKERR_TYPE) {
b_fired = 1; b_fired = 1;
b_status = e->status;
} HANDLE(e, AKERR_IO) { } HANDLE(e, AKERR_IO) {
c_fired = 1; c_fired = 1;
} FINISH_NORETURN(e); } FINISH_NORETURN(e);
AKERR_CHECK(a_fired == 0); AKERR_CHECK(a_fired == 0);
AKERR_CHECK(b_fired == 1); AKERR_CHECK(b_fired == 1);
AKERR_CHECK(b_status == AKERR_TYPE);
AKERR_CHECK(c_fired == 0); AKERR_CHECK(c_fired == 0);
AKERR_CHECK(akerr_slots_in_use() == 0); AKERR_CHECK(akerr_slots_in_use() == 0);
fprintf(stderr, "err_handle_dispatch ok\n"); fprintf(stderr, "err_handle_dispatch ok\n");

View File

@@ -11,6 +11,8 @@
static int group_fired = 0; static int group_fired = 0;
static int other_fired = 0; static int other_fired = 0;
static int group_status = 0;
static int other_status = 0;
akerr_ErrorContext *boom(int status) akerr_ErrorContext *boom(int status)
{ {
@@ -28,8 +30,10 @@ akerr_ErrorContext *run(int status)
} HANDLE(e, AKERR_KEY) } HANDLE(e, AKERR_KEY)
HANDLE_GROUP(e, AKERR_INDEX) { HANDLE_GROUP(e, AKERR_INDEX) {
group_fired++; group_fired++;
group_status = e->status;
} HANDLE(e, AKERR_IO) { } HANDLE(e, AKERR_IO) {
other_fired++; other_fired++;
other_status = e->status;
} FINISH(e, false); } FINISH(e, false);
return e; return e;
} }
@@ -40,15 +44,18 @@ int main(void)
run(AKERR_KEY); run(AKERR_KEY);
AKERR_CHECK(group_fired == 1); AKERR_CHECK(group_fired == 1);
AKERR_CHECK(group_status == AKERR_KEY);
AKERR_CHECK(other_fired == 0); AKERR_CHECK(other_fired == 0);
run(AKERR_INDEX); run(AKERR_INDEX);
AKERR_CHECK(group_fired == 2); AKERR_CHECK(group_fired == 2);
AKERR_CHECK(group_status == AKERR_INDEX);
AKERR_CHECK(other_fired == 0); AKERR_CHECK(other_fired == 0);
run(AKERR_IO); run(AKERR_IO);
AKERR_CHECK(group_fired == 2); AKERR_CHECK(group_fired == 2);
AKERR_CHECK(other_fired == 1); AKERR_CHECK(other_fired == 1);
AKERR_CHECK(other_status == AKERR_IO);
AKERR_CHECK(akerr_slots_in_use() == 0); AKERR_CHECK(akerr_slots_in_use() == 0);
fprintf(stderr, "err_handle_group ok\n"); fprintf(stderr, "err_handle_group ok\n");

31
tests/err_name_bounds.c Normal file
View File

@@ -0,0 +1,31 @@
#include "akerror.h"
#include "err_capture.h"
#include <string.h>
/*
* akerr_name_for_status must reject out-of-range status codes at BOTH ends.
* It already guarded the upper bound, but a negative status indexed
* __AKERR_ERROR_NAMES[negative] -- an out-of-bounds read (and an out-of-bounds
* write when a name is supplied). Every out-of-range status must return the
* "Unknown Error" sentinel instead.
*/
int main(void)
{
akerr_init();
/* Below range. */
AKERR_CHECK(strcmp(akerr_name_for_status(-1, NULL), "Unknown Error") == 0);
AKERR_CHECK(strcmp(akerr_name_for_status(-9999, NULL), "Unknown Error") == 0);
/* Above range (already handled; kept as a guard against regressions). */
AKERR_CHECK(strcmp(akerr_name_for_status(AKERR_MAX_ERR_VALUE + 1, NULL),
"Unknown Error") == 0);
/* A valid code must still resolve to its real name. */
AKERR_CHECK(strcmp(akerr_name_for_status(AKERR_NULLPOINTER, NULL),
"Null Pointer Error") == 0);
fprintf(stderr, "err_name_bounds ok\n");
return 0;
}

View File

@@ -35,6 +35,7 @@ int main(void)
} CLEANUP { } CLEANUP {
} PROCESS(e) { } PROCESS(e) {
} HANDLE(e, AKERR_IO) { } HANDLE(e, AKERR_IO) {
AKERR_CHECK_STATUS(e, AKERR_IO);
handled = 1; handled = 1;
} FINISH_NORETURN(e); } FINISH_NORETURN(e);

View File

@@ -11,6 +11,8 @@
#define ITERATIONS 100000 #define ITERATIONS 100000
static int handled_status = 0;
akerr_ErrorContext *boom(void) akerr_ErrorContext *boom(void)
{ {
PREPARE_ERROR(e); PREPARE_ERROR(e);
@@ -31,6 +33,7 @@ akerr_ErrorContext *one_cycle(void)
} CLEANUP { } CLEANUP {
} PROCESS(e) { } PROCESS(e) {
} HANDLE(e, AKERR_VALUE) { } HANDLE(e, AKERR_VALUE) {
handled_status = e->status;
} FINISH(e, false); } FINISH(e, false);
return e; return e;
} }
@@ -42,7 +45,9 @@ int main(void)
AKERR_CHECK(akerr_slots_in_use() == 0); AKERR_CHECK(akerr_slots_in_use() == 0);
for ( int iter = 0; iter < ITERATIONS; iter++ ) { for ( int iter = 0; iter < ITERATIONS; iter++ ) {
handled_status = 0;
(void)one_cycle(); (void)one_cycle();
AKERR_CHECK(handled_status == AKERR_VALUE);
} }
AKERR_CHECK(akerr_slots_in_use() == 0); AKERR_CHECK(akerr_slots_in_use() == 0);

View File

@@ -0,0 +1,56 @@
#include "akerror.h"
#include "err_capture.h"
/*
* Regression test for the refcount leak: ENSURE_ERROR_READY must increment
* refcount only when it *acquires* a fresh context, not on every FAIL/SUCCEED.
* A function that calls FAIL more than once on the same context and then
* propagates used to arrive at the caller with refcount 2; the caller released
* once, leaking the slot. After enough leaks the pool is exhausted and the
* library exit(1)s.
*/
static int handled_status = 0;
akerr_ErrorContext *validate(void)
{
PREPARE_ERROR(e);
ATTEMPT {
FAIL(e, AKERR_VALUE, "condition 1 failed"); /* acquires the context */
FAIL(e, AKERR_KEY, "condition 2 failed"); /* must NOT re-acquire it */
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true); /* unhandled -> propagate */
SUCCEED_RETURN(e);
}
/* One raise -> catch -> handle cycle; returns NULL (context released). */
akerr_ErrorContext *one_cycle(void)
{
PREPARE_ERROR(e);
ATTEMPT {
CATCH(e, validate());
} CLEANUP {
} PROCESS(e) {
} HANDLE(e, AKERR_KEY) {
handled_status = e->status;
} HANDLE(e, AKERR_VALUE) {
} FINISH(e, false);
return e;
}
int main(void)
{
akerr_init();
AKERR_CHECK(akerr_slots_in_use() == 0);
for ( int i = 0; i < 32; i++ ) {
handled_status = 0;
(void)one_cycle();
AKERR_CHECK(handled_status == AKERR_KEY);
}
AKERR_CHECK(akerr_slots_in_use() == 0);
fprintf(stderr, "err_refcount_double_fail ok\n");
return 0;
}

View File

@@ -27,6 +27,7 @@ int main(void)
} CLEANUP { } CLEANUP {
} PROCESS(e) { } PROCESS(e) {
} HANDLE(e, AKERR_VALUE) { } HANDLE(e, AKERR_VALUE) {
AKERR_CHECK_STATUS(e, AKERR_VALUE);
} FINISH_NORETURN(e); } FINISH_NORETURN(e);
AKERR_CHECK(e == NULL); AKERR_CHECK(e == NULL);

View File

@@ -0,0 +1,54 @@
#include "akerror.h"
#include "err_capture.h"
#include <string.h>
/*
* Regression test for the stack-trace buffer overflow. Each frame appended a
* line with snprintf, but passed the *full* buffer length as the size rather
* than the space remaining, and advanced the cursor by snprintf's would-be
* return value. A trace that filled the buffer therefore wrote past the end of
* stacktracebuf and ran the cursor out of bounds.
*
* We place a context in a struct with a guard region right after it, position
* the trace cursor near the end of the buffer, append one more frame, and
* require that nothing was written past the buffer and the cursor stayed in
* bounds.
*/
static struct {
akerr_ErrorContext ctx;
unsigned char guard[512];
} probe;
akerr_ErrorContext *append_frame(akerr_ErrorContext *e)
{
FAIL_RETURN(e, AKERR_VALUE,
"an error message long enough to overflow a nearly full stack trace buffer");
}
int main(void)
{
akerr_init();
memset(&probe, 0x00, sizeof(probe));
memset(probe.guard, 0xAA, sizeof(probe.guard));
akerr_ErrorContext *e = &probe.ctx;
e->refcount = 1;
/* Two bytes short of full: any real frame would overflow the old code. */
e->stacktracebufptr = probe.ctx.stacktracebuf
+ AKERR_MAX_ERROR_STACKTRACE_BUF_LENGTH - 2;
(void)append_frame(e);
/* Nothing may have been written past the end of stacktracebuf. */
for ( unsigned i = 0; i < sizeof(probe.guard); i++ ) {
AKERR_CHECK(probe.guard[i] == 0xAA);
}
/* The cursor must remain within the buffer. */
AKERR_CHECK(e->stacktracebufptr
<= probe.ctx.stacktracebuf + AKERR_MAX_ERROR_STACKTRACE_BUF_LENGTH);
fprintf(stderr, "err_stacktrace_bounds ok\n");
return 0;
}

View File

@@ -9,6 +9,7 @@
*/ */
static int wrong_handler_fired = 0; static int wrong_handler_fired = 0;
static int swallowed_status = 0;
akerr_ErrorContext *boom(void) akerr_ErrorContext *boom(void)
{ {
@@ -24,6 +25,7 @@ akerr_ErrorContext *swallow_it(void)
ATTEMPT { ATTEMPT {
CATCH(e, boom()); CATCH(e, boom());
} CLEANUP { } CLEANUP {
swallowed_status = (e != NULL) ? e->status : 0;
} PROCESS(e) { } PROCESS(e) {
} HANDLE(e, AKERR_KEY) { } HANDLE(e, AKERR_KEY) {
wrong_handler_fired = 1; /* does not match AKERR_VALUE */ wrong_handler_fired = 1; /* does not match AKERR_VALUE */
@@ -38,6 +40,7 @@ int main(void)
akerr_ErrorContext *res = swallow_it(); akerr_ErrorContext *res = swallow_it();
AKERR_CHECK(wrong_handler_fired == 0); AKERR_CHECK(wrong_handler_fired == 0);
AKERR_CHECK(swallowed_status == AKERR_VALUE);
AKERR_CHECK(res == NULL); /* released even though unhandled */ AKERR_CHECK(res == NULL); /* released even though unhandled */
AKERR_CHECK(akerr_slots_in_use() == 0); AKERR_CHECK(akerr_slots_in_use() == 0);
fprintf(stderr, "err_swallow ok\n"); fprintf(stderr, "err_swallow ok\n");

View File

@@ -1,4 +1,10 @@
#include "akerror.h" #include "akerror.h"
#include <stdlib.h>
static void expect_unhandled_nullpointer(akerr_ErrorContext *errctx)
{
exit((errctx != NULL && errctx->status == AKERR_NULLPOINTER) ? 1 : 0);
}
akerr_ErrorContext *func2(void) akerr_ErrorContext *func2(void)
{ {
@@ -25,6 +31,9 @@ akerr_ErrorContext *func1(void)
int main(void) int main(void)
{ {
akerr_init();
akerr_handler_unhandled_error = &expect_unhandled_nullpointer;
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
CATCH(errctx, func1()); CATCH(errctx, func1());