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

clang-format

Style drift

Free

Pre-commit hook

3

clang-tidy

~500 named checks: bugprone, cert, misc

Seconds

Pre-commit

4

cppcheck

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):

  1. Create projects/<name>/ in a fork of google/oss-fuzz.

  2. Add Dockerfile, build.sh, project.yaml, and at least one fuzz_target.c.

  3. Test locally: python infra/helpers.py build_image <name> then build_fuzzers <name> then run_fuzzer <name> <target>.

  4. Open PR to google/oss-fuzz. Google’s team reviews for the CNCF criteria (widely used, has a security contact, MIT/BSD/Apache-ish).

  5. 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