Files
libvec/BUGS.md
T

326 lines
28 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
> **Verified** `char.c` @ **2026-09-28 12:48:04**, 3 independent passes.
> `char.h` 108 lines, `utils.h` 31, `utils.c` 12, `CMakeLists.txt` 32, `main.c` 55.
> Line numbers are the *only* field that drifts. Re-verify before trusting them.
>
> **Pass 4** @ **2026-09-28**, `push_back` / `pop_back` (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; re-measured per pass.
**Open 79** — 11 S1, 27 S2, 11 S3, 29 C
**1 DECIDE** · **Fixed 12** (listed at the bottom, no rows) · 79 rows
**Severity** `S1` memory safety · `S2` wrong result / broken contract ·
`S3` edge case · `C` cleanup · `DECIDE` open design question
## How to read this
Each function is a `###` with one table. Rows are sorted by severity (S1 first),
so the worst thing in each function is on top. Fixed findings are **not** rows —
they are a flat ID list at the bottom of the file.
Tracking state is a **column**, not a checkbox: a `State` cell carries more than
"done or not done" — `DECIDE` has no checkbox equivalent, and it is load-bearing
here. And task-list syntax inside a table cell is renderer-dependent, so a
`State` column is the form that survives any markdown preview. If your preview
*does* render checkboxes in tables and you want click-to-toggle, a leading `Done`
column can be added without disturbing anything else.
## 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.** 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` / `DECIDE`. `FIXED` means the row is gone and its ID is
in the bottom list.
5. **Fixed findings live in the ID list at the bottom**, nothing else.
## Triage — the 11 open S1s, in suggested order
| # | ID | Location | Summary |
|----|---|---|---|
| 1 | <code><nobr>RS-7</nobr></code> | char.c:175, 200 | `reserve` on a blank vector allocates 1024 but claims 5000. Ordinary `size`, live overflow |
| 3 | <code><nobr>PB-8</nobr></code> | char.c:89, 131 | live heap overflow, 2-line repro |
| 4 | <code><nobr>PB-14</nobr></code> | char.c:131, 154 | write through the freed buffer after `delete`, 2-line repro |
| 5 | <code><nobr>RS-1</nobr></code> | char.c:180-195 | use-after-free in `reserve`'s truncation loop, ASan confirmed |
| 6 | <code><nobr>IN-1</nobr></code> | char.c:783 | inverted `iter_end > iter_start` |
| 7 | <code><nobr>IN-2</nobr></code> | char.c:783 | dead bounds check sitting behind IN-1 |
| 8 | <code><nobr>DL-1</nobr></code> | char.c:293, 297 | `const` delete, double free on second call |
| 9 | <code><nobr>DL-2</nobr></code> | char.h:11-17 | no ownership marker, struct copy double-frees |
| 8 | <code><nobr>UT-2</nobr></code> | utils.c:3-12 | `error_messages[0]` / `[1]` are `NULL` |
| 9 | <code><nobr>PB-3</nobr></code> | char.c:106, 116 | capacity committed before the realloc check |
| 10 | <code><nobr>PB-15</nobr></code> | char.c:87, 128-131 | null-buffer path has no `else`, so `arr == NULL && size > 0` writes to NULL. Defensive |
> **Two of the eleven are the same class.** RS-7 and PB-8 both end in a
> write past a small block, and both come from a struct field claiming more
> bytes than were allocated: `capacity` set from the request rather than the
> block (RS-7, char.c:200), `calloc(1)` with `capacity` untouched (PB-8,
> char.c:89). One rule kills both: **never write `_internal_capacity` without
> writing the allocation it describes.** Verify with a single postcondition —
> after any function that touches capacity, the block size and the field must
> agree. CR-8 was a third member of this class; it is fixed as of Pass 5, and
> `create` now carries the same postcondition in one place (CR-9, char.c:44).
> **Fix IN-1 and IN-2 in the same commit.** The raw read in the 3-arg overload
> is unreachable *only because* IN-1's inversion empties the loop. Fixing IN-1
> alone converts a clean rejection into a live out-of-bounds read, because IN-2's
> bound check stays dead behind the short-circuit.
> **PB-8 and PB-14 are the only two rows in this file with a sanitizer trace
> attached.** Both reproduce in three lines from `main`. `delete` should null
> the struct's fields (DL-1) and push should size the materialization from
> `_internal_capacity` (PB-8); those two fixes close the whole S1 set for this
> function pair.
> **Reproducer harness**: `clang -std=c2x -g -fsanitize=address -Isrc/lib
> -Isrc/lib/char 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.
> **Defensive-only rows.** RZ-2, RS-2, PB-15 and PB-18 are not reachable
> without an allocation of ~2^61 bytes, or a hand-built struct. Kept as
> hardening because the guards are cheap and the invariant is worth stating.
---
## Bugs
### `_vec_char_push_back` / `_vec_char_pop_back` — char.c:71, 136
| ID | Sev | Location | Finding | State |
|-----|---|---|---|---|
| `PB-8` | S1 | char.c:89, 131 | `calloc(1)` on the null-buffer path leaves `_internal_capacity` at 1024, so the 2nd `push_back` writes `arr[1]` one byte past a 1-byte block. **ASan: `heap-buffer-overflow` WRITE of size 1 at char.c:131**, block allocated at char.c:89 | OPEN |
| <code><nobr>PB-14</nobr></code> | S1 | char.c:75, 131, 154 | neither function checks liveness. `_vec_char_delete` frees without clearing the struct (DL-1), so `delete(&v); push_back(&v,'z')` writes through the freed pointer. **ASan: `heap-use-after-free` WRITE of size 1 at char.c:131**, freed at char.c:297. Same for `pop_back`, which writes `'\0'` at char.c:154 | OPEN |
| <code><nobr>PB-15</nobr></code> | S1 | char.c:87, 128-131 | the materialize branch is guarded by `status == ERR_BUF_NULL && _internal_size == 0` and has no `else`. Any state with `arr == NULL && _internal_size > 0` falls through to char.c:126, takes `res = ptr->_internal_arr` (NULL) and writes `arr[size]`. No in-file producer, but nothing checks the invariant either | OPEN |
| <code><nobr>PB-3</nobr></code> | S1 | char.c:106, 116 | `_internal_capacity` assigned before the `!res` check; on OOM the struct overstates its capacity and the next call writes past the real block. Both branches | OPEN |
| <code><nobr>PB-9</nobr></code> | S2 | char.c:85-99| `push_back` materializes a null vector, making it a second path alongside `reserve`; the two disagree on allocation size (1 byte vs 1024) | OPEN |
| <code><nobr>PB-16</nobr></code> | S2 | char.c:116, 131 vs 154 | the growth `realloc` leaves the new tail uninitialized and `push_back` never writes a terminator at `_internal_size`, so `_vec_char_data` hands back a non-NUL-terminated buffer — `strlen(data(&v))` runs off the end. **ASan: `heap-buffer-overflow` READ of 2049 bytes past a 2048-byte region** grown at char.c:116. The lone `'\0'` write in either function is `pop_back`'s vacated slot, so termination is accidental | OPEN |
| <code><nobr>PB-21</nobr></code> | S2 | char.c:81, 104 | after any `reserve` the vector is permanently full: reserve sets `max_size = capacity`, so char.c:81 fails for good and `push_back` can only return `ERR_MAX_PASSED`. Duplicate view of AS-4, kept so this table stands alone | OPEN |
| <code><nobr>PB-17</nobr></code> | S3 | char.c:144-147 vs 87 | `pop_back` refuses to materialize while `push_back` does. A blank vector reports `ERR_BUF_NULL` (21) and logs a spurious "Buffer :- NULL" instead of `ERR_SIZE_ZERO` (22), so a `while (pop_back(&v) == SUCCESS)` drain reports the wrong reason | OPEN |
| <code><nobr>PB-18</nobr></code> | S3 | char.c:103, 116, 120-124 | `_internal_capacity == 0` makes `dst_cap` 0, and `realloc(arr, 0)` frees the block. The `!res` check then returns `ERR_ALLOC_FAIL` with `_internal_arr` dangling and capacity already committed as 0; the next push writes to freed memory. Capacity 0 is unreachable from this file today, so defensive | OPEN |
| <code><nobr>PB-20</nobr></code> | S3 | char.c:154 | only the vacated slot is zeroed, so bytes past `_internal_size` keep their values and are readable out of band through `data()`. `clear` zeroes the whole range and `erase` zeroes the tail; `pop_back` is the inconsistent one | OPEN |
> PB-1 and PB-2 were originally described as firing "on the very next call" and
> as "the clamp silently does nothing". Both claims were wrong in detail: PB-1
> needed `capacity < size+1 <= max_size < 2*capacity`, a band today's `VEC_MAX`
> never reaches, and PB-2's cause was `cap` being recomputed from the
> already-clobbered `capacity`, not the branch being skipped.
> **Pass-4 note.** Four pre-existing rows here were my own errors and were
> deleted: a "dead else" that is the common path, two `is_valid` double-call
> claims in a function that hoists, and an overflow that `VEC_MAX` makes
> impossible. Found by diffing the wording against the source, not by testing.
> The three new S1/S2 rows *were* tested; the reproducer command is above.
### `_vec_char_create` — char.c:23
> **Pass-5 note.** `create`'s growth block was rewritten this pass and the
> rewrite is correct. The invariant it now enforces in one place is
> `size <= _internal_capacity <= _internal_max_size` on return, held by the
> commit-back at char.c:44. Verified under ASan at `size` 0, 1, 1023, 1024,
> 1025, 2047, 2048, 2049, 2999, 5000 and `VEC_MAX - 1`: `capacity >= size` on
> every call, the `calloc` block matches the reported capacity, no overflow.
> One case to know about: `create(NULL, VEC_MAX - 1)` still returns
> `capacity=4611686018427387902` over `arr == NULL`, because growth is correct
> and the `calloc` then fails. That is CR-3 below, and it is unchanged.
> `dst_cap * 2` at char.c:42 cannot wrap, because the loop only doubles while
> the product stays under `_internal_max_size`. An intermediate revision of
> this fix omitted the commit-back and overflowed every `size > CAPACITY`; it
> never reached a build, so no ID was spent on it. Two `create` findings left
> the table this pass and their IDs are in the bottom list: **CR-7**, the
> second growth branch, which tested the ceiling (`capacity*2 > max_size`)
> instead of the loop's own postcondition — a superset that also fired after a
> successful grow, overwriting `capacity` with `size` and forcing the next
> `push_back` to reallocate for one byte; its `>` test also disagreed with the
> loop's `>=` exit on exactly one value, reachable only because `VEC_MAX` is
> odd. **CR-8**, the `>=` guard at char.c:32, which left `size == VEC_MAX`
> allocating 1024 bytes and copying `VEC_MAX` bytes into it. Both are gone
> because the branch they described no longer exists, not because either was
> re-filed under a new ID.
| ID | Sev | Location | Finding | State |
|---|---|---|---|---|
| <code><nobr>CR-6</nobr></code> | S2 | char.c:29, 39-45| null vector's `_internal_capacity` is set to `CAPACITY` (1024) while nothing is allocated. Currently **masked** by AC-1: `capacity()` returns `ERR_BUF_NULL` first, so the lie is not observable through the public API. The observable form of this same lie is PB-8, after `push_back` materializes the buffer | OPEN |
| <code><nobr>CR-3</nobr></code> | S3 | char.c:48-53 | on `calloc` failure returns a struct with an already-grown `capacity` and `arr == NULL`. Defensive. **Now reachable with an ordinary `size`, not just a huge one**: `create(NULL, 5000)` grows `capacity` to 8192 at char.c:44, then the 8192-byte `calloc` fails and the struct survives claiming 8192 over a null buffer | OPEN |
| <code><nobr>CR-4</nobr></code> | DECIDE | char.c:58-65 | no NUL terminator; is a `const char* src, size_t size` pair a byte range or a C string? Affects whether a `from_cstr` helper is needed | DECIDE |
### `_vec_char_is_valid` — char.c:6
| ID | Sev | Location | Finding | State |
|---|---|---|---|---|
| <code><nobr>IV-1</nobr></code> | S2 | char.c:6 | always returns non-zero, so `if (_vec_char_is_valid(&v))` is true for every input including NULL. Every call site compares codes by luck | OPEN |
| <code><nobr>IV-3</nobr></code> | S2 | char.c:6-17| `is_valid` is the only validation primitive in the file and it requires a non-null **buffer**, not just a non-null vector. Every accessor that only needs to validate the vector pointer therefore inherits `ERR_BUF_NULL`. This is the root cause of AC-1, AC-2, SW-1 and ST-2 | OPEN |
### `_vec_char_reserve` — char.c:156
| ID | Sev | Location | Finding | State |
|---|---|---|---|---|
| <code><nobr>RS-7</nobr></code> | S1 | char.c:175, 200 | the `ERR_BUF_NULL` path allocates `ptr->_internal_capacity` bytes, not the requested `capacity`, then line 200 sets `_internal_capacity = capacity` regardless. So `reserve(&v, 5000)` on a blank vector `calloc`s **1024** bytes and the struct claims 5000. **ASan: `heap-buffer-overflow` WRITE at char.c:131, 0 bytes after a 1024-byte region allocated at char.c:175**, reached by 1026 `push_back`s. The `SUCCESS` path at char.c:184 sizes the `realloc` correctly; only the null-buffer path is wrong. Second producer of PB-8, and unlike CR-7 this one needs no huge `size` — 5000 is ordinary | OPEN |
| <code><nobr>RS-1</nobr></code> | S1 | char.c:180, 186-193, 195| realloc result held in `res`, but the truncation loop writes `ptr->_internal_arr`, which is still the pre-realloc pointer. Use-after-free when a shrinking `reserve` moves the block. Introduced by the FX-3 fix. **ASan confirmed**: `create("abcdefgh",8)` then `reserve(3)` gives `heap-use-after-free WRITE` at char.c:194 into a block freed at char.c:184 | OPEN |
| <code><nobr>RS-2</nobr></code> | S3 | char.c:160-164 | `capacity == 0` checked before the NULL-vector check, so a NULL vector with `capacity == 0` reports `ERR_SIZE_ZERO` instead of `ERR_VECTOR_NULL`. Defensive | OPEN |
| <code><nobr>RS-6</nobr></code> | C | char.c:158, 178, 195| clang `-Wsometimes-uninitialized`: `res` used uninitialized at 195 if the `if`/`else if` chain falls through. Not reachable today since `is_valid` returns exactly 3 values, but real fragility | OPEN |
| <code><nobr>RS-3</nobr></code> | C | char.c:197 | `max_size = capacity` is correct under the hard-cap semantics but is documented nowhere. **This is also the cause of ST-1 and AS-4** | OPEN |
| <code><nobr>RS-4</nobr></code> | C | char.c:188-191 | element-at-a-time loop, should be `memset` | OPEN |
### `_vec_char_shrink_to_fit` — char.c:202
| ID | Sev | Location | Finding | State |
|---|---|---|---|---|
| <code><nobr>ST-1</nobr></code> | S2 | char.c:202, 217 -> 197| calls `reserve(ptr, _internal_size)`, and `reserve` sets `max_size = capacity`. So `max_size` is permanently capped to the current size and **the vector can never grow again** — every later `push_back` trips `size+1 > max_size` and returns `ERR_MAX_PASSED`. Silent brick. No save/restore, unlike `resize` (RZ-3) | OPEN |
| <code><nobr>ST-2</nobr></code> | S3 | char.c:204-211| returns `ERR_SIZE_ZERO` on an empty vector; shrinking an empty vector should be a successful no-op. Two unhoisted `is_valid` calls at 204 and 208 | OPEN |
### `_vec_char_swap` — char.c:221
| ID | Sev | Location | Finding | State |
|---|---|---|---|---|
| <code><nobr>SW-1</nobr></code> | S2 | char.c:229-233 | refuses NULL-buffer operands, so a blank vector can never be swapped against a populated one. Blocks the `create(nullptr, 0)` workflow outright | OPEN |
| <code><nobr>SW-2</nobr></code> | C | char.c:223 | temp built via `_vec_char_create(nullptr, 0)`; a plain initializer states the intent | OPEN |
### `_vec_char_delete` — char.c:289
| ID | Sev | Location | Finding | State |
|---|---|---|---|---|
| <code><nobr>DL-1</nobr></code> | S1 | char.c:289, 293 | takes `const *` so it cannot reset the vector; a second call is a double free. Only function still taking `const` in a mutable API | OPEN |
| <code><nobr>DL-2</nobr></code> | S1 | char.h:11-17 | no ownership marker on the struct, so a by-value copy yields two owners of one buffer and `delete` cannot detect it | OPEN |
| <code><nobr>DL-3</nobr></code> | C | char.c:293 | unnecessary `(void*)` cast; `const` on the struct does not propagate to the pointee | OPEN |
### `_vec_char_capacity` / `_vec_char_max_size` / `_vec_char_data` — char.c:261, 275, 252
| ID | Sev | Location | Finding | State |
|---|---|---|---|---|
| <code><nobr>AC-1</nobr></code> | S2 | char.c:254, 267, 281 | all three return `ERR_BUF_NULL` for a null vector, though none of them needs a buffer. `capacity()` should report capacity, not an error | OPEN |
| <code><nobr>DA-1</nobr></code> | S2 | char.c:251, 257| `char*` returned from a `const _VecCharInternal*`, laundering the const-correctness the mutable refactor was meant to establish | OPEN |
### `_vec_char_clear` / `_vec_char_print_vec` — char.c:298, 341
| ID | Sev | Location | Finding | State |
|---|---|---|---|---|
| <code><nobr>CL-1</nobr></code> | C | char.c:310-313 | element-at-a-time zeroing loop, should be `memset` | OPEN |
### `_vec_char_at` — char.c:320
| ID | Sev | Location | Finding | State |
|---|---|---|---|---|
| <code><nobr>AT-1</nobr></code> | S2 | char.c:323, 328, 335| returns error codes (23/25, and 21) as a `char`; `assign` at char.c:642 pushes such a value into the vector as data | OPEN |
| <code><nobr>AT-2</nobr></code> | C | char.c:332 | raw `fprintf` bypasses `LOGERROR` | OPEN |
### `_vec_char_begin` / `_vec_char_end` — char.c:352, 359
| ID | Sev | Location | Finding | State |
|---|---|---|---|---|
| <code><nobr>BE-1</nobr></code> | S2 | char.c:355, 365, 367, 630, 668 | `FAILURE` (`== 1`) returned as an index, indistinguishable from index 1. Single root cause of the `+ 1` at char.c:630 and char.c:668, of the assign-on-empty failures AS-1, and of the erase-on-empty failure ER-1 | OPEN |
| <code><nobr>BE-2</nobr></code> | C | char.c:364 | `(int)` cast on a `size_t` return | OPEN |
### `_vec_char_front` / `_vec_char_back` / `_vec_char_empty` — char.c:371, 379, 387
| ID | Sev | Location | Finding | State |
|---|---|---|---|---|
| <code><nobr>AC-2</nobr></code> | S2 | char.c:392, 393 | inverted: `SUCCESS` (0) when empty, `FAILURE` (1) when not, so `if (empty(v))` is false for an empty vector | OPEN |
| <code><nobr>FB-1</nobr></code> | S2 | char.c:373, 374, 381, 382 | returns `FAILURE` (1) as a `char`; `1` is valid data | OPEN |
| <code><nobr>FB-2</nobr></code> | C | char.c:373, 381 | validates only `ERR_VECTOR_NULL`, unlike every other accessor | OPEN |
### `_vec_char_erase` (both overloads) — char.c:398, 412
| ID | Sev | Location | Finding | State |
|---|---|---|---|---|
| <code><nobr>ER-1</nobr></code> | S2 | char.c:417, 418 | `iter_start >= iter_end` returns `ERR_OUT_OF_BOUNDS`, so `erase(begin(), end())` on an empty vector always errors. This is what breaks both `assign` overloads. On an empty vector the actual trip is char.c:417 (`iter_end > size`) because `end()` returned 1 and the caller added 1 — see BE-1 | OPEN |
| <code><nobr>ER-2</nobr></code> | C | char.c:396, 410, 422-426 | zeroing loop should be `memset`; two stale `// TODO: Fix return value` comments | OPEN |
### `_vec_char_resize` (both overloads) — char.c:432, 518
| ID | Sev | Location | Finding | State |
|---|---|---|---|---|
| <code><nobr>RZ-8</nobr></code> | S2 | char.c:434-444, 520-530 | both overloads reject a null-buffer vector with `ERR_BUF_NULL` before the `_internal_size == 0` branch that would have materialized one, so `resize` cannot be the first call on a blank vector. `push_back` can. **ASan: `create(NULL,0)` then `resize(0,4)` returns 21** and the vector is still empty, where `push_back` four times would have worked. Same shape as PB-17, opposite direction | OPEN |
| <code><nobr>RZ-3</nobr></code> | S2 | char.c:456, 471, 478, 497, 540, 557, 569, 579| the `max_size` save/restore is load-bearing (it undoes `reserve`'s cap) but looks removable. Nothing documents that | OPEN |
| <code><nobr>RZ-1</nobr></code> | S2 | char.c:442-446, 528-532| `size == _internal_size` returns `ERR_OUT_OF_BOUNDS` for a legitimate no-op; the code is also semantically wrong | OPEN |
| <code><nobr>RZ-4</nobr></code> | S3 | char.c:501-504 vs 583-586| 3-arg fills the discarded tail with `val`, 2-arg with `'\0'`. `data()` exposes the difference | OPEN |
| <code><nobr>RZ-2</nobr></code> | S3 | char.c:481, 567 | unguarded `tmpcp *= 2` wraps on overflow. Defensive | OPEN |
| <code><nobr>RZ-5</nobr></code> | C | char.c:462, 492, 548 | dead post-`reserve` capacity checks; `reserve` always assigns `capacity` to its argument exactly | OPEN |
| <code><nobr>RZ-6</nobr></code> | C | char.c:432-515 vs 518-597 | two ~84-line near-identical bodies, with the growth blocks copy-pasted between them | OPEN |
| <code><nobr>RZ-7</nobr></code> | C | char.c:444, 467, 501, 508, 530, 553, 583, 590| fill loops should be `memset`; misleading `LOG("ERR :- ...")` on what is now a no-op | OPEN |
### `_vec_char_assign` (both overloads) — char.c:600, 652
| ID | Sev | Location | Finding | State |
|---|---|---|---|---|
| <code><nobr>AS-1</nobr></code> | S2 | char.c:630, 668 | fails on an empty destination, via ER-1. Fatal now because status is propagated; the immutable version discarded it and continued | OPEN |
| <code><nobr>AS-2</nobr></code> | S2 | char.c:624-647, 661-687| not atomic: `erase` → `reserve` → `push_back` loop. Reserve failure leaves the destination wiped; a `push_back` failure mid-loop leaves it partially filled | OPEN |
| <code><nobr>AS-3</nobr></code> | S2 | char.c:624, 662, 679| no `> _internal_max_size` guard on the assign count | OPEN |
| <code><nobr>AS-4</nobr></code> | S2 | char.c:635, 673 | the internal `reserve` caps `max_size` at the assign count with no save/restore, which `resize` does have | OPEN |
| <code><nobr>AS-6</nobr></code> | C | char.c:600-649 vs 652-688 | near-duplicate overloads differing only in the value source | OPEN |
| <code><nobr>AS-7</nobr></code> | S3 | char.c:642 | no check that `at()` succeeded before pushing its result. Currently correct **only** because the guards at char.c:611-618 and char.c:624 happen to make every index in range and the buffer non-null. Removing or loosening either guard turns this into AT-1's data-corruption case | OPEN |
### `_vec_char_insert` (all three overloads) — char.c:691, 726, 759
| ID | Sev | Location | Finding | State |
|---|---|---|---|---|
| <code><nobr>IN-1</nobr></code> | S1 | char.c:779 | inverted check: `iter_end > iter_start` should be `iter_end <= iter_start`, so every valid non-empty range is rejected and only an empty range is accepted | OPEN |
| <code><nobr>IN-2</nobr></code> | S1 | char.c:779 | that clause short-circuits first, so the `iter_end > iterator->_internal_size` bound never fires | OPEN |
| <code><nobr>IN-4</nobr></code> | S2 | char.c:742, 747-754 | no `max_size` pre-check, so an oversized `amount` partially inserts and then fails, leaving the vector half-modified | OPEN |
| <code><nobr>IN-6</nobr></code> | S3 | char.c:747 | `iter + amount` can overflow `size_t` | OPEN |
| <code><nobr>IN-5</nobr></code> | S3 | char.c:742 | `amount <= 0` on a `size_t` is always just `== 0` | OPEN |
| <code><nobr>IN-8</nobr></code> | S2 | char.c:747-754, 793 | the loop copies via the raw `iterator->_internal_arr[i]` rather than a bounds-checked accessor. It is **unreachable today** only because IN-1's inversion makes `iter_end <= iter_start` always true, so the loop never runs. It becomes a live out-of-bounds read the moment IN-1 is fixed, unless IN-2 is fixed in the same change | OPEN |
| <code><nobr>IN-9</nobr></code> | C | char.c:747-754 | O(amount²) loop of single inserts where one grow + `memmove` would do | OPEN |
### `utils.h` / `utils.c`
| ID | Sev | Location | Finding | State |
|---|---|---|---|---|
| <code><nobr>UT-2</nobr></code> | S1 | utils.c:3-12 | indices 0 and 1 are never assigned, so `error_messages[0]` and `[1]` are `NULL`. `LOGERROR(SUCCESS)` prints `%s` of `NULL`, and both are values functions return. Latent only because no caller exists yet | OPEN |
| <code><nobr>UT-1</nobr></code> | S2 | utils.h:29 | `SIZE_MAX / sizeof(int)` is what the `char` vector uses, capping it at 1/4 of the usable address space | OPEN |
| <code><nobr>UT-3</nobr></code> | S2 | utils.c:3 | `SUCCESS`/`FAILURE` overlap the error-index namespace, so `0` is both "success" and a table slot | OPEN |
| <code><nobr>UT-5</nobr></code> | C | utils.h:23 | `DEBUG` prints the literal `DEBUB: `, so char.c:109 — the only `DEBUG` call in the tree, on the clamp path in `push_back` — emits a misspelled prefix | OPEN |
| <code><nobr>UT-4</nobr></code> | C | utils.h:7, 17, 20, 22-25, 27, 29 · utils.c:6, 10 | `static const` in a header gives every TU its own copy; `ERR_NULL` is now dead (defined and given a message, never referenced from `char.c`); `ERR_SIZE_ZERO` message mentions `< 0` on a `size_t`; `extern const char*` should be `const char* const[]`; `LOG`/`LOGERROR` are hardwired to stdout and single-arg; `find_len` is UB for pointers; `CAPACITY` is a magic number in a shared header | OPEN |
### `char.h`
| ID | Sev | Location | Finding | State |
|---|---|---|---|---|
| <code><nobr>CH-9</nobr></code> | S2 | char.h:11-106 (29 identifiers)| every public identifier is reserved by the C standard: `_VecCharInternal` and all 28 `_vec_char_*` functions begin with `_` at file scope, which C11 7.1.3 reserves for the implementation. Undefined behaviour on collision with any libc or third-party name. `clang -Weverything` reports all 28 | OPEN |
| <code><nobr>CH-4</nobr></code> | C | char.h:6-8 | `<stdio.h>`/`<stdlib.h>`/`<string.h>` in the header, only needed by the `.c` | OPEN |
| <code><nobr>CH-5</nobr></code> | C | char.h | no doc comments on any function, despite the `/**< */` style in utils.h | OPEN |
| <code><nobr>CH-7</nobr></code> | C | char.h:22-23 | no `from_cstr` / NUL-termination path for the `char` specialization. See CR-4 | OPEN |
| <code><nobr>CH-8</nobr></code> | 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 | OPEN |
| <code><nobr>CH-1</nobr></code> | C | char.h:32 vs char.c:156 | param named `max_size` in the header, `capacity` in the definition | OPEN |
| <code><nobr>CH-2</nobr></code> | C | char.h:23-106 | `const` on by-value params throughout (`const size_t size`, `const char val`, `const size_t idx`, …) | OPEN |
| <code><nobr>CH-3</nobr></code> | C | char.h | no `size()` accessor, so callers reach into `_internal_size` directly | OPEN |
### Project
| ID | Sev | Location | Finding | State |
|---|---|---|---|---|
| <code><nobr>PR-2</nobr></code> | C | char.c:1-3 | mixed brace style (single-line `if` in `erase`/`front`/`back`/`begin`/`empty` vs expanded blocks); `.clang-format` exists and is not run | OPEN |
| <code><nobr>PR-3</nobr></code> | C | char.c:204, 208, 224, 225, 229, 230, 254, 255, 263, 267, 277, 281, 291, 300, 304, 322, 326, 343, 344, 354, 361, 373, 381, 389, 390, 400, 401, 415, 416, 434, 438, 520, 524, 603, 607, 611, 615, 654, 658, 693, 697, 763, 767, 771, 775| `is_valid` called 2-3× per function and logging each time, in every function except `push_back`, `pop_back` and `reserve` — those three hoist | OPEN |
| <code><nobr>PR-7</nobr></code> | C | CMakeLists.txt:28, main.c:20-54 | the `test_runner` target exists but `main()` is entirely commented out, so nothing in the tree calls a single function in this tracker. This is the only place a regression in the 10 open S1s could be caught, and it is dead | OPEN |
| <code><nobr>PR-4</nobr></code> | C | main.c:23-53 | 100% commented out, references removed API `Vec8_t` and `v1.size` | OPEN |
| <code><nobr>PR-5</nobr></code> | C | include/libvec.h:4, CMakeLists.txt:12-14 | empty placeholder; `int`/`float`/`string` are empty dirs already listed as comments in CMakeLists.txt | OPEN |
## 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 | | `SW-` | swap |
| `AC-` | accessors | | `CL-` | clear, print_vec |
| `DA-` | data | | `IV-` | is_valid |
| `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 | | | |
---
`PB-1` `PB-2` `PB-4` `PB-6` `PB-7` `FX-2` `FX-3` `FX-4` `FX-5` `PR-1` `CR-7` `CR-8`