Skip to content

Commit 61c1446

Browse files
feat(pool): add opt-in debug hardening — poisoning, guard word, safe-linking
Harden the intrusive free list against the classic use-after-free / pointer-corruption primitives an in-band next-link exposes (ADR-0009 §1), behind a single compile-time knob `PBR_MEMORY_POOL_HARDENING` (OFF by default; a `harden` CMake preset turns it on). Three layered protections: - Freed-block poisoning (0xDE), verified on the next allocation — use-after-free detection. - A per-slot trailing guard word: neither-constant on free is a buffer overflow past block_size, still-freed is a double-free (closing the ADR-0012 gap). The guard lives in *added* slot stride, so the user-visible block_size and the ADR-0009 alignment guarantee are unchanged. - glibc-style free-list safe-linking (ptr XOR (slot_addr >> 12)): a leaked/overwritten next-link is neither directly usable nor silently followed; corruption surfaces as an alignment fault on reveal. On detection a swappable HardeningViolationHandler fires; the default prints a diagnostic and abort()s (the ADR-0012 defined-loud-failure stance), and tests install a recording handler to assert detection without terminating the process. The knob is fully compiled out when off, so the default build is byte-for-byte and cycle-for-cycle unchanged (read_next/write_next/ reveal_next/slot_stride inline to the exact prior load/store). Works with fixed and dynamic pools across all three thread-safety policies; composes under the InstrumentedPool decorator. Purely additive and ABI-compatible (SemVer MINOR, a v1.2.0 candidate); a hardened build is deliberately not layout-compatible with a non-hardened one. A `harden` CI matrix cell builds and runs the detection tests on each Tier-1 platform (the memory-safety net where ASan is unavailable, e.g. MSVC). Decided in ADR-0043; ROADMAP item 9.2. Closes #109. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent d4afec5 commit 61c1446

16 files changed

Lines changed: 803 additions & 44 deletions

File tree

.github/workflows/ci.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,22 @@ jobs:
6464
- { os: ubuntu-24.04, compiler: clang, preset: release }
6565
- { os: ubuntu-24.04, compiler: clang, preset: asan }
6666
- { os: ubuntu-24.04, compiler: clang, preset: ubsan }
67+
# Opt-in debug hardening (ADR-0043) — cross-platform (no sanitizer
68+
# flags); builds the hardened configuration and runs its detection
69+
# tests, the "clean in both configurations" acceptance for #109.
70+
- { os: ubuntu-24.04, compiler: gcc, preset: harden }
71+
- { os: ubuntu-24.04, compiler: clang, preset: harden }
6772
# Windows x86_64 — sanitizer presets are POSIX-only.
6873
- { os: windows-2022, compiler: msvc, preset: debug }
6974
- { os: windows-2022, compiler: msvc, preset: release }
75+
# MSVC has no ASan in this matrix, so hardening is the memory-safety net here.
76+
- { os: windows-2022, compiler: msvc, preset: harden }
7077
# macOS arm64
7178
- { os: macos-14, compiler: apple-clang, preset: debug }
7279
- { os: macos-14, compiler: apple-clang, preset: release }
7380
- { os: macos-14, compiler: apple-clang, preset: asan }
7481
- { os: macos-14, compiler: apple-clang, preset: ubsan }
82+
- { os: macos-14, compiler: apple-clang, preset: harden }
7583
steps:
7684
- name: Check out the source tree
7785
uses: actions/checkout@v6

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,24 @@ dated version block (`## [X.Y.Z] — YYYY-MM-DD`) when a release PR closes a mil
2020

2121
### Added
2222

23+
- **Opt-in debug hardening — freed-block poisoning, a guard word, and free-list
24+
safe-linking.** A compile-time knob `PBR_MEMORY_POOL_HARDENING` (OFF by default; a `harden`
25+
CMake preset turns it on) hardens the intrusive free list against the classic
26+
use-after-free / pointer-corruption primitives an in-band next-link exposes: freed blocks are
27+
**poisoned** (`0xDE`) and verified on the next allocation (use-after-free), a per-slot
28+
trailing **guard word** detects a contiguous write past `block_size` (buffer overflow) and a
29+
repeated free (double-free — the [ADR-0012](docs/adr/0012-foreign-pointer-and-out-of-range-pointer-policy.md)
30+
gap), and the next-link is stored with glibc-style **safe-linking**
31+
(`ptr XOR (slot_addr >> 12)`) so a leaked/overwritten link is neither usable nor silently
32+
followed. On detection a swappable `HardeningViolationHandler`
33+
([`pool_hardening.hpp`](src/main/cpp/it/d4np/memorypool/pool_hardening.hpp)) fires — the
34+
default prints a diagnostic and `abort()`s. The guard lives in *added* slot stride, so the
35+
user-visible `block_size` and the [ADR-0009](docs/adr/0009-free-list-layout-block-size-constraints-and-alignment-guarantee.md)
36+
alignment guarantee are unchanged, and the default build is byte-for-byte and cycle-for-cycle
37+
unchanged (the knob is fully compiled out). Works with fixed and dynamic pools across all
38+
three thread-safety policies. Purely additive and ABI-compatible; a hardened build is
39+
deliberately not layout-compatible with a non-hardened one (never mix configurations).
40+
[ADR-0043](docs/adr/0043-opt-in-debug-hardening.md). Closes #109.
2341
- **`std::pmr::memory_resource` adapter — `PoolMemoryResource`.** A new header-only Adapter
2442
([`pool_memory_resource.hpp`](src/main/cpp/it/d4np/memorypool/pool_memory_resource.hpp))
2543
binds one `Pool` behind the runtime `std::pmr::memory_resource` interface, so a single

