02 — Static Analysis, Fuzzing, and CI¶
Production C is not just “code that compiles.” It is code that survives -Wall -Wextra -Wpedantic -Werror, clang-tidy, cppcheck, an ASan/UBSan/TSan/MSan sweep, a Coverity scan, an oss-fuzz run, and a matrix build across gcc/clang/msvc on Linux/macOS/Windows. Each layer catches a different class of bug. Miss any of them and something ships to a customer that never should have.
This file is your minimum viable pipeline. If your gemm_bench and quantize_ext repos don’t have this by end of M12 W3, come back before moving on.
The layered defense¶
Think of the tools as a filter stack: cheap tools catch trivial bugs, expensive tools catch the ones that would have shipped. Run them in this order because their cost climbs geometrically.
Layer |
Tool |
What it catches |
Cost |
When to run |
|---|---|---|---|---|
1 |
Compiler flags |
Unused vars, shadowing, sign compares, missing prototypes |
Free |
Every save |
2 |
|
Style drift |
Free |
Pre-commit hook |
3 |
|
~500 named checks: bugprone, cert, misc |
Seconds |
Pre-commit |
4 |
|
Cross-TU flow analysis; catches things clang misses |
Seconds |
Pre-commit |
5 |
Sanitizers (ASan/UBSan) |
Use-after-free, OOB, UB at runtime |
2–3× slowdown |
Every test run in CI |
6 |
TSan, MSan |
Races, uninitialized reads |
5–15× slowdown |
Nightly |
7 |
libFuzzer / AFL++ |
Deep input-driven bugs |
Hours-days |
Nightly / oss-fuzz |
8 |
Coverity / CodeQL |
Interprocedural, taint tracking |
Cloud minutes |
Weekly / on PR |
Layer 1 — compiler flags you should treat as mandatory¶
Add these to every serious C project. They are cheap and they turn a class of runtime bugs into compile errors.
CFLAGS_STRICT = -std=c11 -Wall -Wextra -Wpedantic \
-Wshadow -Wconversion -Wsign-conversion -Wcast-align \
-Wstrict-prototypes -Wmissing-prototypes -Wold-style-definition \
-Wnull-dereference -Wdouble-promotion -Wformat=2 \
-fstack-protector-strong -D_FORTIFY_SOURCE=3
CFLAGS_DEBUG = $(CFLAGS_STRICT) -g3 -O1 \
-fsanitize=address,undefined -fno-omit-frame-pointer
CFLAGS_RELEASE = $(CFLAGS_STRICT) -O3 -DNDEBUG -flto
-D_FORTIFY_SOURCE=3 (glibc ≥ 2.34) is the modern replacement for =2 and catches more memcpy / strcpy bounds bugs at link time. Turn it on. -Wformat=2 catches format-string mistakes that were CVEs a decade ago and are still CVEs today.
One trap: -Werror on a library breaks builds for downstream users on newer compilers (a warning added in gcc 15 becomes an error for people on gcc 15). Use -Werror in CI, not in the shipped Makefile.
Layer 3 — clang-tidy configuration¶
Drop this .clang-tidy at your repo root. It is a strong baseline; loosen only with justification in a comment.
Checks: >
bugprone-*,
cert-*,
clang-analyzer-*,
misc-*,
performance-*,
portability-*,
readability-*,
-readability-magic-numbers,
-readability-identifier-length
WarningsAsErrors: 'bugprone-*,cert-*,clang-analyzer-*'
HeaderFilterRegex: '.*'
Run with clang-tidy -p build src/*.c. It requires a compile_commands.json — generate it with cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON or Bear (bear -- make).
Layer 5 — sanitizers in CI, always¶
Run your entire test suite twice in CI: once under ASan+UBSan, once under TSan. Any project without this is one PR away from a shipped use-after-free.
# .github/workflows/sanitize.yml
name: sanitize
on: [push, pull_request]
jobs:
asan:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- run: sudo apt-get install -y clang-18
- run: CC=clang-18 CFLAGS="-fsanitize=address,undefined -g -O1 -fno-omit-frame-pointer" make test
env:
UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=1
ASAN_OPTIONS: abort_on_error=1:strict_string_checks=1:detect_leaks=1
MSan requires an MSan-instrumented libc — skip it unless you can invest in a full libc++ rebuild. TSan is production-usable and worth turning on if you use pthreads.
Layer 7 — fuzzing with libFuzzer (the entry-level workflow)¶
For any C function that takes an untrusted byte buffer, write a fuzz target. This is the entire harness:
// fuzz/fuzz_parse.c
#include <stdint.h>
#include <stddef.h>
#include "my_parser.h"
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
struct parsed *p = parse(data, size);
if (p) parsed_free(p);
return 0;
}
Compile and run:
clang -g -O1 -fsanitize=fuzzer,address,undefined \
fuzz/fuzz_parse.c src/parser.c -o fuzz_parse
./fuzz_parse corpus/ -max_total_time=300
Even 5 minutes on your laptop finds most stupid bugs. 24 hours on a beefy box finds the interesting ones.
OSS-Fuzz — the deep end (verified July 2026)¶
OSS-Fuzz has been running Google’s continuous fuzzing infrastructure on open-source projects since 2016. By May 2025 it had found 13,000+ vulnerabilities and 50,000+ bugs across 1,000+ projects, including a 20-year-old OpenSSL bug that an LLM-generated harness (oss-fuzz-gen) caught in November 2024. If you contribute meaningfully to a C project this year, you will interact with it.
Onboarding a project (still the 2024 flow, verified):
Create
projects/<name>/in a fork ofgoogle/oss-fuzz.Add
Dockerfile,build.sh,project.yaml, and at least onefuzz_target.c.Test locally:
python infra/helpers.py build_image <name>thenbuild_fuzzers <name>thenrun_fuzzer <name> <target>.Open PR to
google/oss-fuzz. Google’s team reviews for the CNCF criteria (widely used, has a security contact, MIT/BSD/Apache-ish).Once merged, Google runs your fuzzers 24/7 across their cluster and files private bugs to your security contact.
Official docs: https://google.github.io/oss-fuzz/getting-started/new-project-guide/.
Important change from earlier tutorials: the OSS-Fuzz Reward Program (the direct bounty stream at bughunters.google.com) was sunset on May 1, 2026. General OSS-Fuzz service continues; monetary rewards now go through the Google Patch Rewards Program and OSS VRP. Do not tell junior engineers “contribute to oss-fuzz to earn money” — that door closed.
CI matrix — what to run on every PR¶
Production C is expected to build on gcc AND clang AND msvc, on Linux AND macOS AND Windows. GitHub Actions makes this cheap:
strategy:
matrix:
os: [ubuntu-24.04, macos-14, windows-2025]
cc: [gcc, clang, cl]
exclude:
- {os: macos-14, cc: cl}
- {os: ubuntu-24.04, cc: cl}
- {os: windows-2025, cc: gcc} # or keep for MinGW
At a minimum: gcc-latest on Linux, clang-latest on macOS, cl (MSVC) on Windows. If any of the three break, curl / SQLite / Valkey users find out first.
Coverity, CodeQL, and the commercial layer¶
Coverity Scan is free for open source (register at scan.coverity.com); it runs a heavier interprocedural analysis than clang-tidy and catches bugs the free tools miss. CodeQL (GitHub, free for public repos) does taint tracking — useful for finding tainted-input paths that reach system(), sprintf, strcpy. Turn both on for any repo you care about; they cost you nothing but a config file.
Cheat sheet — the minimum viable CI for your own C repo¶
Copy this into gemm_bench and quantize_ext:
.github/workflows/
├── build.yml # matrix: {gcc, clang} × {ubuntu, macos}, -Wall -Wextra -Werror
├── sanitize.yml # ASan + UBSan on every push
├── fuzz.yml # 5-min libFuzzer smoke run per PR
└── codeql.yml # GitHub-managed, one line to enable
.clang-tidy # baseline shown above
.clang-format # LLVM style, tabs=4
What most people get wrong about this¶
They install the tools once, get a wall of warnings, get demoralized, and never run them again. The move is the opposite: fix warnings for one file at a time, commit, move on. In a week you will have -Werror clean. The wall of warnings is not evidence your code is broken; it is evidence you did not baseline it yet.
Second thing they get wrong: they think sanitizers are for finding other people’s bugs. Sanitizers are for finding your bugs before your users do. Run them on your own toy projects, not just when reading someone else’s code.
Return to README.md · Next: 03_secure_c_and_cve_literacy.md