CHORE: Just fixed some random bugs
This commit is contained in:
@@ -1,10 +1,10 @@
|
|||||||
# libvec bug tracker
|
# libvec bug tracker
|
||||||
|
|
||||||
**Pass 11** — verified **2026-10-09** — `char.c` @ 936 lines · `char.h` @ 108
|
**Pass 12** — verified **2026-10-12** — `char.c` @ 957 lines · `char.h` @ 108
|
||||||
|
|
||||||
| Open | S1 | S2 | S3 | C | DECIDE | Fixed |
|
| Open | S1 | S2 | S3 | C | DECIDE | Fixed |
|
||||||
|:----:|:--:|:--:|:--:|:-:|:------:|:-----:|
|
|:----:|:--:|:--:|:--:|:-:|:------:|:-----:|
|
||||||
| **49** | 4 | 16 | 7 | 22 | 0 | 58 |
|
| **42** | 8 | 15 | 7 | 12 | 0 | 63 |
|
||||||
|
|
||||||
`S1` memory safety · `S2` wrong result / broken contract · `S3` edge case · `C` cleanup · `DECIDE` open design question
|
`S1` memory safety · `S2` wrong result / broken contract · `S3` edge case · `C` cleanup · `DECIDE` open design question
|
||||||
|
|
||||||
@@ -14,287 +14,205 @@
|
|||||||
|
|
||||||
## Contents
|
## Contents
|
||||||
|
|
||||||
- [Triage — the 4 open S1s](#triage)
|
- [Triage — the 8 open S1s](#triage)
|
||||||
- [Findings](#findings)
|
- [Findings](#findings)
|
||||||
- [Core storage](#core-storage) — push_back/pop_back · cpy · reserve · shrink_to_fit · swap · delete · clear/print
|
- [Core storage](#core-storage)
|
||||||
- [Validation & accessors](#validation--accessors) — __null_check · capacity/max_size/data · at · begin/end · front/back/empty
|
- [Validation & accessors](#validation--accessors)
|
||||||
- [Algorithms](#algorithms) — erase · resize · assign · insert
|
- [Algorithms](#algorithms)
|
||||||
- [Support](#support) — utils · char.h · project
|
- [Support](#support)
|
||||||
- [Fixed](#fixed)
|
- [Fixed](#fixed)
|
||||||
- [Conventions](#conventions)
|
- [ID map](#id-map)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<a id="triage"></a>
|
<a id="triage"></a>
|
||||||
|
|
||||||
## Triage — the 4 open S1s
|
## Triage — the 8 open S1s
|
||||||
|
|
||||||
In suggested fixing order.
|
In suggested fixing order.
|
||||||
|
|
||||||
| # | ID | Location | Summary |
|
| # | 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)` |
|
| 1 | `IV-5` | char.c:40-44 | `__null_check`'s NULL-`status` branch derefs the very NULL it just tested, and seven call sites pass the caller's `err` through here → `SEGV` at 42. **ASan Pass 12: SEGV in `__null_check`** |
|
||||||
| 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) |
|
| 2 | `FB-1` | char.c:457-462, 475-480 | `front`/`back` guard inverted: populated vectors return 0 + `ERR_OUT_OF_BOUNDS`; drained vectors fall through to `*nullptr` → SEGV at 462/480. `back` probes index 0 |
|
||||||
| 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 |
|
| 3 | `IN-10` | char.c:886-893 | amount-insert never validates: guard at 887 reads a fresh local (always 0), NULL `ptr` derefs at 893 → SEGV |
|
||||||
| 4 | `DL-2` | char.h:11-17 | no ownership marker, by-value struct copy double-frees |
|
| 4 | `ER-4` | char.c:541-542 | range-erase zeroing `memset` length is the whole `_internal_size`, not `diff`, so it writes past the buffer → `heap-buffer-overflow` WRITE at 541 |
|
||||||
|
| 5 | `RZ-10` | char.c:626-629, 712-715 | resize-shrink `memset` length is the whole old `_internal_size`, not `old - new` → `heap-buffer-overflow` WRITE at 628/714 |
|
||||||
### Reading notes
|
| 6 | `RZ-11` | char.c:630-634, 716-720 | resize-growth `memset` length is `_internal_arr[size]` (an element value) instead of `size - _internal_size`; reads one past the block when `size == capacity` → `heap-buffer-overflow` READ at 633/719 |
|
||||||
|
| 7 | `RS-1` | char.c:264-269 | truncation zeroed through the post-realloc pointer: `memset(&arr[capacity], 0, size - capacity)` (267) writes past the shrunk block → `heap-buffer-overflow` WRITE at 267 |
|
||||||
> **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.
|
| 8 | `DL-2` | char.h:8-14 | no ownership marker, by-value struct copy double-frees |
|
||||||
|
|
||||||
> **`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.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
<a id="findings"></a>
|
||||||
|
|
||||||
## Findings
|
## Findings
|
||||||
|
|
||||||
<a id="core-storage"></a>
|
<a id="core-storage"></a>
|
||||||
|
|
||||||
### Core storage
|
### Core storage
|
||||||
|
|
||||||
<a id="pb"></a>
|
#### `_vec_char_push_back` / `_vec_char_pop_back` — char.c:141, 202
|
||||||
|
|
||||||
#### `_vec_char_push_back` / `_vec_char_pop_back` — char.c:120, 181
|
|
||||||
|
|
||||||
*1 S2*
|
*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 |
|
| 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 |
|
| `PB-9` | S2 | char.c:161-173 vs 242-254 vs 839-853 | three blank-materialize paths, no shared shape. `push_back`: `malloc(1)`, `capacity=1`, `size=0`, writes `arr[0]`. `reserve`: `calloc(capacity)` and commits `capacity`. `insert` one-arg: `malloc(1)`, `capacity=1`, `size=1`, writes `arr[0]`. Same input state, three allocations, three struct results; `insert` and `resize` disagree with `push_back` on which blank states are accepted |
|
||||||
|
|
||||||
<a id="cpy"></a>
|
#### `_vec_char_cpy` — char.c:103 — *Clear*
|
||||||
|
|
||||||
#### `_vec_char_cpy` — char.c:82
|
#### `_vec_char_reserve` — char.c:221
|
||||||
|
|
||||||
*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*
|
*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 |
|
| 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 |
|
| `RS-1` | S1 | char.c:264-269 | `ptr->_internal_arr = res` (264) 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)` (267-268) writes past it. **ASan Pass 12: `heap-buffer-overflow` WRITE at 267** — push 4, `reserve(&v, 2)`. 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:277 — *Clear*
|
||||||
|
|
||||||
#### `_vec_char_shrink_to_fit` — char.c:256
|
#### `_vec_char_swap` — char.c:295 — *Clear*
|
||||||
|
|
||||||
*none open*
|
#### `_vec_char_delete` — char.c:364
|
||||||
|
|
||||||
> 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*
|
*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 |
|
| 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)` |
|
| `DL-2` | S1 | char.h:8-14 | 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 12: `attempting double-free` at char.c:376** — `w = v; delete(&v); delete(&w)` |
|
||||||
|
|
||||||
<a id="clear"></a>
|
#### `_vec_char_clear` / `_vec_char_print_vec` — char.c:383, 89 — *Clear*
|
||||||
|
|
||||||
#### `_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>
|
<a id="validation--accessors"></a>
|
||||||
|
|
||||||
### Validation & accessors
|
### 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).
|
#### `__null_check` / `__origin_override` — char.c:37, 69
|
||||||
|
|
||||||
<a id="accessors"></a>
|
*1 S1*
|
||||||
|
|
||||||
#### `_vec_char_capacity` / `_vec_char_max_size` / `_vec_char_data` — char.c:304, 317, 291
|
| ID | Sev | Location | Finding |
|
||||||
|
|:---|:--:|:---------|:--------|
|
||||||
|
| `IV-5` | S1 | char.c:40-44 | the `status == nullptr` branch exists to catch a NULL `err_t*` but dereferences `status` on the next line (`status->flags = ...`, 42; `status->origin = ...`, 43). Seven call sites hand `__null_check` the caller's `err` verbatim (print_vec 92, pop_back 205, begin 425, end 438, front 451, back 469, empty 487), so any of those with a NULL `err` is a live null-deref. **ASan Pass 12: SEGV in `__null_check` at char.c:42.** Either return before the deref or drop the branch |
|
||||||
|
|
||||||
*none open*
|
#### `_vec_char_capacity` / `_vec_char_max_size` / `_vec_char_data` / `_vec_char_size` — char.c:338, 351, 312, 325
|
||||||
|
|
||||||
> 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.
|
*1 C*
|
||||||
|
|
||||||
<a id="at"></a>
|
| ID | Sev | Location | Finding |
|
||||||
|
|:---|:--:|:---------|:--------|
|
||||||
|
| `AC-3` | C | char.c:326-335 | new `size` accessor records its failure origin as `"CAPACITY"` (331), copied from `_vec_char_capacity`. It also takes a non-`const` `_VecCharInternal*` (326) while only reading, unlike its `const` siblings |
|
||||||
|
|
||||||
#### `_vec_char_at` — char.c:366
|
#### `_vec_char_at` — char.c:400
|
||||||
|
|
||||||
*1 S3 · 1 C*
|
*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:410-417 | the OOB path sets `err` (flags + origin AT) and returns `nullptr`, but still prints a raw line to **stdout** via `printf` (412-413), polluting program output and bypassing the error channel. The residual hazard moved to the callers: `front`/`back` dereference the `nullptr` (462, 480) → SEGV (FB-1) |
|
||||||
|
| `AT-3` | C | char.c:400, 418 | `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:421, 434
|
||||||
|
|
||||||
|
*1 S2*
|
||||||
|
|
||||||
| ID | Sev | Location | Finding |
|
| 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) |
|
| `BE-1` | S2 | char.c:421-432, 434-445, 749-750, 799-800 | `begin` returns raw `status.flags` (bitmask 2/4) as an index on failure (429), and `end`'s `(size_t)(size - 1)` (444) is **reachable for a drained vector** — `size == 0` wraps to `SIZE_MAX` (harness Pass 12: 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 749-750, 799-800, so `assign` into a drained destination erases `[0, 0)` and fails with `ERR_OUT_OF_BOUNDS` (AS-1) |
|
||||||
| `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_front` / `_vec_char_back` / `_vec_char_empty` — char.c:447, 465, 483
|
||||||
|
|
||||||
#### `_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*
|
*1 S1 · 1 S2*
|
||||||
|
|
||||||
| ID | Sev | Location | Finding |
|
| 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 |
|
| `FB-1` | S1 | char.c:457-462, 475-480 | the 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 12: 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)` (462), `back` → `*_vec_char_at(ptr, size - 1, err)` (480) — **SEGV** (ASan Pass 12: SEGV at 462 and 480). `back` also probes index 0 (475) instead of `size - 1`. 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** |
|
| `AC-2` | S2 | char.c:483-498 | `empty()` still returns 0 in *both* directions: a drained vector takes the `size == 0` branch and `return SUCCESS` (493-496), a populated one falls through to `return status.flags`, also 0 (497) — so `if (empty(&v))` is never true for a valid vector. **Harness Pass 12: drained → 0/err 0, populated → 0/err 0** |
|
||||||
|
|
||||||
<a id="algorithms"></a>
|
<a id="algorithms"></a>
|
||||||
|
|
||||||
### Algorithms
|
### Algorithms
|
||||||
|
|
||||||
<a id="erase"></a>
|
#### `_vec_char_erase` (both overloads) — char.c:501, 520
|
||||||
|
|
||||||
#### `_vec_char_erase` (both overloads) — char.c:466, 485
|
*1 S1 · 1 S2 · 1 S3 · 1 C*
|
||||||
|
|
||||||
*1 S2 · 1 S3 · 1 C*
|
|
||||||
|
|
||||||
| ID | Sev | Location | Finding |
|
| 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-4` | S1 | char.c:541-542 | the trailing-zero `memset` starts at `_internal_size - diff` but its length is `sizeof(char) * ptr->_internal_size` (the *whole* old size, though `_internal_size` is not decremented until 543), so it writes `[size - diff, 2*size - diff)` — past the block whenever more than `diff` elements remain. Should be `diff` bytes. **ASan Pass 12: `heap-buffer-overflow` WRITE at 541** — 10 elements in a 13-byte block, `erase(&v, 0, 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-1` | S2 | char.c:532-537 | `erase` on an empty vector never no-ops: a drained vector passes the `__null_check` guard now, then the range check (`size < iter_end \|\| iter_end <= iter_start`) fires — `[0, 1)` because `0 < 1`, and even `[0, 0)` because `0 <= 0`. **Harness Pass 12: erase(v, 0, 1) on drained → err=16, origin ERASE** |
|
||||||
| `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) |
|
| `ER-3` | S3 | char.c:511 | the single-arg overload silently returns on `iter >= size` with `err` untouched (harness Pass 12: erase index 99 → 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:500, 519 | two stale `// TODO: Fix return value` comments remain (the second doubled as `// // TODO`) |
|
||||||
|
|
||||||
<a id="resize"></a>
|
#### `_vec_char_resize` (both overloads) — char.c:547, 639
|
||||||
|
|
||||||
#### `_vec_char_resize` (both overloads) — char.c:516, 613
|
*2 S1 · 2 S2 · 2 S3 · 3 C*
|
||||||
|
|
||||||
*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 |
|
| 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-10` | S1 | char.c:626-629, 712-715 | shrink zeroing `memset(&arr[size], 0, ptr->_internal_size)` uses the *whole old* `_internal_size` as the length instead of `old - new`, so it writes past the block whenever `0 < size < old`. Should be `_internal_size - size` bytes. **ASan Pass 12: `heap-buffer-overflow` WRITE at 628** — 5 elements in a 6-byte block, `resize(&v, 2)` (also 714 for the 3-arg overload) |
|
||||||
| `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-11` | S1 | char.c:630-634, 716-720 | growth fill `memset(&arr[_internal_size], 0, ptr->_internal_arr[size])` passes `_internal_arr[size]` — an *element value* (or 0xbe uninitialized) — as the byte count instead of `size - _internal_size`, and when `size == _internal_capacity` that read is one past the block. **ASan Pass 12: `heap-buffer-overflow` READ at 633** — 5 elements in a 6-byte block, `resize(&v, 6)` (also 719 for the 3-arg overload) |
|
||||||
| `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-3` | S2 | char.c:573-596, 665-688 | the `max_size` save/restore (575/594, 667/686) is pure belt-and-braces now that `reserve` leaves `max_size` alone, and looks removable. Nothing documents that |
|
||||||
| `RZ-2` | S3 | char.c:574, 671 | unguarded `tmpcp *= 2` wraps on overflow. Defensive |
|
| `RZ-1` | S2 | char.c:559-564, 651-656 | `size == _internal_size` returns `ERR_OUT_OF_BOUNDS` for a legitimate no-op; the code is also semantically wrong |
|
||||||
| `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-4` | S3 | char.c:594-596 vs 686-688 | the 3-arg blank branch fills with `val`, the 2-arg with `0`. `data()` exposes the difference |
|
||||||
| `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-2` | S3 | char.c:605, 695 | unguarded `tmpcp *= 2` wraps on overflow. Defensive |
|
||||||
| `RZ-7` | C | char.c:559-562, 597-600, 604-607, 656-659, 688-691, 695-698 | fill loops should be `memset` |
|
| `RZ-5` | C | char.c:584-589, 607-611, 618-623, 676-681, 699-703 | dead post-`reserve` capacity checks in the blank branches (584, 676) and the 2-arg growth branch (618); the 3-arg growth branch has no check at all, relying on `reserve` to be exact |
|
||||||
|
| `RZ-6` | C | char.c:547-637 vs 639-723 | two ~90-line near-identical bodies, with the growth blocks copy-pasted between them |
|
||||||
|
| `RZ-7` | C | char.c:590-593, 682-685 | the blank-branch fill loops should be `memset` |
|
||||||
|
|
||||||
<a id="assign"></a>
|
#### `_vec_char_assign` (both overloads) — char.c:725, 783
|
||||||
|
|
||||||
#### `_vec_char_assign` (both overloads) — char.c:704, 762
|
|
||||||
|
|
||||||
*4 S2 · 1 S3 · 1 C*
|
*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 |
|
| 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-1` | S2 | char.c:730-735, 749-750, 799-800 | fails on any empty destination. A **blank** destination trips the `__null_check` guard (730-735) → err=4, origin ASSIGN. A **drained** one passes the guard, then `end()` returns `SIZE_MAX` (BE-1) and `end + 1` wraps to 0 (749, 799), so `erase(0, 0)` fails the range check → err=16. 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-2` | S2 | char.c:749-778, 799-823 | 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-3` | S2 | char.c:757, 807 | 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-7` | S2 | char.c:764-770 | the `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 12: dest `{x,y}` ← src `{a,b}` → flags=16, size=0). 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-9` | S3 | char.c:788-792 | 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 799 rejects the same state downstream |
|
||||||
| `AS-6` | C | char.c:704-760 vs 762-804 | near-duplicate overloads differing only in the value source |
|
| `AS-6` | C | char.c:725-781 vs 783-825 | near-duplicate overloads differing only in the value source |
|
||||||
|
|
||||||
<a id="insert"></a>
|
#### `_vec_char_insert` (all three overloads) — char.c:827, 882, 918
|
||||||
|
|
||||||
#### `_vec_char_insert` (all three overloads) — char.c:806, 861, 897
|
|
||||||
|
|
||||||
*1 S1 · 2 S2 · 2 S3 · 2 C*
|
*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 |
|
| 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-10` | S1 | char.c:886-893 | the amount overload never calls `__null_check`: the guard at 887-892 tests a freshly-initialized local `status`, which is always 0, so it can never fire; `ptr` is then dereferenced raw at 893. Every other entry point validates first. **ASan Pass 12: SEGV at char.c:893** — `_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-1` | S2 | char.c:932-938 | 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 945 never runs). Valid insert-range never inserts. **Harness Pass 12: dest `{a,b}`, src `{Z}`, insert at 1 range [0,1) → err=16, size stays 2** |
|
||||||
| `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-4` | S2 | char.c:854-859, 884-893 | no `max_size` pre-check in the amount overload: the loop pushes until one-arg insert trips `size+1 > max_size` (854) 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-5` | S3 | char.c:899-904 | `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-6` | S3 | char.c:905 | `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-8` | C | char.c:945-955 | the loop reads `iterator->_internal_arr[i]` raw (948) 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 932 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 |
|
| `IN-9` | C | char.c:905-914 | O(amount²) loop of single inserts where one grow + `memmove` would do |
|
||||||
|
|
||||||
<a id="support"></a>
|
<a id="support"></a>
|
||||||
|
|
||||||
### Support
|
### Support
|
||||||
|
|
||||||
<a id="utils"></a>
|
|
||||||
|
|
||||||
#### `utils.h` / `utils.c`
|
#### `utils.h` / `utils.c`
|
||||||
|
|
||||||
*3 S2 · 3 C*
|
*3 S2 · 1 C*
|
||||||
|
|
||||||
| ID | Sev | Location | Finding |
|
| 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-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-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-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 |
|
||||||
| `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-4` | C | utils.h:7-9, 63-65 | `static const` in a header gives every TU its own copy; `ERR_SIZE_ZERO` message mentions `< 0` on a `size_t`; `LOG`/`DEBUG` are hardwired to stdout and single-arg; `find_len` is UB for pointers; `CAPACITY` (7) and `VEC_FAULT_BITS` (8) are now referenced by nothing in the tree |
|
||||||
| `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` — *Clear*
|
||||||
|
|
||||||
#### `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
|
#### Project
|
||||||
|
|
||||||
*5 C*
|
*2 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 |
|
| 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-8` | C | char.c:72, 112 (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:27-30) — a cheap defensive check would still harden the API. IV-5 is the same hazard at the one site that *tries* to check |
|
||||||
| `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-2` | C | char.c:179, 500, 519 | mixed brace style (single-line `if` at 179 vs expanded blocks) and stale `TODO` comments at 500/519; `.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 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -303,85 +221,39 @@ In suggested fixing order.
|
|||||||
## Fixed
|
## Fixed
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><b>58 fixed IDs — no rows</b></summary>
|
<summary><b>63 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
|
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
|
RS-2 RS-3 RS-4 RS-6 RS-7 RS-8
|
||||||
IV-1 IV-3 IV-4
|
IV-1 IV-3 IV-4
|
||||||
CH-1 CH-7
|
CH-1 CH-3 CH-7
|
||||||
CR-3 CR-4 CR-6 CR-7 CR-8 CR-10 CR-11
|
CR-3 CR-4 CR-6 CR-7 CR-8 CR-10 CR-11
|
||||||
SW-1 SW-2
|
SW-1 SW-2
|
||||||
FB-2
|
FB-2
|
||||||
AC-1 DA-1
|
AC-1 DA-1
|
||||||
AT-1
|
AT-1
|
||||||
DL-1 DL-3
|
DL-1 DL-3
|
||||||
UT-3
|
UT-2 UT-3 UT-5
|
||||||
PR-1 PR-3
|
PR-1 PR-3 PR-4
|
||||||
CL-1 CL-2
|
CL-1 CL-2
|
||||||
RZ-8 RZ-9
|
RZ-8 RZ-9
|
||||||
AS-4 AS-8
|
AS-4 AS-8
|
||||||
ST-1 ST-2
|
ST-1 ST-2
|
||||||
IN-2 IN-11 IN-12
|
IN-2 IN-11 IN-12
|
||||||
FX-2 FX-3 FX-4 FX-5
|
FX-2 FX-3 FX-4 FX-5
|
||||||
|
BE-2
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Closed by owner decision (not counted fixed): `CH-9` (leading `_` intended private), `PR-7` (main not the test harness), `CH-4` (header includes), `CH-5` (doc comments), `CH-8` (struct naming), `PR-5` (placeholder comments), `CH-2` (const on by-value params for LSP/clarity).
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<a id="conventions"></a>
|
<a id="id-map"></a>
|
||||||
|
|
||||||
## Conventions
|
## ID map
|
||||||
|
|
||||||
<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 |
|
| Prefix | Scope | Prefix | Scope |
|
||||||
|:------:|:------|:------:|:------|
|
|:------:|:------|:------:|:------|
|
||||||
@@ -395,6 +267,4 @@ clang -std=c2x -g -fsanitize=address -Isrc -Isrc/lib \
|
|||||||
| `ER-` | erase | `CH-` | char.h |
|
| `ER-` | erase | `CH-` | char.h |
|
||||||
| `RZ-` | resize | `PR-` | project |
|
| `RZ-` | resize | `PR-` | project |
|
||||||
| `AS-` | assign | `FX-` | retired first-pass IDs |
|
| `AS-` | assign | `FX-` | retired first-pass IDs |
|
||||||
| `IN-` | insert | | |
|
| `IN-` | insert | | |
|
||||||
|
|
||||||
</details>
|
|
||||||
+17
-13
@@ -39,8 +39,10 @@ __null_check(const _VecCharInternal* ptr, err_t* status)
|
|||||||
{
|
{
|
||||||
if (status == nullptr)
|
if (status == nullptr)
|
||||||
{
|
{
|
||||||
status->flags = ERR_ERR_T_NULL;
|
err_t err = ERR_T_INIT;
|
||||||
status->origin = "NULL_CHECK";
|
err.flags = ERR_ERR_T_NULL;
|
||||||
|
err.origin = "NULL_CHECK";
|
||||||
|
*status = err;
|
||||||
return ERR_ERR_T_NULL;
|
return ERR_ERR_T_NULL;
|
||||||
}
|
}
|
||||||
if (ptr == nullptr)
|
if (ptr == nullptr)
|
||||||
@@ -176,7 +178,10 @@ _vec_char_push_back(_VecCharInternal* ptr, const char val, err_t* err)
|
|||||||
{
|
{
|
||||||
size_t dst_cap =
|
size_t dst_cap =
|
||||||
(ptr->_internal_capacity / 2) + ptr->_internal_capacity;
|
(ptr->_internal_capacity / 2) + ptr->_internal_capacity;
|
||||||
if (dst_cap == ptr->_internal_capacity) dst_cap++;
|
if (dst_cap == ptr->_internal_capacity)
|
||||||
|
{
|
||||||
|
dst_cap++;
|
||||||
|
}
|
||||||
if (dst_cap > ptr->_internal_max_size)
|
if (dst_cap > ptr->_internal_max_size)
|
||||||
{
|
{
|
||||||
dst_cap = ptr->_internal_max_size;
|
dst_cap = ptr->_internal_max_size;
|
||||||
@@ -323,12 +328,12 @@ _vec_char_data(const _VecCharInternal* ptr, err_t* err)
|
|||||||
}
|
}
|
||||||
|
|
||||||
size_t
|
size_t
|
||||||
_vec_char_size(_VecCharInternal* ptr, err_t* err)
|
_vec_char_size(const _VecCharInternal* ptr, err_t* err)
|
||||||
{
|
{
|
||||||
err_t status = ERR_T_INIT;
|
err_t status = ERR_T_INIT;
|
||||||
if (__null_check(ptr, &status) != 0)
|
if (__null_check(ptr, &status) != 0)
|
||||||
{
|
{
|
||||||
__origin_override(err, "CAPACITY");
|
__origin_override(err, "SIZE");
|
||||||
err->flags |= status.flags;
|
err->flags |= status.flags;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -409,8 +414,6 @@ _vec_char_at(const _VecCharInternal* ptr, const size_t idx, err_t* err)
|
|||||||
}
|
}
|
||||||
if (idx >= ptr->_internal_size)
|
if (idx >= ptr->_internal_size)
|
||||||
{
|
{
|
||||||
printf("At Index: [%zu] is out of bounds for size: [%zu]\n",
|
|
||||||
idx, ptr->_internal_size);
|
|
||||||
__origin_override(err, "AT");
|
__origin_override(err, "AT");
|
||||||
err->flags |= ERR_OUT_OF_BOUNDS;
|
err->flags |= ERR_OUT_OF_BOUNDS;
|
||||||
return nullptr;
|
return nullptr;
|
||||||
@@ -454,7 +457,7 @@ _vec_char_front(const _VecCharInternal* ptr, err_t* err)
|
|||||||
err->flags |= status.flags;
|
err->flags |= status.flags;
|
||||||
return status.flags;
|
return status.flags;
|
||||||
}
|
}
|
||||||
if (_vec_char_at(ptr, 0, err) != nullptr)
|
if (_vec_char_at(ptr, 0, err) == nullptr)
|
||||||
{
|
{
|
||||||
err->flags |= ERR_OUT_OF_BOUNDS;
|
err->flags |= ERR_OUT_OF_BOUNDS;
|
||||||
return 0;
|
return 0;
|
||||||
@@ -472,7 +475,7 @@ _vec_char_back(const _VecCharInternal* ptr, err_t* err)
|
|||||||
err->flags |= status.flags;
|
err->flags |= status.flags;
|
||||||
return status.flags;
|
return status.flags;
|
||||||
}
|
}
|
||||||
if (_vec_char_at(ptr, 0, err) != nullptr)
|
if (_vec_char_at(ptr, 0, err) == nullptr)
|
||||||
{
|
{
|
||||||
err->flags |= ERR_OUT_OF_BOUNDS;
|
err->flags |= ERR_OUT_OF_BOUNDS;
|
||||||
return 0;
|
return 0;
|
||||||
@@ -492,12 +495,11 @@ _vec_char_empty(const _VecCharInternal* ptr, err_t* err)
|
|||||||
}
|
}
|
||||||
if (ptr->_internal_size == 0)
|
if (ptr->_internal_size == 0)
|
||||||
{
|
{
|
||||||
return SUCCESS;
|
return ptr->_internal_size;
|
||||||
}
|
}
|
||||||
return status.flags;
|
return status.flags;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Fix return value
|
|
||||||
__attribute__((overloadable)) void
|
__attribute__((overloadable)) void
|
||||||
_vec_char_erase(_VecCharInternal* ptr, const size_t iter, err_t* err)
|
_vec_char_erase(_VecCharInternal* ptr, const size_t iter, err_t* err)
|
||||||
{
|
{
|
||||||
@@ -508,7 +510,10 @@ _vec_char_erase(_VecCharInternal* ptr, const size_t iter, err_t* err)
|
|||||||
err->flags |= status.flags;
|
err->flags |= status.flags;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (iter >= ptr->_internal_size) return;
|
if (iter >= ptr->_internal_size)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
memmove(&ptr->_internal_arr[iter], &ptr->_internal_arr[iter + 1],
|
memmove(&ptr->_internal_arr[iter], &ptr->_internal_arr[iter + 1],
|
||||||
(ptr->_internal_size - iter - 1) * sizeof(char));
|
(ptr->_internal_size - iter - 1) * sizeof(char));
|
||||||
ptr->_internal_arr[ptr->_internal_size - 1] = 0;
|
ptr->_internal_arr[ptr->_internal_size - 1] = 0;
|
||||||
@@ -516,7 +521,6 @@ _vec_char_erase(_VecCharInternal* ptr, const size_t iter, err_t* err)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// // TODO: Fix return value
|
|
||||||
__attribute__((overloadable)) void
|
__attribute__((overloadable)) void
|
||||||
_vec_char_erase(_VecCharInternal* ptr, const size_t iter_start,
|
_vec_char_erase(_VecCharInternal* ptr, const size_t iter_start,
|
||||||
const size_t iter_end, err_t* err)
|
const size_t iter_end, err_t* err)
|
||||||
|
|||||||
+1
-1
@@ -38,7 +38,7 @@ const char*
|
|||||||
_vec_char_data(const _VecCharInternal* ptr, err_t* err);
|
_vec_char_data(const _VecCharInternal* ptr, err_t* err);
|
||||||
|
|
||||||
size_t
|
size_t
|
||||||
_vec_char_size(_VecCharInternal* ptr, err_t* err);
|
_vec_char_size(const _VecCharInternal* ptr, err_t* err);
|
||||||
|
|
||||||
size_t
|
size_t
|
||||||
_vec_char_capacity(const _VecCharInternal* ptr, err_t* err);
|
_vec_char_capacity(const _VecCharInternal* ptr, err_t* err);
|
||||||
|
|||||||
Reference in New Issue
Block a user