CMakeLists.txt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,21 @@ if(PBR_MEMORY_POOL_ENABLE_DIAGNOSTICS)
119119
target_compile_definitions(pbr_memory_pool PUBLIC PBR_MEMORY_POOL_DIAGNOSTICS=1)
120120
endif()
121121

122+
# ---------------------------------------------------------------------------
123+
# Opt-in debug hardening (ADR-0043). When ON, the free list gains freed-block
124+
# poisoning, a per-slot guard word (buffer-overflow + double-free detection),
125+
# and next-pointer safe-linking (see pool_hardening.hpp). OFF by default — a
126+
# release build is byte-for-byte unchanged. Defined PUBLIC because a hardened
127+
# build changes the on-disk free-list encoding AND the physical slot stride, so
128+
# the library and every consumer / test linking it must agree; never mix a
129+
# hardened and a non-hardened build.
130+
# ---------------------------------------------------------------------------
131+
option(PBR_MEMORY_POOL_HARDENING
132+
"Enable opt-in debug hardening: poisoning, guard word, free-list safe-linking (ADR-0043)" OFF)
133+
if(PBR_MEMORY_POOL_HARDENING)
134+
target_compile_definitions(pbr_memory_pool PUBLIC PBR_MEMORY_POOL_HARDENING=1)
135+
endif()
136+
122137
# ---------------------------------------------------------------------------
123138
# Thread-safety policy (ADR-0020). Selects how the implementation
124139
# synchronizes the free-list head, fixed library-wide at build time. The

CMakePresets.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,15 @@
8585
"rhs": "Windows"
8686
}
8787
},
88+
{
89+
"name": "harden",
90+
"inherits": "debug",
91+
"displayName": "Debug + Hardening",
92+
"description": "Debug + opt-in debug hardening: freed-block poisoning, per-slot guard word (overflow + double-free), and free-list safe-linking (ADR-0043). Cross-platform (no sanitizer flags).",
93+
"cacheVariables": {
94+
"PBR_MEMORY_POOL_HARDENING": "ON"
95+
}
96+
},
8897
{
8998
"name": "bench",
9099
"displayName": "Benchmark",
@@ -104,13 +113,15 @@
104113
{"name": "asan", "configurePreset": "asan"},
105114
{"name": "ubsan", "configurePreset": "ubsan"},
106115
{"name": "tsan", "configurePreset": "tsan"},
116+
{"name": "harden", "configurePreset": "harden"},
107117
{"name": "bench", "configurePreset": "bench"}
108118
],
109119
"testPresets": [
110120
{"name": "debug", "configurePreset": "debug", "output": {"outputOnFailure": true}, "execution": {"jobs": 0}},
111121
{"name": "release", "configurePreset": "release", "output": {"outputOnFailure": true}, "execution": {"jobs": 0}},
112122
{"name": "asan", "configurePreset": "asan", "output": {"outputOnFailure": true}, "execution": {"jobs": 0}},
113123
{"name": "ubsan", "configurePreset": "ubsan", "output": {"outputOnFailure": true}, "execution": {"jobs": 0}},
114-
{"name": "tsan", "configurePreset": "tsan", "output": {"outputOnFailure": true}, "execution": {"jobs": 0}}
124+
{"name": "tsan", "configurePreset": "tsan", "output": {"outputOnFailure": true}, "execution": {"jobs": 0}},
125+
{"name": "harden", "configurePreset": "harden", "output": {"outputOnFailure": true}, "execution": {"jobs": 0}}
115126
]
116127
}

ROADMAP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ Goal: *post-`v1.0.0`*, stand up a **modular, professional documentation-translat
137137
Goal: a coherent, post-`v1.0.0` wave of **additive, ABI-compatible** capabilities on top of the frozen public surface — richer standard-library interop, opt-in memory-safety hardening, a fuzzing harness, and a broader benchmark baseline. Every item is additive, so the milestone targets `v1.2.0` (SemVer `MINOR`); each ships independently under the §6.1 one-PR-at-a-time rule, and closing the milestone is the `v1.2.0` bump ([ADR-0037](docs/adr/0037-new-feature-roadmap-placement.md)). Each item comes from a tracking issue.
138138

