Files
libvec/BUGS.md
T

400 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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](#triage)
- [Findings](#findings)
- [Core storage](#core-storage) — push_back/pop_back · cpy · reserve · shrink_to_fit · swap · delete · clear/print
- [Validation & accessors](#validation--accessors) — __null_check · capacity/max_size/data · at · begin/end · front/back/empty
- [Algorithms](#algorithms) — erase · resize · assign · insert
- [Support](#support) — utils · char.h · project
- [Fixed](#fixed)
- [Conventions](#conventions)
---
<a id="triage"></a>
## 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
<a id="core-storage"></a>
### Core storage
<a id="pb"></a>
#### `_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 |
<a id="cpy"></a>
#### `_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).
<a id="reserve"></a>
#### `_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 |
<a id="shrink"></a>
#### `_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.
<a id="swap"></a>
#### `_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).
<a id="delete"></a>
#### `_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)` |
<a id="clear"></a>
#### `_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.
<a id="validation--accessors"></a>
### 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).
<a id="accessors"></a>
#### `_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.
<a id="at"></a>
#### `_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 |
<a id="beginend"></a>
#### `_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 |
<a id="frontback"></a>
#### `_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** |
<a id="algorithms"></a>
### Algorithms
<a id="erase"></a>
#### `_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 || iter_end <= iter_start`) fires — for `[0, 1)` because `0 < 1`, and even `[0, 0)` because `0 <= 0`. **Harness Pass 11: erase(begin, end+1) on drained → err=16, origin ERASE.** Past the guard, `begin()`/`end()` still hand back flag bitmasks and `SIZE_MAX` (BE-1) |
| `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) |
<a id="resize"></a>
#### `_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 `calloc`s 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` |
<a id="assign"></a>
#### `_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 |
<a id="insert"></a>
#### `_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 |
<a id="support"></a>
### Support
<a id="utils"></a>
#### `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 |
<a id="charh"></a>
#### `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 |
<a id="project"></a>
#### 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 |
---
<a id="fixed"></a>
## Fixed
<details>
<summary><b>58 fixed IDs — no rows</b></summary>
```
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
```
</details>
---
<a id="conventions"></a>
## Conventions
<details>
<summary><b>How to read this</b></summary>
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](#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.
</details>
<details>
<summary><b>Rules</b></summary>
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.
</details>
<details>
<summary><b>Verification history</b></summary>
> **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](#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:**
```sh
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.
</details>
<details>
<summary><b>ID map</b></summary>
`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 | | |
</details>