Skip to content

Hardening pass: sanitizers, coverage, cross-engine oracle, perf gates#14

Merged
NiceAndPeter merged 6 commits into
mainfrom
hardening-pass
Jun 25, 2026
Merged

Hardening pass: sanitizers, coverage, cross-engine oracle, perf gates#14
NiceAndPeter merged 6 commits into
mainfrom
hardening-pass

Conversation

@NiceAndPeter

Copy link
Copy Markdown
Owner

A hardening pass on an already-mature, TODO-free codebase: the value here is in CI
coverage and regression nets, not feature work. Four independently-reviewable
commits, no production-header changes.

What's in it

ASan + UBSan in CI (a09a55c)
New BOUND_SANITIZE CMake option and a Clang-18 Debug cell running the suite +
a short fuzz run under -fsanitize=address,undefined -fno-sanitize-recover=all.
The fast paths lean on hand-rolled 128-bit intermediates, signed-overflow
detection and bit-shifts — exactly the UB UBSan catches — yet nothing exercised
them before. Clean locally under g++ sanitizers: 374 tests, 36 examples, 92M+
fuzz checks, zero findings.

Coverage job + edge tests (db9e10e)
Wires the existing -DBOUND_COVERAGE target into a dedicated job (lcov summary +
HTML artifact). Closes two reachable gaps it surfaced: the wrap-identity
full-u64-span path (apply_wrap urange==0), and a real grid rejecting
NaN/±inf with not_finite. Line coverage 98.1 → 98.2%.

Cross-engine differential oracle + freestanding sanitize (01a5886)
Enforces the documented guarantee that the cordic/dbl/flt transcendental engines
agree to within ~one output notch (the engines genuinely differ on ~10% of
inputs, measured max spread exactly one notch — so the property is non-vacuous).
Also applies the sanitizer flags to single_header_freestanding_smoke and runs
it, so the -fno-exceptions / BND_NO_STRING error path is exercised under
ASan+UBSan rather than only compiled.

Performance regression gates (f106f7c)
Two deterministic, runner-noise-immune gates (the bench harness is build-only,
so nothing caught a perf regression before):

  • Codegen guard — compiles the arithmetic fast paths at -O2 and asserts via
    objdump that the scalar add stays call-free and the loop vectorizes. Verified
    to fire on both regression modes.
  • Cachegrind instruction-count gate — runs a small, deterministic,
    BND_PERF_SCALE-scaled workload under cachegrind and fails a >5% growth vs a
    committed baseline. Bootstraps the baseline on first run (writes + passes,
    uploads it as an artifact to review/commit); active once committed. The check
    script is validated end-to-end against a fake-valgrind shim.

Kept lean to respect CI time: cachegrind runs only on its dedicated cell, on a
~1e6-op workload, with a 10-minute timeout; the sanitized fuzz is a short fixed
run while the long sweep stays on the existing heavy cells.

Notes for the reviewer

  • The cachegrind perf-baseline artifact from the first run on this branch needs
    to be downloaded and committed as tests/perf_baseline.json to make that gate
    active; until then it bootstraps and passes.
  • A planned fifth item (dedupe the assignment/core.hpp dispatch plumbing) was
    investigated and declined: dispatch_out_of_range is already the shared
    scaffold, handle_out_of_range isn't triplicated, and the apply_* kernels
    are legitimately domain-specific. The only boilerplate (the &/&&
    ref-qualifier pairs) can't be collapsed without C++23 deducing-this, which
    would break the -DBOUND_CXX20 backport. No net-positive safe refactor.
  • The valgrind and clang-18 tool invocations run in CI (no sudo locally to
    install them); the CMake/script/parser logic around them is verified.

🤖 Generated with Claude Code

Peter Neiss and others added 6 commits June 25, 2026 09:56
Add a BOUND_SANITIZE CMake option (AddressSanitizer + UndefinedBehaviorSanitizer,
-fno-sanitize-recover=all so any finding is a hard failure) and a Clang-18 Debug
Linux CI cell that builds the whole suite under it. The fast paths lean on
hand-rolled 128-bit intermediates, signed-overflow detection and bit-shifts —
exactly the UB UBSan catches — yet nothing exercised them in CI before.

