Repository navigation
fix: size the storage in the initializer list constructor - #74
Merged
Merged
Conversation
The constructor derived m_size from the length of the data but never sized
m_data, and it validated that length only against size_from_data_length —
which accepts any triangular length, not the one the type requires. Three
ways to break it, all confirmed under ASan:
DDScalar<2, double, Dynamic>{1, 2, 3, 4, 5, 6}
// SEGV — m_data is an empty vector when std::copy writes into it
DDScalar<2, double, 3>{1, ..., 15}
// stack-buffer-overflow — 15 doubles into a std::array<double, 10>
DDScalar<2, double, 3>{1, 2, 3}
// silently constructed, 7 of 10 slots uninitialized, size() reports 3
// while m_size is 1 — h(2, 2) returned 3.26e-314
The static cases pass size_from_data_length because 15 and 3 are valid
triangular lengths for size 4 and size 1 respectively; they are just not
valid for size 3. check_valid_size compares against TSize and catches both.
For the dynamic variant, assign() sizes the vector from the data. Deriving
m_size from the length only ever made sense for that variant — for static
types m_size is unused — so the intent was clearly to support it.
Not reachable from Python: the bindings construct from data via create(),
which validated the length already.
This was referenced Jul 26, 2026
oberbichler
added a commit
that referenced
this pull request
Jul 26, 2026
Three memory-safety bugs in a row were found by running the tests under
AddressSanitizer by hand. This makes that part of CI.
Two options on the test target, both off by default:
- HYPERJET_SANITIZE builds with -fsanitize=address,undefined and
-fno-sanitize-recover=all, so undefined behaviour aborts instead of
printing and passing.
- HYPERJET_WERROR promotes warnings to errors. Warnings themselves
(-Wall -Wextra -Wpedantic, /W4) are always on, so they are visible
locally too, but only one CI job fails on them — warning sets differ
between compilers, and an unrelated toolchain should not be able to
block every build.
The new job runs both on clang, which also covers a third front-end next
to GCC 14 and MSVC. Eigen and doctest are now included as SYSTEM headers so
third-party warnings cannot fail our build.
Verified that the job is not decorative: with a reproducer for the
initializer list bug still open in #74, it fails as intended —
test case CRASHED: SIGSEGV - Segmentation violation signal
==1413==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000
#7 DDScalar<2l, double, -1l>::DDScalar(std::initializer_list<double>)
hyperjet.h:314
Warnings that -Werror surfaced:
- SScalar::hypot kept f3, a2, b2 and c2 from the DDScalar version, where
they carry the second-order terms. SScalar is first order only, so they
were dead.
- DDScalar(const Data&, index) initialized m_data before m_size while the
declaration order is the reverse (-Wreorder-ctor). Harmless today, since
the vector constructor does not read m_size, but it is a warning inside
a header shipped to users.
Neither of those warnings appeared before, because nothing instantiated the
functions: the tests never touched SScalar::hypot or a dynamic DDScalar. A
warnings job is only as good as the instantiation coverage, so this adds the
missing SScalar::hypot test case — which is what surfaced the dead
variables. The dynamic constructor gets covered by the tests in #73.
Also adds <algorithm> and <stdexcept>, which the header used through
transitive includes only. Included here because the new job compiles with a
front-end the project has not built with before, and missing includes are
exactly what breaks first under a new toolchain.
Workflow-level permissions: contents: read while in this file. The other
three workflows still need the same.
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
m_sizeis derived from the length of the data, butm_datais never sized — and the length is validated only againstsize_from_data_length, which accepts any triangular length, not the one the type actually requires. Three ways to break it, all confirmed under AddressSanitizer:The two static cases get past
size_from_data_lengthbecause 15 and 3 are valid triangular lengths — for size 4 and size 1 respectively. They are simply not valid for size 3, and nothing compared them againstTSize.In a release build the dynamic case traps immediately (
exit 133); under ASan it is a null write.Fix
check_valid_sizealready exists and does exactly the right thing per variant: compare againstTSizefor static types, reject a negative size for dynamic ones.assignsizes the vector from the data.Making the dynamic variant work rather than constraining the constructor away seems right: deriving
m_sizefrom the data length only ever made sense for the dynamic variant — for static typesm_sizeis unused, sincesize()returnsTSize— so supporting it was clearly the intent.Tests
TEST_CASE("Initializer list construction"), written first. RED was the test binary dying with exit 133 (SIGTRAP) and no doctest output at all, because the dynamic construction on the first line trapped before anything could be reported.Covered: the dynamic variant for order 1 and 2 (size, data length, and every value read back through
f/g/h), the valid static length, both invalid-but-triangular static lengths, and a non-triangular length as a guard on the pre-existing check.After: C++ 41/41 test cases, 415 assertions in both Debug and Release · Python 147 passed ·
clang-format --Werrorandruff format --checkclean · all three cases re-verified under ASan — dynamic constructs correctly, both static mismatches throwstd::runtime_error: Invalid size, no sanitizer reports.The existing
dd1/dd2/dd3globals intest.cppandbenchmarks.cppuse exactly 10 elements forDDScalar<2, double, 3>, so they exercise the valid path and would have failed had the length check been wrong.Notes
Not reachable from Python — the bindings construct from data via
create(), which validated the length already.Independent of #73 (still open); the two touch different parts of the header and merge in either order.
This is the third memory-safety bug in a row that an ASan/UBSan job would have caught before merge. That is item 7 on the review list and probably worth doing before the remaining fixes.
Still open from the review:
x *= xandx /= xproduce wrong derivatives (self-aliasing) in bothDDScalarandSScalar.resize()silently scrambles the Hessian layout for order 2;pad_right()does it correctly.