139139
- [x] 9.1 **`std::pmr::memory_resource` adapter** (`PoolMemoryResource`) — the "door left open" in [ADR-0018](docs/adr/0018-stl-allocator-adapter.md): a `std::pmr::memory_resource` subclass binding one `Pool` so any `std::pmr`-aware container can draw from it through `std::pmr::polymorphic_allocator`, without the `PoolAllocator<T>` per-type rebind. Deterministic `(bytes, alignment)` routing to the bound pool — over-sized / over-aligned requests delegate to a configurable upstream resource, and exhaustion of a pool-eligible request throws `std::bad_alloc` rather than falling back (preserving the deterministic deallocate routing) — with `is_equal` by `(pool, upstream)` identity, gated behind `PBR_MEMORY_POOL_HAS_PMR` where `<memory_resource>` is available. Header-only, additive, ABI-compatible. Decided in [ADR-0042](docs/adr/0042-pmr-memory-resource-adapter.md); implemented in [`pool_memory_resource.hpp`](src/main/cpp/it/d4np/memorypool/pool_memory_resource.hpp) with [`pool_memory_resource_test.cpp`](src/test/cpp/it/d4np/memorypool/pool_memory_resource_test.cpp) (issue #107).
140-
- [ ] 9.2 **Opt-in debug hardening** — freed-block poisoning, canaries, and free-list safe-linking (which also yields double-free detection); zero cost when the gate is off (issue #109).
140+
- [x] 9.2 **Opt-in debug hardening** — freed-block poisoning, canaries, and free-list safe-linking (which also yields double-free detection); zero cost when the gate is off (issue #109). Decided in [ADR-0043](docs/adr/0043-opt-in-debug-hardening.md); implemented in [`memory_pool.cpp`](src/main/cpp/it/d4np/memorypool/memory_pool.cpp) behind the compile-time `PBR_MEMORY_POOL_HARDENING` knob (a `harden` CMake preset), with the swappable violation-handler surface in [`pool_hardening.hpp`](src/main/cpp/it/d4np/memorypool/pool_hardening.hpp) and [`pool_hardening_test.cpp`](src/test/cpp/it/d4np/memorypool/pool_hardening_test.cpp) (CTest `pool_hardening`). The "canary" is realized as one trailing **guard word** living in *added* slot stride — so the user-visible `block_size` and the ADR-0009 alignment guarantee are unchanged and the default build is byte-for-byte unchanged (the mechanism is fully compiled out). Poisoning (`0xDE`) catches use-after-free on the next allocation, the guard word catches a write past `block_size` and a double-free, and glibc-style safe-linking (`ptr XOR (slot_addr >> 12)`) protects the in-band next-link. A `harden` CI matrix cell builds and tests the hardened configuration on each Tier-1 platform. Works with fixed and dynamic pools across all three thread-safety policies.
141141
- [ ] 9.3 **Coverage-guided fuzzing harness** for the pool surface — a libFuzzer target under `src/fuzz/`, time-boxed in CI (issue #108).
142142
- [ ] 9.4 **Benchmark extension** — external allocator baselines (jemalloc / tcmalloc) and p99 tail-latency reporting (issue #111).
143143

SECURITY.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ This is a single-maintainer reference project, so timelines are **best-effort**,
4343

4444
In scope: memory-safety and correctness defects in the library's own code reachable through its public API or documented build options — e.g. out-of-bounds access, use-after-free, double-free, leaks, data races in the `MUTEX` / `LOCKFREE` policies, or integer-overflow in size computations.
4545

46+
For defense-in-depth and bug-finding, the library ships an **opt-in debug-hardening build** (compile-time knob `PBR_MEMORY_POOL_HARDENING`, OFF by default; [ADR-0043](docs/adr/0043-opt-in-debug-hardening.md)) that turns use-after-free, buffer-overflow-past-`block_size`, and double-free into deterministic, loud failures via freed-block poisoning, a per-slot guard word, and glibc-style free-list safe-linking. It complements the sanitizer matrix (and works where ASan does not, e.g. MSVC); it is a debugging aid, not a substitute for the sanitizers, and a hardened build is intentionally not layout-compatible with a default one.
47+
4648
Out of scope: misuse that the documentation explicitly calls undefined behaviour (e.g. pairing storage and object-lifetime verbs incorrectly, or a moved-from wrapper used after move), issues in a consumer's own code, and vulnerabilities in third-party toolchains. The default single-threaded build is intentionally not thread-safe (spec §2.4) — concurrent use of a `NONE`-policy pool is not a vulnerability.
4749

4850
## See also

0 commit comments

Comments
 (0)