Gate every push on cppcheck: 15 findings, one of them a reported use-after-free in aksl_fclose #45
Notifications
Due Date
No due date set.
Reference: andrew/libakstdlib#45
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Add a
cppcheckstatic-analysis gate to this repository: run on every push, fail thepipeline on any finding, and run again in the pre-push hook so it fails on the workstation
first. This is one of four -- akbasic, libakerror, libakstdlib and libakgl all get the same
gate, built to the same plan, and this issue is the libakstdlib half.
Do libakerror first. It is six findings in one file and it is where the shape of
scripts/cppcheck.shand thestatic_analysisjob gets settled; this repository copies it.libakstdlib is second because it is the only one of the four that already has a pre-push
hook, so it is the first place the hook half of the plan can actually be finished.
Measured, before any change
cppcheck 2.19.0,
--std=c99 --platform=unix64 --enable=style, oversrc include:By file:
src/stdlib.c7,src/collections.c6,src/string.c2.The one error is the reason this is worth doing at all:
Read that one before anything else. It points at
streaminsideFAIL_NONZERO_RETURN(e, fclose(stream), ...)inaksl_fclose-- cppcheck's caret sits onthe argument of the
fclosethat does the deallocating. The obvious way for that to be areal defect is a macro that evaluates its argument twice, and it does not:
FAIL_NONZERO_RETURNexpands toif ( __x != 0 ) { FAIL(...); return ...; }, oneevaluation. So this is most likely cppcheck mis-modelling a deallocating call used as a
condition, and the likely outcome is an inline suppression naming that.
It is still the first thing to check rather than the first thing to dismiss. A
use-after-free that ASan, valgrind and a 99.5%-line coverage suite have all run past would
be exactly the class of defect a static checker exists to catch. If it turns out real, it
gets its own defect issue rather than being fixed quietly inside this one.
The rest are style:
unreadVariable5,constVariablePointer5,variableScope2,constParameterPointer1, and oneknownConditionTrueFalse:Expect that last one to be the error macros again -- cppcheck does not follow
ATTEMPT/FINISHany better than gcov does, which is the same reason this repository's CIcomments already give for its 40% branch gate.
Scope for this repository
No
examples/,tools/orutil/in this tree.tests/(21 files) is excluded per theplan below, and
deps/libakerroris excluded by the scope: measured,cppcheck . -i testshere reports 30 findings and 24 of them are libakerror's, gated in libakerror's own
pipeline by the companion issue.
Work
src/stdlib.c:399. If the use-after-free is real, file it as a defect in itsown right and link it here; if it is not, inline-suppress it with the reason.
line above it.
scripts/cppcheck.shwith the scope above.static_analysisjob to.gitea/workflows/ci.yaml..githooks/pre-pushas the first step, before the default build. It is2--6 seconds against that hook's existing tens of seconds, and it should be skipped
with a warning when cppcheck is absent -- the same way that hook already treats
doxygen, and for the reason written there.
tests/exclusion in TODO.md with its consequence.The plan (identical in all four repositories)
The gate
Four deviations from the sketch, each with a measurement behind it:
An explicit scope, not
..cppcheck .descends intodeps/and into whateverbuild*/happens to be lying around. Measured in libakstdlib:cppcheck . -i testsreports 30 findings, 24 of them inside
deps/libakerror-- the same six defects thisplan already gates in libakerror's own pipeline, reported a second time from a
repository that cannot fix them. akbasic and libakgl vendor SDL, SDL_image, SDL_mixer,
SDL_ttf, jansson, libccd and clay, which is thousands of findings and minutes of
runtime for code nobody here owns. Build directories are the other half: the mutation
harness copies the whole tree into one, and the pre-push hook runs on a workstation
where two or three of them exist.
--error-exitcode=1. cppcheck exits 0 when it has findings. Verified on 2.19.0:exit 0 with six findings reported, exit 1 with the flag, exit 0 on a clean tree. Without
this flag the job is green no matter what it prints.
--inline-suppr, and no baseline file. Every finding is either fixed or carries a// cppcheck-suppress <id>on the line above it with the reason in a comment. Asuppression list in a separate file is a list nobody reads; a suppression at the site is
a documented, tracked defect, which is what the engineering rules ask for. There is no
"current findings" baseline: the tree goes clean before the gate turns on.
-i tests, as sketched -- deferred, not decided against. Tests are where a leak ismost likely to hide, and this excludes them. That is a named deferral for TODO.md, not a
judgement that test code does not matter. (
-i 'tests/*'and-i testsboth work on2.19.0; an explicit scope makes the question moot.)
Where it lives
scripts/cppcheck.sh-- one definition that CI and the pre-push hook both run, so a pushthat would fail CI fails on the workstation first. Same shape as the existing
scripts/coverage.py,scripts/memcheck.shandscripts/mutation_test.py.SCOPEis the repository's own C and nothing else -- named per repository below.CI
A new
static_analysisjob:The check itself is 2 s (libakgl) to 6 s (akbasic) measured on this workstation; the apt
install dominates the job.
Version drift is a real cost and is accepted rather than engineered around: a newer
cppcheck finds new things, and the gate goes red on a push that did not cause it. That is
fix-forward. Printing the version is what makes it diagnosable. Do not pin an old cppcheck
to keep the tree quiet.
pre-push
First step in the hook -- it is the cheapest gate there by an order of magnitude. Skipped
with a warning when cppcheck is not installed, matching the doxygen precedent in
libakstdlib's hook: a hook that fails closed on a missing optional tool only teaches
everyone to pass
--no-verify. CI is the hard gate.Order
job shape; the other three copy it.
.githooks/pre-push.Acceptance, per repository
scripts/cppcheck.shexits 0 on a clean checkout with no suppressions beyond those whosereason is written at the site.
static_analysisjob exists and has been proven red once, by pushing a deliberatefinding on a throwaway branch and watching the pipeline fail.
wiring waits on it.
-i testsdeferral is recorded in TODO.md with its consequence.Measured with cppcheck 2.19.0 on 2026-08-05.
Siblings, same plan:
.githooks/pre-push)Order: libakerror, then libakstdlib, then akbasic and libakgl in parallel.