Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
63 changes: 63 additions & 0 deletions docs/bugs/2026/06/BUG-0004-grow-pool-growth-size-overflow.md
Original file line number Diff line number Diff line change
@@ -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).
1 change: 1 addition & 0 deletions docs/bugs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
6 changes: 6 additions & 0 deletions src/main/cpp/it/d4np/memorypool/memory_pool.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading