Skip to content

fix(svm): check every allocation in the bundled predictor, and move the Cppcheck lane - #1498

Merged
lusoris merged 2 commits into
masterfrom
fix/svm-cppcheck-219
Sep 19, 2026
Merged

lusoris merged 2 commits into
masterfrom
fix/svm-cppcheck-219

Conversation

@lusoris

@lusoris lusoris commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Moving CI to the newer runner image moved the analyser with it: ubuntu-24.04 carries cppcheck 2.13 and ubuntu-26.04 carries 2.19, with nothing older in its archive. 2.19 reported 90 findings in core/src/svm.cpp that 2.13 never looked for, and #1494 parked the Cppcheck lane on the old image rather than suppress them. This fixes them and moves the lane.

Finding Count What it is
nullPointerOutOfMemory 88 a malloc result dereferenced without a test; 74 of them through the Malloc macro
noCopyConstructor / noOperatorEq 2 the Cache class owns raw heap storage and allowed itself to be copied

The allocation fix follows the policy this file already had. Malloc goes through a helper that reports and aborts on failure, which is exactly what the same file does at its realloc sites, where the comment reads "OOM in a hot scoring path — no recovery model". The callers here, predict.c and brisque.c, have no way to unwind a half-built model. On success the helper returns exactly what malloc returned, so no score can move. Cache's copy operations are deleted, as the Kernel class in the same file already does; a copy would double-free the head array and every cached column.

Why the diff is large: the touched-file rule

Editing the file brought all of it into scope, and 14 blocks exceeded the 60-line limit. Each is split along the seams its own comments already marked:

Area Split into
Solver::Solve (237 lines) setup, pair selection, the two alpha-update branches, the gradient update, the finish
Solver::select_working_set, Solver_NU::select_working_set their two scans each
svm_train (227 lines) regression branch, weighted C, permuted inputs, the pairwise loop, the model head, the coefficient layout, the workspace free
sigmoid_train initial point, gradient and Hessian, line search
svm_cross_validation, svm_save_model, svm_check_parameter, svm_predict_values, svm_group_classes one helper each
SVMModelParser (235-line template body), Solver, SVR_Q method definitions moved out of the class bodies

Two realloc-growth blocks that were duplicated between svm_group_classes and the parameter check now share one helper. Every expression is the one it replaced, in the same order.

Verification

Check Result
cppcheck 2.19, CI's exact flags, in an ubuntu:26.04 container 90 findings to 0, exit 0
Blocks over 60 lines in the file 14 to 0
praetorctl audit passes
meson test --suite=fast 139 of 139
test_svm_api (the fork's own libsvm API test) passes
Netflix pair at --precision max, every frame vs the pre-change binary byte-identical, pooled metrics identical, 48 of 48 frames
Checkerboard pairs, 1 px and 10 px 35.0686714193046 and 7.985899011514694

The score check is the one that matters, and it is a comparison against a binary built from master's source, not against remembered numbers.

no docs needed: no user-discoverable surface changes. The contributor-facing consequence, that a future libsvm re-vendor would undo all of this, is in docs/rebase-notes.md.

Type

  • fix — bug fix
  • refactor — no behavior change

Checklist

Deep-dive deliverables (ADR-0108)

  • Research digest — no digest needed: trivial. The findings, the fix and the verification are in this description and in the state row; nothing here needed investigation beyond running the analyser.
  • Decision matrix — no alternatives: only-one-way fix. .cppcheck-suppressions.txt states there is no vendored tier, so suppressing was not an option, and the size limit is not optional either.
  • AGENTS.md invariant note — carried in docs/rebase-notes.md, the cross-package invariant index the harness imports.
  • Reproducer / smoke-test command — below.
  • CHANGELOG fragment — changelog.d/fixed/svm-checked-allocation.md.
  • Rebase note — docs/rebase-notes.md.

Reproducer

docker run --rm -v "$PWD:/w:ro" -w /w ubuntu:26.04 bash -c \
  'apt-get update -qq && apt-get install -y -qq cppcheck &&
   cppcheck --enable=warning,performance,portability --check-level=exhaustive \
     --inline-suppr --library=posix --error-exitcode=1 core/src/svm.cpp'

meson setup build core -Denable_cuda=false -Denable_sycl=false && ninja -C build
meson test -C build --suite=fast
meson test -C build test_svm_api

To check the scores yourself, build master and this branch, score the Netflix pair with --precision max --json from each, and compare the frames arrays.

Known follow-ups

  • The training half of libsvm (svm_train, svm_cross_validation, sigmoid_train, svm_save_model) is reachable only from core/test/test_svm_api.c; scoring calls svm_predict. Whether the fork should keep compiling it is worth its own decision.

cppcheck 2.19 reports 90 findings in this file that 2.13 never looked for: 88
nullPointerOutOfMemory, where a malloc result is dereferenced without a test,
and the Cache class owning raw storage with neither a copy constructor nor an
assignment operator. The analyser version comes from the CI runner image, so
they appeared when the matrix moved to ubuntu-26.04. They are real, and
ADR-1142 gives vendored code no exemption, so they are fixed rather than
suppressed and the Cppcheck lane moves to 26.04 with the rest of the matrix.

The Malloc macro now goes through a helper that reports and aborts on failure,
which is the policy this file already follows at its realloc sites: OOM in a
scoring path has no recovery model, and the callers here cannot unwind a
half-built model. That covers 74 of the 88 in one place. On success the helper
returns exactly what malloc returned. Cache's copy operations are deleted, as
the Kernel class in the same file already does.

Touching the file brought the whole of it into scope for the size limit, which
14 blocks exceeded. Each is split along the seams its own comments already
marked: the solver into setup, pair selection, the two alpha-update branches,
the gradient update and the finish; both working-set selections into their two
scans; the trainer into its regression branch, weighted C, permuted inputs, the
pairwise loop, the model head, the coefficient layout and the workspace free;
the sigmoid trainer into initial point, gradient and line search; and the model
parser's methods out of its class body. Two class bodies shrank by moving small
accessors out. Every expression is the one it replaced, in the same order.

Verified: cppcheck 2.19 with CI's exact flags goes from 90 findings to 0; no
block exceeds 60 lines; the governance audit passes; the fast suite is 139 of
139 and test_svm_api passes. Scores are byte-identical: all 48 frames of the
Netflix pair at --precision max match the pre-change binary exactly, and both
checkerboard pairs give 35.0686714193046 and 7.985899011514694.
Replaces the open row #1494 added with the closed one, and adds the changelog fragment plus the rebase note that tells the next re-vendor what it would undo.
@github-actions github-actions Bot added the type:bug Something isn't working label Sep 19, 2026
@lusoris
lusoris merged commit 74fdfdd into master Sep 19, 2026
81 checks passed
@lusoris
lusoris deleted the fix/svm-cppcheck-219 branch September 19, 2026 19:11
lusoris added a commit that referenced this pull request Sep 19, 2026
…n the stale baseline (#1503)

standards-gate.yml installed praetor at e4b35cb3c7fe (2026-09-18); main is at 846da5908d15. Before moving the pin, both engines and a CI-form go-install of 846da59 recorded the baseline on the same origin/master tree: all three produce byte-identical infraction sets, 1414 entries with the same fingerprints, so the bump changes no finding.

The committed baseline said 1433 while the tree measures 1414. The 19 are two files cleaned in merged PRs without re-recording: core/src/svm.cpp 14 -> 0 (#1498) and core/test/test_ciede_neon.c 5 -> 0. A too-loose baseline fails nothing, so those entries were headroom for 19 new findings. Reported upstream as cordanaLLM/praetor#349.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type:bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant