Demo 7 — Quality pipeline: static analysis, tests, sanitizers, ABI, debugging
A complete pre-merge quality pipeline for a small C++ library and its service, all running inside containers. Static analysis, unit tests, sanitizers, ABI checks, hermetic builds, and a gdbserver sidecar — the six tools that, between…
The full source for this demo lives in
examples/demo-07-quality-pipeline/— clone the repo,cdin, and./demo.sh.
Tutorial sections: §12 Static Analysis & Debugging in Containers + §13 Reproducibility & ABI
A complete pre-merge quality pipeline for a small C++ library and its service, all running inside containers. Static analysis, unit tests, sanitizers, ABI checks, hermetic builds, and a gdbserver sidecar — the six tools that, between them, catch most of what goes wrong in production C++.
Why this matters
Most C++ production incidents fall into a small number of recognizable patterns: memory corruption that the tests didn’t exercise, a use-after-free under specific timing, an ABI break shipped quietly when a header’s struct member was added, or a build that worked on someone’s laptop and didn’t reproduce in CI. Each pattern has a well-understood tool that catches it — but those tools only catch it when they’re actually wired into the pipeline.
This demo is the engineering practice that makes those tools default rather than aspirational. The whole pipeline runs in containers; the lockfile is checked in; the ABI reference is checked in; the sanitizer build is one CI job and one build flag. Each piece is small; their composition is what catches bugs before they ship.
§12 covers the analysis and debugging toolkit; §13 covers reproducibility and ABI. This demo wires both into one shell script.
What this demo shows
Six tools running in sequence in a CI-shaped pipeline:
- Static analysis —
cppcheckandclang-tidyruns over the source, each producing a parseable report. Warnings fail the build with a non-zero exit so CI can gate on them. - Unit tests — GoogleTest + gmock. The library has a
deliberate abstraction-cost example (a
Channelinterface vs a templated CRTP form) and a microbenchmark unit-test that prints both timings. - Sanitizers — an ASan + UBSan instrumented variant built in a separate stage; runs the same test suite under sanitizer instrumentation. Leaks, OOB reads/writes, and undefined behavior surface as a non-zero exit with a stack trace.
- ABI compatibility —
libabigail’sabidiffcompares the current build’s library against a stored “v1.0” reference symbol set. A meaningful change to a public header (e.g. adding a member to a struct that’s part of the ABI) makesabidifffail loudly with the diff. - Hermetic build — Conan 2 lockfile + CMake presets for
full reproducibility. The lockfile is checked in;
conan installconsumes it rather than re-resolving. - gdbserver sidecar — a separate Containerfile target that
ships a debug build with
gdbserverlistening, pluscompose.debug.ymlto bring it up next to the main service.
How to run
./demo.sh # full pipeline (analyze + test + asan + abi)
./demo.sh --analyze-only # only run cppcheck + clang-tidy
./demo.sh --test-only # only build and run gtest (release)
./demo.sh --asan-only # only build and run gtest under ASan + UBSan
./demo.sh --abi-only # only run abidiff against the reference
./demo.sh --debug # also bring up the gdbserver sidecar
./demo.sh --clean
Expected runtime: ~5-7 minutes for the full pipeline on a cold cache, ~1-2 minutes on a warm Conan cache.
What you’ll see
Each phase prints a clear pass/fail line and writes reports under
reports/ for CI consumption (cppcheck XML, clang-tidy txt,
gtest XML, ASan stderr, abidiff txt). A representative successful
run:
==> cppcheck: PASS (0 warnings, 0 errors)
==> clang-tidy: PASS (0 warnings on changed files)
==> gtest release: PASS (47 tests, 1.2 s)
==> gtest ASan+UBSan: PASS (47 tests, 4.8 s, no leaks)
==> abidiff vs v1.0 reference: PASS (no ABI changes)
==> Pipeline result: PASS
A run with an intentional ABI break introduced (add a member to a
public struct in include/lib/) produces:
==> abidiff vs v1.0 reference: FAIL
1 function with some indirect sub-type change:
[C] 'function void Foo::bar()' has some indirect sub-type changes:
parameter 1 of type 'Foo&' has sub-type changes:
'struct Foo' changed:
type size hasn't changed
1 data member insertion:
'int Foo::new_field', at offset 32 (in bits) at lib.h:14:1
How to read the output
Each tool surfaces a different class of bug:
- cppcheck flags structural issues — uninitialized variables, null dereferences, scope confusion, leaks in linear control flow. Fast (seconds), high signal, no false positives in the common case.
- clang-tidy flags style and modern-C++ issues — missing
override, non-constmember functions that could be, pre-C++17 idioms, performance anti-patterns. Slower (depends on enabled checks), more opinionated. - gtest failures are correctness regressions — straightforward to interpret; failing test names point at the regression.
- ASan failures are memory-safety bugs — heap-buffer-overflow, use-after-free, leaks. The stack trace usually points directly at the offending line. These were always there; ASan made them visible.
- UBSan failures are undefined-behavior bugs — signed integer overflow, null deref, alignment violations. Often subtler than ASan output; sometimes you have to consult the standard to understand what the compiler was about to optimize away.
- abidiff failures mean a downstream rebuild is required — the library’s binary interface has changed in a way that breaks ABI compatibility. The diff tells you which struct, which function, which symbol changed. Either revert the change, bump the SONAME, or accept the rebuild burden on consumers.
Core dumps from the containerized service
When the service crashes, you want a core file. ulimit -c
unlimited inside the container is not enough on its own — the
kernel’s core_pattern lives on the host, so the path you set
has to be reachable from the container’s mount namespace.
The recipe:
# On the host: prepare a writable core-dump directory
sudo mkdir -p /var/cores && sudo chmod 1777 /var/cores
# Point core_pattern at the host directory (lives on the host kernel)
echo '/var/cores/core.%e.%p.%t' | sudo tee /proc/sys/kernel/core_pattern
# Bring up the service with unlimited core size + the host dir bind-mounted
podman run --rm \
--ulimit core=-1 \
--volume /var/cores:/var/cores \
--name demo07-svc \
cpp-tut/demo-07:svc
After a crash, /var/cores/core.demo07-svc.<PID>.<timestamp>
appears on the host. Open it with the debug sidecar pattern:
podman run --rm -it \
--volume /var/cores:/cores:ro \
--volume "$(pwd)/build/release-debuginfo":/symbols:ro \
--entrypoint=gdb \
cpp-tut/demo-07:gdbserver \
/symbols/demo07-svc /cores/core.demo07-svc.<PID>.<timestamp>
The debug sidecar has gdb; the production svc image (built on
ubi-minimal) does not. This is §12’s debug-sidecar pattern in
miniature.
Caveats and gotchas
abidiffrequires DWARF info. The library is built with-gfor the abi-only step, then re-built without for the runtime image.clang-tidyneedscompile_commands.json. CMake generates it viaCMAKE_EXPORT_COMPILE_COMMANDS=ONin our preset.gdbserverover rootless networking works, but the kernel must allow ptrace on the target (Fedora’s defaultkernel.yama.ptrace_scope=0is fine; some hardened distros set it higher).- ASan’s shadow-memory mapping interacts with seccomp. The
ASan stage runs with
--security-opt=seccomp=unconfinedand may also needvm.mmap_min_addr=4096on hosts where the default is higher. See §12’s “Runtime sanitizers in containers” for the diagnosis path if ASan fails to start. - The ABI reference is a snapshot. It captures the library’s ABI at a known-good commit. Updating the reference (when a legitimate SONAME bump happens) is a manual step; the demo doesn’t try to be clever about when to do it automatically.
Source materials
This demo deepens material from the project’s bibliography:
- Iglberger, C++ Software Design, ch. 3-5 — the design principles that static analysis can detect violations of; loose coupling, value semantics, the ABI-stability argument
- Ghosh, Building Low Latency Applications with C++, ch. 14 — what an “all sanitizers, all the time” CI looks like for latency-sensitive code; the trade-off between coverage and CI duration
- libabigail manual — the canonical reference for
abidiff’s semantics and exit codes when integrating into CI gates
Linked tutorial sections
- §12 Static Analysis & Debugging in Containers — every tool above is one of the analysis-and-debugging responses §12 walks through (static analysis = build-time prevention, sanitizers = CI-time prevention, debugger + core dumps = incident-time diagnosis).
- §13 Reproducibility & ABI
— the Conan lockfile, CMakePresets, and
abidiffinvocation here are the minimum-viable version of the §13 “Reproducibility & ABI” toolkit. The §13 prose covers Konflux + Cachi2 for full hermetic CI, gcov/lcov + clang source-based coverage, and the abidiff CI integration — those further integrations are documented in §13 but not exercised in this demo’s scripts. - §14 Pitfalls — the abstraction-cost microbenchmark unit-test in this demo’s library is §14’s worked example: the Channel interface vs the templated CRTP form, with measured numbers showing the cost.