The sanitized cell additionally runs the property fuzzer under UBSan as a short
fixed run (seed 1, 500 iters/property, ~90M checks, ~75s); the long sweep stays
on the existing heavy cells. Suite, examples and fuzz are all clean under g++
sanitizers locally.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wire the existing -DBOUND_COVERAGE target into CI as its own gcc-14 job that
builds everything, runs the coverage target, prints the lcov summary, and uploads
the HTML report — coverage was measurable but never tracked.

Close two reachable gaps the report surfaced:
 - wrap on a grid spanning the whole u64 range hits apply_wrap's urange==0 fast
   path (span wraps to 0 in umax), where the wrap is the identity; asserted as a
   raw round-trip in test_wrap_overflow.cpp.
 - a real (double-backed) grid rejects NaN/+-inf with errc::not_finite; pinned in
   test_real_exact.cpp as a behavioral regression.

Line coverage 98.1 -> 98.2%. The remaining lines are defensive traps (shadowed
by earlier guards) and other-engine-only branches exercised by the float/CORDIC
cells, not the default-engine coverage run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
determinism.md and the cmath engine note guarantee the cordic/dbl/flt
transcendental engines agree to within ~one output notch on a shared snap grid,
but nothing enforced it — the engine tests only pinned exact special values. Add
prop_cross_engine: a randomized sweep asserting cordic agrees with both dbl and
flt for sin/cos/tanh/atan and sqrt on [0,4]. The engines genuinely differ
(~10% of inputs, measured) with a true spread of exactly one notch, so the
property is non-vacuous; the 8-notch tolerance catches gross divergence while
absorbing legitimate ±1-notch ties and binary32 rounding. Guarded on
!BND_MATH_NO_FP (the fixed engine leaves only cordic — nothing to compare).

Also apply the BOUND_SANITIZE flags to single_header_freestanding_smoke (it
links no bound::bound, so the interface flags miss it) and run it in the
sanitizer cell, so the -fno-exceptions / BND_NO_STRING error path is exercised
under ASan+UBSan rather than only compiled.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The bench harness is build-only — a benchmark has no pass/fail — so nothing
caught a perf regression. The library's pitch is zero-overhead, autovec-friendly
arithmetic; a silent de-optimization (a fast path falling back to the rational
path, or losing vectorization) would pass CI today. Two deterministic gates,
both immune to shared-runner noise:

 * Codegen guard (tests/perf_codegen.cpp + check_codegen.sh): compiles the
   arithmetic fast paths at -O2 and asserts via objdump that the scalar add
   stays call-free (no checked/rational/error fallback inlined in) and the loop
   vectorizes (packed integer add). Verified to fire on both regression modes.
   Registered as a labelled ctest, x86-64 GNU/Clang; runs across the linux
   matrix and is pinned in a dedicated perf job.
 * Cachegrind instruction-count gate (tests/perf_workload.cpp + check_perf.py):
   runs a small, deterministic, BND_PERF_SCALE-scaled workload under cachegrind
   and fails a >5% growth in retired instructions vs tests/perf_baseline.json.
   Bootstraps the baseline on first run (writes + passes, uploads it as an
   artifact to review and commit); active once the baseline is committed. The
   check script is validated end-to-end against a fake valgrind shim.

Kept lean to respect the CI time budget: cachegrind runs only on the dedicated
perf cell, on a ~1e6-op workload, with a 10-minute timeout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The first CI run failed the codegen guard on GCC 14: it doesn't autovectorize the
loop at -O2 while GCC 15 (local) does, so asserting a packed SIMD add was
compiler-version-flaky. Demote vectorization to an informational note.

Keep the robust, compiler-stable invariant — both fast paths must be call-free —
and apply it to the loop body too. (Note: bound's rational arithmetic on
compile-time grids constexpr-folds to call-free code, so this guards mainly
against a fast path calling into the error handler / a non-inlined helper, i.e.
"no throw in the inner loop"; the cachegrind instruction-count gate remains the
fine-grained perf signal.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Captured from the perf cell's first run on this branch (gcc-14, ubuntu-24.04):
integer_qformat = 9,793,323 retired instructions (Ir). With the baseline
present, check_perf.py now gates — a >5% instruction-count regression fails CI
instead of bootstrapping. Refresh with `check_perf.py --update` when a change
legitimately moves it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@NiceAndPeter
NiceAndPeter merged commit a15792e into main Jun 25, 2026
20 checks passed
@NiceAndPeter
NiceAndPeter deleted the hardening-pass branch June 25, 2026 08:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant