Files
libvec/BUGS.md
T

31 KiB
Raw Blame History

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

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_back malloc(1) → capacity=1, size=0, then WRITE arr[0] (140-152). reserve calloc(capacity) → capacity=capacity (221-232). insert one-arg malloc(1) → capacity=1, size=1, WRITE arr[0] (818-832). Same input state (blank vector), three allocations, three struct shapes; insert and resize also disagree with push_back on which blank states are accepted.

max_size is no longer capped by reserve. The old ptr->_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.c gained a DESIGN CONTRACT block (11-32): raw byte container, buffers NEVER NUL-terminated, err must be non-NULL, slots past size are 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 removed max_size line.

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: malloc failure now reports ERR_ALLOC_FAIL (108-112), not ERR_BUF_NULL, and src_size is 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_size capping). Three retired in Pass 11: RS-7 (the blank path is now calloc(capacity) at 223 and commits exactly the requested capacity — harness: reserve(VEC_INIT, 5) → cap=5, err=0), RS-4 (truncation is memset, 246-247) and RS-2 (guard restructured, 205-219 — a NULL vector now reports ERR_VEC_NULL before the capacity == 0 check). 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 the ERR_BUF_NULL path at 261-266 — pre-existing, intended. The double __null_check at 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_NULL only (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-delete push_back are safe). DL-3 retired in Pass 11: the (void*) cast on the free argument 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: clear uses memset (361). CL-2 retired in Pass 7: print_vec on a drained vector prints the newline with err untouched; blank vectors take the intended ERR_BUF_NULL path.

Validation & accessors

IV-3 and IV-4 retired in Pass 7: __null_check no longer ORs informational ERR_SIZE_ZERO into the validity mask (only ERR_VEC_NULL and ERR_BUF_NULL remain), and the unused local res is 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() returns const char*, so the const-laundering is gone). Blank vectors still error via ERR_BUF_NULL — intended, nothing to return.

_vec_char_at — char.c:366

1 S3 · 1 C

AT-1 retired in Pass 8: at() returns char* and reports failure as nullptr.

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 now callocs 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 internal reserve no longer caps max_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 sets capacity=1 and size=1 and writes arr[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 char vector is a raw byte container, never a C string, so there is no from_cstr gap — 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 err requirement, so a NULL err_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
  1. IDs are permanent. Never reused, never renumbered. An ID leaves the file only when its row is deleted outright, and only under rule 3.
  2. Locations drift freely. They are the only field updated in place.
  3. 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 DISPUTED state, no corrections log — a tracker is not a diary.
  4. State is OPEN; it is carried by the Sev cell. FIXED means the row is gone and its ID is in the bottom list.
  5. 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 the create rewrite. Pass 6 @ 2026-10-08 — the err_t migration. Full re-read: char.c 877 lines, char.h 108, utils.h 73, utils.c 71, main.c 105, CMakeLists.txt 33. create and is_valid are gone — _vec_char_cpy and __null_check replace them — and every function now reports through an err_t* out-param. Compiles under -std=c2x -Wall -Wextra with 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 in char.c (874 lines): __null_check dropped the unused res and its ERR_SIZE_ZERO probe, empty() was rewritten (still wrong, AC-2), and both resize overloads now branch on ptr->_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 made end()/front()/back() reach their wrap/at paths on empty vectors (BE-1, FB-1) and turned pop_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 in char.c (876 lines) and char.h: data() went const char*, at() returns char* with nullptr on both error paths and sets err (+ origin AT) on OOB, and front/back/assign dereference 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 drained front/back a live null-deref SEGV — FB-1 promoted S2→S1 — and added AT-3. main.c dropped its char.h include. All line refs re-derived. Pass 9 @ 2026-10-09 — char.c (887 lines): clear switched to memset (CL-1 retired) and at()'s OOB print moved from fprintf(stderr) to printf(stdout) (AT-2). front/back and assign1 gained at()-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). Drained front/back SEGV unchanged. Re-verified with 43 ASan harness runs. Pass 10 @ 2026-10-09 — editing pass in char.c (935 lines) and char.h (108). File gained a DESIGN CONTRACT block (7-32: raw byte container, never NUL-terminated, err must be non-NULL, spare slots uninitialized); pop_back added a size-0 early return (187-191); push_back's blank materialize resets _internal_size = 0 (148); delete is non-const and resets _internal_arr = nullptr (345-346); reserve's max_size = capacity is commented out (249); one-arg insert added 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 writes arr[1] past malloc(1) (ASan WRITE at char.c:829). RS-7's blast radius widened: blank reserve + 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 in char.c (936 lines); char.h unchanged. cpy gained a max_size guard (101-106) and reports ERR_ALLOC_FAIL on malloc failure (108-112); reserve's blank path switched to calloc(capacity) (223), its guard order was restructured (205-219), and truncation became a memset — but the truncation runs after ptr->_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_fit gained a drained no-op (267-270); swap guard narrowed to ERR_VEC_NULL with a struct-assign exchange (278-288); delete dropped the (void*) cast and resets size/capacity (340-346); the old max_size = capacity line 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 writes arr[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