31 KiB
libvec bug tracker
Pass 11 — verified 2026-10-09 — char.c @ 936 lines · char.h @ 108
| Open | S1 | S2 | S3 | C | DECIDE | Fixed |
|---|---|---|---|---|---|---|
| 49 | 4 | 16 | 7 | 22 | 0 | 58 |
S1 memory safety · S2 wrong result / broken contract · S3 edge case · C cleanup · DECIDE open design question
Line numbers are the only field that drifts. Re-verify before trusting them.
Contents
- Triage — the 4 open S1s
- Findings
- Core storage — push_back/pop_back · cpy · reserve · shrink_to_fit · swap · delete · clear/print
- Validation & accessors — __null_check · capacity/max_size/data · at · begin/end · front/back/empty
- Algorithms — erase · resize · assign · insert
- Support — utils · char.h · project
- Fixed
- Conventions
Triage — the 4 open S1s
In suggested fixing order.
| # | ID | Location | Summary |
|---|---|---|---|
| 1 | RS-1 |
char.c:236, 243-249 | truncation is zeroed through the post-realloc pointer: memset(&arr[capacity], 0, size - capacity) (246) writes past the shrunk block → heap-buffer-overflow. ASan Pass 11: WRITE at char.c:246 for push 4 + reserve(&v, 2) |
| 2 | FB-1 |
char.c:423-428, 441-446 | front/back guard inverted: valid vectors return 0 + ERR_OUT_OF_BOUNDS, drained vectors fall through to *at() nullptr deref → SEGV at 428. back probes index 0 (441) |
| 3 | IN-10 |
char.c:865-872 | amount-insert never validates: guard at 866 reads a fresh local (always 0), NULL ptr derefs at 872 → SEGV |
| 4 | DL-2 |
char.h:11-17 | no ownership marker, by-value struct copy double-frees |
Reading notes
Blank-state shapes (PB-9). Three blank-materialize paths, all in-bounds since Pass 11:
push_backmalloc(1)→capacity=1, size=0, then WRITEarr[0](140-152).reservecalloc(capacity)→capacity=capacity(221-232).insertone-argmalloc(1)→capacity=1, size=1, WRITEarr[0](818-832). Same input state (blank vector), three allocations, three struct shapes;insertandresizealso disagree withpush_backon which blank states are accepted.
max_sizeis no longer capped byreserve. The oldptr->_internal_max_size = capacity;line is gone entirely — it was commented out at 249 in Pass 10 and deleted in Pass 11. PB-21, ST-1, AS-4 and RS-3 stay retired.
Sanitizer traces. Rows with ASan output from Pass 6-11: FB-1 (SEGV on 0x0 at char.c:428), IN-10 (SEGV on 0x8 at char.c:872), RS-1 (heap-buffer-overflow WRITE at char.c:246), DL-2 (double-free — struct copy, two deletes). All reproducers are under 25 lines.
Defensive-only rows. RZ-2 and PR-8 are not reachable without an allocation of ~2^61 bytes, a hand-built struct, or a NULL
err. Kept as hardening because the guards are cheap and the invariant is worth stating. (CR-11 and RS-2 were in this class and are now fixed.)
Documented-by-contract (Pass 10).
char.cgained a DESIGN CONTRACT block (11-32): raw byte container, buffers NEVER NUL-terminated,errmust be non-NULL, slots pastsizeare uninitialized. That resolved PB-16, PB-20, CR-4 and CH-7 — the rows did not lie, the contract now says the behavior is intended.
Findings
Core storage
_vec_char_push_back / _vec_char_pop_back — char.c:120, 181
1 S2
PB-17, PB-14 and PB-15 left this block in Pass 10 and were re-verified in Pass 11 (harness: drained pop → flags=0, size stays 0 — origin POP_BACK is set without any flag, 190-194;
delete; push_back→ err=0, size=1, arr[0]='z'; hand-built{NULL, 5, …}push → size 1, no overflow). PB-16/20 retired as documented-by-contract; PB-21 retired with the removedmax_sizeline.
| ID | Sev | Location | Finding |
|---|---|---|---|
PB-9 |
S2 | char.c:140-152 vs 221-232 vs 818-832 | three blank-materialize paths, no shared shape. push_back: malloc(1), capacity=1, size=0, writes arr[0]. reserve: calloc(capacity) and commits capacity (correct since Pass 11, RS-7). insert one-arg: malloc(1), capacity=1, size=1, writes arr[0] (in-bounds since Pass 11, IN-11). Same input state, three different allocations, three different struct results. insert and resize also disagree with push_back on which blank states are accepted |
_vec_char_cpy — char.c:82
none open
CR-4 retired in Pass 10 (the DESIGN CONTRACT answers the byte-range vs C-string question, char.c:11-16). CR-10 and CR-11 retired in Pass 11:
mallocfailure now reportsERR_ALLOC_FAIL(108-112), notERR_BUF_NULL, andsrc_sizeis guarded against_internal_max_size(101-106).
_vec_char_reserve — char.c:200
1 S1
RS-8 retired in Pass 7, RS-3 in Pass 10 (no more
max_sizecapping). Three retired in Pass 11: RS-7 (the blank path is nowcalloc(capacity)at 223 and commits exactly the requested capacity — harness:reserve(VEC_INIT, 5)→ cap=5, err=0), RS-4 (truncation ismemset, 246-247) and RS-2 (guard restructured, 205-219 — a NULL vector now reportsERR_VEC_NULLbefore thecapacity == 0check). RS-1 remains open and got worse — the fix moved the assignment before the truncation and turned a use-after-free into a heap-buffer-overflow.
| ID | Sev | Location | Finding |
|---|---|---|---|
RS-1 |
S1 | char.c:236, 243-249 | ptr->_internal_arr = res (243) runs before the truncation, so the discarded tail is zeroed through the NEW pointer — but realloc already shrunk the block to capacity bytes, and memset(&ptr->_internal_arr[capacity], 0, ptr->_internal_size - capacity) (246-247) writes past it. ASan Pass 11: heap-buffer-overflow WRITE at char.c:246 — push 4, reserve(&v, 2) (block is 2 bytes, WRITE lands 0 bytes after). The tail to zero no longer exists after a shrinking realloc: the memset must run on the old buffer before realloc, or be dropped — spare slots are uninitialized by contract |
_vec_char_shrink_to_fit — char.c:256
none open
ST-1 retired in Pass 10. ST-2 retired in Pass 11: the drained no-op landed (267-270) before the forward to
reserve; harness: push 2, pop 2, shrink → err=0, size=0 (capacity keeps its old value — shrink of an empty vector is a successful no-op). Blank vectors still take theERR_BUF_NULLpath at 261-266 — pre-existing, intended. The double__null_checkat 260-261 is redundant but harmless.
_vec_char_swap — char.c:274
none open
SW-2 retired in Pass 7. SW-1 retired in Pass 11: the guard now checks
ERR_VEC_NULLonly (278-284) and the exchange is two struct assignments (285-287). Harness:swap(blank, {x,y})→ err=0, operands exchanged (blank side now owns the buffer).
_vec_char_delete — char.c:330
1 S1
DL-1 retired in Pass 10 (resets
_internal_arr = nullptr, so double-delete and post-deletepush_backare safe). DL-3 retired in Pass 11: the(void*)cast on thefreeargument is gone —free(ptr->_internal_arr)(342). The by-value-copy hazard is unchanged (DL-2).
| ID | Sev | Location | Finding |
|---|---|---|---|
DL-2 |
S1 | char.h:11-17 | no ownership marker on the struct, so a by-value copy yields two owners of one buffer; each delete clears only its own struct's pointer, so the second free hits the first's freed block. ASan Pass 10: attempting double-free — w = v; delete(&v); delete(&w) |
_vec_char_clear / _vec_char_print_vec — char.c:349, 73
none open
CL-1 retired in Pass 9:
clearusesmemset(361). CL-2 retired in Pass 7:print_vecon a drained vector prints the newline with err untouched; blank vectors take the intendedERR_BUF_NULLpath.
Validation & accessors
IV-3 and IV-4 retired in Pass 7:
__null_checkno longer ORs informationalERR_SIZE_ZEROinto the validity mask (onlyERR_VEC_NULLandERR_BUF_NULLremain), and the unused localresis gone. That single change is what un-broke accessors, reserve, swap, resize and insert on drained vectors (AC-1, RS-8, SW-1, RZ-9, AS-8, IN-12, CL-2 all retired).
_vec_char_capacity / _vec_char_max_size / _vec_char_data — char.c:304, 317, 291
none open
AC-1 retired in Pass 7 (all three accessors work on a drained vector) and DA-1 retired in Pass 8 (
data()returnsconst char*, so the const-laundering is gone). Blank vectors still error viaERR_BUF_NULL— intended, nothing to return.
_vec_char_at — char.c:366
1 S3 · 1 C
AT-1 retired in Pass 8:
at()returnschar*and reports failure asnullptr.
| ID | Sev | Location | Finding |
|---|---|---|---|
AT-2 |
S3 | char.c:376-383 | the OOB path sets err (flags + origin AT) and returns nullptr, but still prints a raw line to stdout via printf (378-379), polluting program output and bypassing the error channel. The residual hazard moved to the callers: front/back dereference the nullptr (428, 446) → SEGV (FB-1), and assign dereferences at 750 (AS-7) |
AT-3 |
C | char.c:366, 384 | at() returns a mutable char* into the buffer of a const _VecCharInternal* — const-laundering callers can write elements behind the API's back, bypassing push_back/resize bookkeeping; the pointer also invalidates on any realloc |
_vec_char_begin / _vec_char_end — char.c:387, 400
1 S2 · 1 C
| ID | Sev | Location | Finding |
|---|---|---|---|
BE-1 |
S2 | char.c:388-410, 728, 778 | FAILURE no longer exists, so the failure value changed but the defect did not: begin returns raw status.flags (bitmask 2/4) as an index (395), and end's (int)(size - 1) (410) is reachable for a drained vector — size == 0 wraps to SIZE_MAX (harness: end(drained) = 18446744073709551615, flags=0). Flags-as-index and SIZE_MAX-as-end are indistinguishable from real positions; SIZE_MAX + 1 wraps to 0 at char.c:728, 778, so assign into a drained destination erases [0, 0) and fails with ERR_OUT_OF_BOUNDS (AS-1). Root of ER-1 |
BE-2 |
C | char.c:410 | (int) cast on a size_t return — now the direct cause of the SIZE_MAX wrap above |
_vec_char_front / _vec_char_back / _vec_char_empty — char.c:413, 431, 449
1 S1 · 1 S2
| ID | Sev | Location | Finding |
|---|---|---|---|
FB-1 |
S1 | char.c:423-428, 441-446, 376-383 | the Pass-9 guard meant to kill the drained SEGV is inverted and probes the wrong index. at() returns nullptr on failure, so if (_vec_char_at(...) != nullptr) fires for valid vectors: front/back on a populated vector return 0 with ERR_OUT_OF_BOUNDS (harness Pass 11: front('ab')=0/flags=16, back=0/flags=16 — expected 97/98). For a drained vector at returns nullptr, the condition is false, control falls through to the deref — front → *_vec_char_at(ptr, 0, err) (428), back → *_vec_char_at(ptr, size - 1, err) (446) — SEGV (ASan Pass 11: SEGV on 0x0 at char.c:428). back also probes index 0 (441) instead of size - 1. Both branches wrong: condition must be == nullptr, and back must probe the last index |
AC-2 |
S2 | char.c:450-464 | empty() still returns 0 in both directions: a drained vector takes the size == 0 branch and return SUCCESS (461), a populated one falls through to return status.flags, also 0 (463) — so if (empty(&v)) is never true for a valid vector. Harness: drained → 0/err 0, populated → 0/err 0 |
Algorithms
_vec_char_erase (both overloads) — char.c:466, 485
1 S2 · 1 S3 · 1 C
| ID | Sev | Location | Finding |
|---|---|---|---|
ER-1 |
S2 | char.c:498-503 | erase on an empty vector never no-ops: a drained vector passes the __null_check guard now, then the range check (498-503: `size < iter_end |
ER-3 |
S3 | char.c:477 vs 498-503 | the single-arg overload silently returns on iter >= size (477) with err untouched (harness: drained erase-1 → flags=0), while the range overload reports ERR_OUT_OF_BOUNDS for the same condition. A typo'd index is invisible to an err-driven caller |
ER-2 |
C | char.c:466, 485, 478-481, 507-511 | zeroing loops should be memset; two stale // TODO: Fix return value comments remain (466, 485) |
_vec_char_resize (both overloads) — char.c:516, 613
2 S2 · 2 S3 · 3 C
RZ-9 retired in Pass 7 (drained resize grows). RZ-8 retired in Pass 10 — and confirmed in Pass 11 the fix stuck: the blank branch flows through
_vec_char_reserve(546, 643), which nowcallocs the requested capacity, so the fill loop stays in bounds (harness:resize(VEC_INIT, 4)→ err=0, size=4, cap=4).
| ID | Sev | Location | Finding |
|---|---|---|---|
RZ-3 |
S2 | char.c:544, 563, 641, 660 | the max_size save/restore is now pure belt-and-braces (reserve leaves max_size alone since Pass 10) and looks removable. Nothing documents that |
RZ-1 |
S2 | char.c:528-533, 625-630 | size == _internal_size returns ERR_OUT_OF_BOUNDS for a legitimate no-op; the code is also semantically wrong |
RZ-4 |
S3 | char.c:595-608 vs 686-699 | 3-arg fills the discarded tail with val, 2-arg with '\0' (harness Pass 11: resize(3) then resize(4,'v') → arr[2]=0 from the 2-arg call, arr[3]='v'). data() exposes the difference |
RZ-2 |
S3 | char.c:574, 671 | unguarded tmpcp *= 2 wraps on overflow. Defensive |
RZ-5 |
C | char.c:553-558, 587-592, 650-655 | dead post-reserve capacity checks in the size == 0 branches of both overloads (553, 650) and in the 2-arg growth branch (587); the 3-arg growth branch has no check at all, relying on reserve to be exact |
RZ-6 |
C | char.c:516-611 vs 613-702 | two ~95-line near-identical bodies, with the growth blocks copy-pasted between them |
RZ-7 |
C | char.c:559-562, 597-600, 604-607, 656-659, 688-691, 695-698 | fill loops should be memset |
_vec_char_assign (both overloads) — char.c:704, 762
4 S2 · 1 S3 · 1 C
AS-8 retired in Pass 7 but the fix regressed in Pass 9 (inverted
at()guard, AS-7) — harness Pass 11 still shows flags=16, dest wiped. AS-4 retired in Pass 10: the internalreserveno longer capsmax_size, so assign can't brick growth anymore.
| ID | Sev | Location | Finding |
|---|---|---|---|
AS-1 |
S2 | char.c:709-715, 728-729, 778-779 | fails on any empty destination. A blank destination trips the __null_check guard (709-715) → err=4, origin ASSIGN (harness Pass 11). A drained one passes the guard, then end() returns SIZE_MAX (BE-1) and end + 1 wraps to 0 (728, 778), so erase(0, 0) fails the range check → err=16 (harness: flags=16, size stays 0). Two paths, same refusal |
AS-2 |
S2 | char.c:728-758, 778-802 | not atomic: erase → reserve → push_back loop. Reserve failure leaves the destination wiped; a push_back failure mid-loop leaves it partially filled. Still no rollback |
AS-3 |
S2 | char.c:736, 786 | no > _internal_max_size guard on the assign count |
AS-7 |
S2 | char.c:743-751 | the Pass-9 at() guard in assign1 is inverted: at() returns nullptr on failure, so the != nullptr branch fires for valid iterators — assign1 fails with ERR_OUT_OF_BOUNDS on the first element of every non-empty source, after erase already wiped the destination (harness Pass 11: dest {x,y} ← src {a,b} → flags=16, size=0). The nullptr-deref it was meant to prevent is still live at 750. Condition must be == nullptr |
AS-9 |
S3 | char.c:767-771 | the null guard in the amount overload has no return, unlike assign1 — control falls through with err already flagged. Masked today only because the erase at 778 rejects the same state downstream |
AS-6 |
C | char.c:704-760 vs 762-804 | near-duplicate overloads differing only in the value source |
_vec_char_insert (all three overloads) — char.c:806, 861, 897
1 S1 · 2 S2 · 2 S3 · 2 C
IN-12 retired in Pass 7 (one-arg insert works on drained vectors). IN-11 retired in Pass 11: the blank branch no longer writes past its
malloc(1)— it setscapacity=1andsize=1and writesarr[0](826-831); harness:insert(VEC_INIT, 0, 'q')→ err=0, size=1, arr[0]='q', ASan clean. IN-10 unchanged and still live.
| ID | Sev | Location | Finding |
|---|---|---|---|
IN-10 |
S1 | char.c:865-872 | the amount overload never calls __null_check: the guard at 866-870 tests a freshly-initialized local status, which is always 0, so it can never fire; ptr is then dereferenced raw at 872. Every other entry point in the file validates first. ASan Pass 11: SEGV READ of 0x8 at char.c:872 — _vec_char_insert(nullptr, 0, 2, 'x', &e) |
IN-1 |
S2 | char.c:911-917 | inverted range check: iter_end > iter_start rejects every non-empty range with ERR_OUT_OF_BOUNDS and accepts only empty ranges, which then silently do nothing (loop at 924 never runs). Valid insert-range never inserts. Harness: dest {a,b}, src {Z}, insert at 1 range [0,1) → err=16, size stays 2; empty range [1,1) → err=0 |
IN-4 |
S2 | char.c:833-838, 884-893 | no max_size pre-check in the amount overload: the loop pushes until one-arg insert trips size+1 > max_size (833) and then returns, but everything already inserted stays — an oversized amount half-inserts and leaves the vector half-modified |
IN-5 |
S3 | char.c:878-882 | amount <= 0 on a size_t is always just == 0 |
IN-6 |
S3 | char.c:884 | iter + amount can overflow size_t, wrapping the loop bound and exiting early — a silent partial insert |
IN-8 |
C | char.c:924-934 | the loop reads iterator->_internal_arr[i] raw (927) instead of a bounds-checked accessor. Unreachable today: IN-1's inversion rejects every non-empty range, so the body never runs. The iter_end > iterator->_internal_size clause at 911 does bound the read once IN-1 is fixed, so this stays a style finding |
IN-9 |
C | char.c:884-893 | O(amount²) loop of single inserts where one grow + memmove would do |
Support
utils.h / utils.c
3 S2 · 3 C
| ID | Sev | Location | Finding |
|---|---|---|---|
UT-1 |
S2 | utils.h:9 | SIZE_MAX / sizeof(int) is what the char vector uses as _internal_max_size, capping it at 1/4 of the usable address space |
UT-6 |
S2 | utils.c:14-16 | vec_get_error_desc's default case is an expression statement — "Unknown"; with no return — so an out-of-range code falls off the end of a non-void function: UB, and the caller printf's a garbage char*. Current call sites only pass constants, so it is one cast away |
UT-7 |
S2 | utils.c:22-35 | vec_print_errs prints err.origin only on the SUCCESS branch (24), where it is always NULL on an untouched err_t — %s of NULL, UB — and the failure branch (27-35) never prints origin at all. The root-cause feature surfaces exactly backwards. Harness: flags=8 + origin set prints only the description; flags=0 + origin prints it |
UT-2 |
C | utils.h:67-69 | LOGERROR expands error_messages[error_code], but the table was deleted from utils.c and never declared in the header — any expansion is an undeclared-identifier compile error. The design was also wrong twice over: codes are bit flags (ERR_OUT_OF_BOUNDS = 16), not table indices, so it would index past any 10-entry table anyway. Latent — no caller remains |
UT-5 |
C | utils.h:66 | DEBUG prints the literal DEBUB: , so char.c:162 — the only DEBUG call in the tree, on the clamp path in push_back — emits a misspelled prefix |
UT-4 |
C | utils.h:7-9, 16-17, 65-71 | static const in a header gives every TU its own copy; ERR_SIZE_ZERO message mentions < 0 on a size_t; LOG/DEBUG/LOGERROR are hardwired to stdout and single-arg; find_len is UB for pointers; CAPACITY and VEC_FAULT_BITS are now referenced by nothing in the tree |
char.h
1 S2 · 5 C
CH-7 retired in Pass 10: the DESIGN CONTRACT (char.c:11-16) states the
charvector is a raw byte container, never a C string, so there is nofrom_cstrgap — that was the answer CR-4 was waiting for.
| ID | Sev | Location | Finding |
|---|---|---|---|
CH-9 |
S2 | char.h:11-106 · char.c:34, 46, 53 | every public identifier is reserved by the C standard: _VecCharInternal and all 27 _vec_char_* functions begin with _ at file scope, which C11 7.1.3 reserves for the implementation, and the .c adds three __-prefixed helpers (__null_check ×2, __origin_override), reserved the same way. Undefined behaviour on collision with any libc or third-party name |
CH-4 |
C | char.h:4-9 | <stdio.h>/<stdlib.h>/<string.h>/<stdint.h> in the header, only needed by the .c |
CH-5 |
C | char.h | no doc comments on any function; the file-level DESIGN CONTRACT (char.c:7-32) documents the err requirement and ownership, but no function explains its own contract |
CH-8 |
C | char.h:11-17 | the public struct is named with a _ prefix and its four fields are named _internal_* instead of being PRIVATE, so the "internal" contract is expressed only by spelling. Combined with CH-9 this is the whole naming problem |
CH-2 |
C | char.h:22-106 | const on by-value params throughout (const size_t size, const char val, const size_t idx, …) |
CH-3 |
C | char.h | no size() accessor, so callers reach into _internal_size directly |
Project
5 C
PR-8 demoted S3→C in Pass 10: the DESIGN CONTRACT (char.c:25) now documents the non-NULL
errrequirement, so a NULLerr_t*is caller UB, not an undocumented hazard. No runtime check was added.
| ID | Sev | Location | Finding |
|---|---|---|---|
PR-8 |
C | char.c:88, 334 (and every guard) | no function checks err for NULL before the first error path derefs it; a NULL err_t* segfaults on the first failure. Now documented (char.c:25) — a cheap defensive check would still harden the API |
PR-2 |
C | char.c:42, 49, 76-79, 851-857 | mixed brace style (single-line if vs expanded blocks) and a stray blank line between the shift loop and return at 857; .clang-format exists and is not run |
PR-7 |
C | CMakeLists.txt:6-31, main.c:38-102 | there is no test target at all — CMakeLists builds libvec and libvec_bin and nothing else — and main() exercises zero libvec functions: every vector call is commented out. Nothing in the tree calls a single function in this tracker, so none of the 4 open S1s can regress loudly |
PR-4 |
C | main.c:15, 38-102 | main still has no char.h include (only a commented // #include <libvec.h>, 15), so even uncommenting would not compile; the commented body still references the removed API (Vec8_t, create, by-value v1 = push_back(&v1, …), print_vec, delete, swap, v1.size) and the mismatch with the err_t API (_vec_char_push_back(&v1, 'c', &status)) |
PR-5 |
C | include/libvec.h:4, CMakeLists.txt:11-14 | empty placeholder; int/float/string are empty dirs already listed as comments in CMakeLists.txt |
Fixed
58 fixed IDs — no rows
PB-1 PB-2 PB-3 PB-4 PB-6 PB-7 PB-8 PB-14 PB-15 PB-16 PB-17 PB-18 PB-20 PB-21
RS-2 RS-3 RS-4 RS-6 RS-7 RS-8
IV-1 IV-3 IV-4
CH-1 CH-7
CR-3 CR-4 CR-6 CR-7 CR-8 CR-10 CR-11
SW-1 SW-2
FB-2
AC-1 DA-1
AT-1
DL-1 DL-3
UT-3
PR-1 PR-3
CL-1 CL-2
RZ-8 RZ-9
AS-4 AS-8
ST-1 ST-2
IN-2 IN-11 IN-12
FX-2 FX-3 FX-4 FX-5
Conventions
How to read this
Each function is a #### block with one table; rows are sorted by severity, so the worst finding in each function is on top. Fixed findings are not rows — they live in the Fixed ID list only.
Severity is the Sev column. Every row is OPEN (there is no DECIDE row since Pass 10), so there is no separate state column to scan.
Rules
- IDs are permanent. Never reused, never renumbered. An ID leaves the file only when its row is deleted outright, and only under rule 3.
- Locations drift freely. They are the only field updated in place.
- False findings are deleted, not archived. If a row turns out to be my own error, the row is removed. A new ID carries the accurate finding if there is one. No
DISPUTEDstate, no corrections log — a tracker is not a diary. - State is
OPEN; it is carried by theSevcell.FIXEDmeans the row is gone and its ID is in the bottom list. - Fixed findings live in the ID list, nothing else.
Verification history
Passes 1-3 @ 2026-09-28 12:48:04 —
char.c, 3 independent reads. Pass 4 @ 2026-09-28 —push_back/pop_back(old char.c:67-157), ASan. Pass 5 @ 2026-09-28 — whole file re-read plus 10 harness runs, ASan. Line count moved 804 to 800 with thecreaterewrite. Pass 6 @ 2026-10-08 — theerr_tmigration. Full re-read:char.c877 lines,char.h108,utils.h73,utils.c71,main.c105,CMakeLists.txt33.createandis_validare gone —_vec_char_cpyand__null_checkreplace them — and every function now reports through anerr_t*out-param. Compiles under-std=c2x -Wall -Wextrawith one warning (IV-4). Six ASan harness runs. Thirteen rows died with the old code and their IDs are in Fixed. Pass 7 @ 2026-10-09 — post-migration cleanup landed inchar.c(874 lines):__null_checkdropped the unusedresand itsERR_SIZE_ZEROprobe,empty()was rewritten (still wrong, AC-2), and bothresizeoverloads now branch onptr->_internal_size == 0. Re-verified with 40 ASan harness runs. Eight rows retired — IV-3, IV-4, AC-1, RS-8, AS-8, RZ-9, IN-12, CL-2. The removal madeend()/front()/back()reach their wrap/atpaths on empty vectors (BE-1, FB-1) and turnedpop_back's soft refusal into a live underflow write (PB-17 promoted S3→S1). All line refs re-derived. Pass 8 @ 2026-10-09 — accessor refactor landed inchar.c(876 lines) andchar.h:data()wentconst char*,at()returnschar*withnullptron both error paths and setserr(+ origin AT) on OOB, andfront/back/assigndereference the new pointer. Re-verified with 41 ASan harness runs. Two rows retired — AT-1 and DA-1 — and AT-2 demoted S2→S3. The pointer change made drainedfront/backa live null-deref SEGV — FB-1 promoted S2→S1 — and added AT-3.main.cdropped itschar.hinclude. All line refs re-derived. Pass 9 @ 2026-10-09 —char.c(887 lines):clearswitched tomemset(CL-1 retired) andat()'s OOB print moved fromfprintf(stderr)toprintf(stdout)(AT-2).front/backand assign1 gainedat()-null guards that are inverted: front/back on populated now return 0 +ERR_OUT_OF_BOUNDS(FB-1), assign1 fails on every non-empty source (AS-7 promoted S3→S2, the Pass-7 AS-8 fix regressed). Drainedfront/backSEGV unchanged. Re-verified with 43 ASan harness runs. Pass 10 @ 2026-10-09 — editing pass inchar.c(935 lines) andchar.h(108). File gained a DESIGN CONTRACT block (7-32: raw byte container, never NUL-terminated,errmust be non-NULL, spare slots uninitialized);pop_backadded a size-0 early return (187-191);push_back's blank materialize resets_internal_size = 0(148);deleteis non-const and resets_internal_arr = nullptr(345-346);reserve'smax_size = capacityis commented out (249); one-arginsertadded a blank-materialize branch (818-831). Re-verified with a fresh 20-case ASan harness. Thirteen rows retired — PB-14, PB-15, PB-16, PB-17, PB-20, PB-21, DL-1, ST-1, RZ-8, AS-4, RS-3 (behavioral fixes / documented-contract resolutions), CR-4 (the DECIDE — the header now pins raw-byte semantics) and CH-7. IN-11 promoted S3→S1: the new blank path writesarr[1]pastmalloc(1)(ASan WRITE at char.c:829). RS-7's blast radius widened: blankreserve+ pushes → WRITE at 173;resize(VEC_INIT, 4)→ WRITE at 561. PR-8 demoted S3→C (contract documented at char.c:25). Front/back inverted guards (FB-1) and assign1 (AS-7) still live; 6 open S1s remained. All line refs re-derived. Pass 11 @ 2026-10-09 — editing pass inchar.c(936 lines);char.hunchanged.cpygained amax_sizeguard (101-106) and reportsERR_ALLOC_FAILon malloc failure (108-112);reserve's blank path switched tocalloc(capacity)(223), its guard order was restructured (205-219), and truncation became amemset— but the truncation runs afterptr->_internal_arr = res(243), so the tail zeroing now writes past the shrunk block: RS-1 got worse (ASan Pass 11: heap-buffer-overflow WRITE at char.c:246, was a use-after-free at 242).shrink_to_fitgained a drained no-op (267-270);swapguard narrowed to ERR_VEC_NULL with a struct-assign exchange (278-288);deletedropped the(void*)cast and resets size/capacity (340-346); the oldmax_size = capacityline was deleted entirely, not just commented. Re-verified with a fresh 24-case ASan harness. Nine rows retired — CR-10, CR-11, RS-2, RS-4, RS-7, ST-2, SW-1, DL-3, IN-11 (the blank insert writesarr[0], 818-832, in bounds). FB-1 and IN-10 re-confirmed live (SEGV at 428 and 872). 4 open S1s remain. All line refs re-derived.
Reproducer harness:
clang -std=c2x -g -fsanitize=address -Isrc -Isrc/lib \
t.c src/lib/char/char.c src/lib/utils/utils.c
-std=c2x is required — char.c uses nullptr, so the project does not compile as C17. Reproducers that ask for a multi-exabyte calloc additionally need ASAN_OPTIONS=allocator_may_return_null=1, or ASan aborts on the 2^61-byte calloc before the interesting line runs. Pass 6-11 harnesses were scratch files outside the tree (only this tracker may be written), not committed.
ID map
FX-1 … FX-5 are first-pass IDs. FX-1 was a duplicate of PB-6 and was never given a row.
| Prefix | Scope | Prefix | Scope |
|---|---|---|---|
PB- |
push_back, pop_back | AT- |
at |
RS- |
reserve | FB- |
front, back |
ST- |
shrink_to_fit | BE- |
begin, end |
CR- |
create → cpy | SW- |
swap |
AC- |
accessors | CL- |
clear, print_vec |
DA- |
data | IV- |
is_valid → __null_check |
DL- |
delete | UT- |
utils.h, utils.c |
ER- |
erase | CH- |
char.h |
RZ- |
resize | PR- |
project |
AS- |
assign | FX- |
retired first-pass IDs |
IN- |
insert |