diff --git a/CHANGELOG.md b/CHANGELOG.md index acebf70..1f8acec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,13 @@ dated version block (`## [X.Y.Z] — YYYY-MM-DD`) when a release PR closes a mil Move-assigning over an instrumented pool released its `Pool` and observers without notifying `PoolEvent::destroyed`, asymmetric with the destructor. It now notifies before reassignment. Header-only; no API change. +- **Overflow guard in `grow_pool` ([BUG-0004](docs/bugs/2026/06/BUG-0004-grow-pool-growth-size-overflow.md)).** + The dynamic-growth path computed `total * (grow_factor_ - 1)` before any overflow + check, so that product could wrap `size_t` and feed the downstream `block_size` + guard an already-wrapped value. Added a `would_overflow_product` guard on the + growth-count product first, mirroring the create-path guard; on overflow the pool + falls back to fixed-mode exhaustion. Latent (not runtime-reachable — RAM exhausts + first); no API change. ## Released versions diff --git a/docs/bugs/2026/06/BUG-0004-grow-pool-growth-size-overflow.md b/docs/bugs/2026/06/BUG-0004-grow-pool-growth-size-overflow.md new file mode 100644 index 0000000..c23e8d6 --- /dev/null +++ b/docs/bugs/2026/06/BUG-0004-grow-pool-growth-size-overflow.md @@ -0,0 +1,63 @@ +--- +id: BUG-0004 +title: Unguarded size_t overflow in grow_pool growth-size computation +status: fixed +severity: low +reporter: third-party +discovered: 2026-06-15 +affected-versions: ">=0.5.0,<1.1.2" +fixed-in: v1.1.2 +--- + +# BUG-0004: Unguarded size_t overflow in grow_pool growth-size computation + +## Summary + +In the dynamic-growth slow path, `grow_pool` computed +`add = total * (grow_factor_ - 1)` **before** any overflow check, then validated +`add * block_size_` with `would_overflow_product`. The first multiplication could +itself wrap `size_t`, so the subsequent guard validated an already-wrapped `add`. + +## Environment + +- **Affected versions:** `>=0.5.0,<1.1.2` (dynamic growth landed in Milestone 5). +- **Configuration:** dynamic pool (`grow_factor_ >= 2`); `NONE` or `MUTEX` + (`LOCKFREE` never grows, ADR-0024 §2, so `grow_pool` is not compiled there). + +## Reproduction + +Not reachable at runtime: `total` is the accumulated live block count, so +`total * (grow_factor_ - 1)` only overflows when `total` approaches +`SIZE_MAX / (grow_factor_ - 1)` — far more blocks than any machine can back. The +defect is found by inspection, not by a test that can actually allocate that much. + +## Expected vs. actual + +- **Expected:** every product that feeds an allocation size is overflow-checked + before use, as `memory_pool_create` does for `block_size * block_count`. +- **Actual:** `total * (grow_factor_ - 1)` was computed unchecked; only the downstream + `add * block_size_` product was guarded — against a possibly-wrapped `add`. + +## Root cause + +A missing overflow guard on the growth-count multiplication, inconsistent with the +meticulous overflow handling on the create path (ADR-0009 §3). + +## Impact + +Low / latent: RAM is exhausted long before `total` reaches the overflow boundary, so +in practice growth fails benignly first. It is a correctness/consistency gap, not a +reachable fault. + +## Fix / workaround + +Added `if (would_overflow_product(total, grow_factor_ - 1)) return false;` before +computing `add`, mirroring the create-path guard — on overflow the pool falls back to +fixed-mode exhaustion (returns `false`), exactly as for the existing guards. No test +is added because the condition is not runtime-reachable through the public API. + +## References + +- Fixing change: branch `fix/grow-pool-overflow-guard` — see the `CHANGELOG` `Fixed` entry. +- [`memory_pool.cpp`](../../../../src/main/cpp/it/d4np/memorypool/memory_pool.cpp) — `grow_pool`. +- [ADR-0009](../../../adr/0009-free-list-layout-block-size-constraints-and-alignment-guarantee.md) (overflow guards) · [ADR-0022](../../../adr/0022-dynamic-growth-policy-and-chunk-linking.md) / [ADR-0024](../../../adr/0024-dynamic-growth-synchronization-and-creation-surface.md) (dynamic growth). diff --git a/docs/bugs/README.md b/docs/bugs/README.md index 6e7f7c3..9431e26 100644 --- a/docs/bugs/README.md +++ b/docs/bugs/README.md @@ -82,3 +82,4 @@ Newest first, grouped by year and month. | [BUG-0001](2026/06/BUG-0001-instrumented-pool-growth-counter-data-race.md) | Data race on `InstrumentedPool::last_growths_` under concurrent use | `fixed` | high | 2026-06-15 | | [BUG-0002](2026/06/BUG-0002-instrumented-pool-live-counter-underflow.md) | `InstrumentedPool::deallocate` underflows `live_` on a foreign / double-freed pointer | `fixed` | medium | 2026-06-15 | | [BUG-0003](2026/06/BUG-0003-instrumented-pool-move-assign-missing-destroyed-event.md) | `InstrumentedPool` move-assignment does not notify `destroyed` for the replaced pool | `fixed` | low | 2026-06-15 | +| [BUG-0004](2026/06/BUG-0004-grow-pool-growth-size-overflow.md) | Unguarded `size_t` overflow in `grow_pool` growth-size computation | `fixed` | low | 2026-06-15 | diff --git a/src/main/cpp/it/d4np/memorypool/memory_pool.cpp b/src/main/cpp/it/d4np/memorypool/memory_pool.cpp index 5740ce0..a2e8baa 100644 --- a/src/main/cpp/it/d4np/memorypool/memory_pool.cpp +++ b/src/main/cpp/it/d4np/memorypool/memory_pool.cpp @@ -245,6 +245,12 @@ bool grow_pool(memory_pool* pool) noexcept { for (const Chunk* chunk = pool->overflow_; chunk != nullptr; chunk = chunk->next_) { total += chunk->block_count_; } + // Guard the growth-count product itself before computing it: total can be large + // after several growths, and total * (grow_factor_ - 1) could wrap size_t, + // feeding the block_size guard below an already-wrapped `add` (BUG-0004). + if (would_overflow_product(total, pool->grow_factor_ - 1U)) { + return false; + } const std::size_t add = total * (pool->grow_factor_ - 1U); if (add == 0U) { return false;