Safeguard
Developer Tools•

Memory safety checks in CI: what to run, in what order

A practical CI plan for C and C++ projects: warnings, hardened builds, sanitizers and dependency checks, ordered by cost and by what each one catches.

Safeguard Security Team
3 min read

A sensible CI plan for native code runs cheap checks on every commit and expensive ones on merge or nightly: compiler warnings first, then a hardened build, then sanitizer runs of the test suite, then dependency checks. Android's documentation also recommends integrating memory safety tools into CI and testing.

Stage one: warnings and static checks

Cost: low. Build with the warning set from the OpenSSF hardening guide, such as -Wall, -Wformat=2, -Wconversion and -Wimplicit-fallthrough, and use -Werror in development builds only. Run a static analyzer. This catches format string misuse and risky conversions before anything runs.

Stage two: hardened build and tests

Cost: low to moderate. Build with fortify source, stack protector, the C++ library hardening macro and the other core flags, then run the unit tests. A hardening trap here shows you a real out-of-bounds access in a test, which is the cheapest place to find it.

Stage three: sanitizer builds

Cost: moderate, since AddressSanitizer typically slows a program by about 2x. Run the full tests under AddressSanitizer on each merge. Run UndefinedBehaviorSanitizer with the full runtime in the same job or a second one. Remember that fortify source is incompatible with AddressSanitizer, so use a separate flag set.

Stage four: hardware assisted checks

Cost: depends on hardware. Where you have MTE capable devices, run tests in SYNC mode for precise reports.

Stage five: dependency checks

Cost: low. Scan the lockfile and the native libraries you vendor against known vulnerability data on every pull request, and fail on findings above your threshold. Many memory bugs in your product are old bugs in someone else's code.

Try it yourself

Click through it at your own pace, or open it full screen. Watch how a gate decides whether a pull request can merge.

Budgeting the time

Split the checks by when they run. Pull request jobs should finish fast enough that people wait for them: warnings, a hardened build and the quick tests. Sanitizer runs of the whole suite can run on merge or nightly if they take too long for a pull request, as long as a failure creates a ticket with an owner.

If a sanitizer job is too slow, shard the tests across runners before you cut coverage. The documented slowdown is roughly 2x for AddressSanitizer, so doubling runner time is often cheaper than debugging a leak that shipped.

Keep the build matrix honest

Maintain at least three build flavors: a normal developer build, a sanitizer build and a release build with hardening. Keep their flags in one shared file, so a change in one place is visible in review. Check that a failing sanitizer build blocks the merge, not just posts a comment that nobody reads.

Make failures actionable

  1. Name an owner for each job.
  2. Store sanitizer reports as artifacts with the failing test name.
  3. Do not allow retries to hide a sanitizer failure, since a flaky memory bug is still a bug.
  4. Track how many sanitizer failures reach main each month.

Quick check

Should the sanitizer build be the release build?

No. Sanitizers are testing tools. Ship a separate build with production hardening flags.

Safeguard's policy gates can enforce a dependency finding threshold on pull requests, and the free scan at https://gold.safeguard.sh/scan gives a first look at a repository.

Sources

Last reviewed: 19 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.