Story: Hotfix: the nightly Valgrind run reports uninitialised members and library leaks
Table of Contents
This page documents a story in Sprint 26. It captures the goal, current status, acceptance criteria, and the tasks that compose it.
1. Goal
The nightly memcheck run reports defects in four suites, and the outputs
under tmp/ are the CDash pages for them:
52/87 MemCheck: #55: ores.trading.api.tests ........... Defects: 4 58/87 MemCheck: #52: ores.refdata.core.tests .......... Defects: 3329 83/87 MemCheck: #69: ores.ore.core.tests .............. Defects: 99 85/87 MemCheck: #85: ores.analytics.core.tests ........ Defects: 214
Two different defects wear the same number.
The uninitialised-value defects are one class: a generated struct member
with no in-class initialiser. Only :default_value: reaches the member
initialiser the class template emits, and a model that declares none leaves
a default-constructed object indeterminate. Nothing detects that until a
write path reads the member, and sqlgen reads every member when it
stringifies an entity for its INSERT:
ores.analytics.core.tests:credit_simulation_matrix_config.t0andt1, and the eightp_*probabilities ofcredit_simulation_matrix_row_config, aredoublecolumns with no default. The generated generator cannot fill them, so a synthetic row reaches the INSERT with eight indeterminate members.ores.ore.core.tests: 40 of the 43 defects areyyjsonreadingbond_trs::funding_ratewhile the roundtrip test writes the payload, and 3 are the date reflector readingbond_future's two unset dates.funding_rateisnullable: truewith a baredouble, so the entity wraps it and the mapper maps zero to NULL; the domain member was indeterminate instead of zero.ores.trading.api.tests: the test builds avanilla_swap_instrumentby default-initialising it and writing it out, which reads both dates.ores.analytics.core.testsagain, forcredit_simulation_config's nullable bareboolandintmembers, which the generated entity config test builds and inserts.
The 3329 records for ores.refdata.core.tests are all still reachable,
not lost: 3254 are the coverage runtime registering every translation
unit's counters at load, and the rest are glibc's atexit handler nodes,
libpq's per-connection state (the pool lives for the process), and the
Catch2 reporter's output stream. build/valgrind/custom.supp already
carries 196 rules of exactly that kind; these categories were missing.
2. Status
| Field | Value |
|---|---|
| State | DONE |
| Parent sprint | Sprint 26 |
| Now | Nothing. |
| Waiting on | Nothing. |
| Next | Nothing. |
| Last touched | 2026-09-29 |
3. Acceptance
- The four suites report no defect under valgrind: each log holds no error and no leak record.
- Every uninitialised member is initialised by a
:default_value:its model declares, and the affected components are regenerated. org_loaderrefuses a nullable column whose:cpp_type:is a bare value type and that declares no default, so the shape that caused several of these cannot be merged again.build/valgrind/custom.suppgains a rule per library category, each naming why the allocation is still reachable rather than lost.- The full suite, the codegen suite and the catalogue sweep pass on this branch.
4. Tasks
| Task | State | Start | End | Description |
|---|---|---|---|---|
| Implement Hotfix: the nightly Valgrind run reports uninitialised members and library leaks | DONE | 2026-09-29 | 2026-09-29 | Reproduce every defect locally with origins, declare the model defaults and regenerate, extend the suppressions with the library categories, add the codegen gate, and prove the four suites and the full suite clean. |
5. Decisions
- The defaults are declared in the models rather than synthesised in the
templates. The estate's mechanism for this is
:default_value:, the sprint 25 compute-host fix used it for the same defect, and the value that means "absent" is the model's to state. The alternative – synthesising a default for every arithmetic and chrono type incore.py– closes the whole class at once but decides a semantic value on the model's behalf; it is filed as a capture with the census so the decision is taken with the size known. - The gate refuses the nullable half of the shape and not the non-nullable
half. A nullable column with a bare
doublecannot carry NULL, so the convention that zero is the absence has to be stated; that is the shape that producedbond_trs::funding_rate. The non-nullable half is 87 columns today and refusing it would make this branch a catalogue-wide model change. - The suppressions name the allocation site's library frame, not the loader.
A rule anchored on
llvm_gcov_init,__cxa_atexit,PQconnectdbor the Catch2 reporter suppresses the category and leaves any allocation through another path visible. - Each finding was reproduced locally with
--track-origins=yesbefore it was fixed. The saved CDash pages are truncated at about a megabyte, and the first version of this work fixed 64 of the 214 analytics defects because it trusted the page; the second entity's 146 only appeared when the binary was re-run.
6. Out of scope
- The 87 non-nullable columns across the catalogue that still declare no default and generate an indeterminate member. The census is filed as 87 non-nullable columns still generate an indeterminate member.
- The pre-existing generated drift in components outside the drift gate.
Regenerating
trading-cppto update the models rewrote one unrelated line oftrade_service.cpp, a clang-format wrapping difference; it was reverted so this branch carries only its own change. - The seed-profile eventing tests of
ores.iam.core.tests, which leave the profile they insert in the database and so only pass once per database. Found while verifying, unrelated to this change, and filed as The iam seed-profile eventing tests only pass once per database.