C hardening baseline
This skill is a pure-C policy. It never prescribes a C++ idiom (RAII, destructors, smart pointers, lock guards, containers) as C practice. C++ code goes to modern-cpp-practices.
Contract
| Field | Bound contract |
|---|---|
| Trigger | C code is being written, audited, or hardened and the project needs one baseline for the C standard, undefined behavior, integer and buffer safety, the sanitizer and fuzzing matrix, and build and CI hardening flags. |
| Authority | Read-only. Writes nothing. Chat output only. No remote mutation. |
| Side effect | Emits a guidance report to chat. No source or build file is modified. |
| Done | The report names the C standard with the compiler that supports it, the undefined-behavior catalog with the sanitizer that traps each entry, the integer and buffer rules with their checked-arithmetic form, the hardening flag set the identified compiler accepts, the sanitizer and fuzzing jobs for CI, and the findings the audit checklist produced. |
Inputs
- C source code or build files (required): the code being written, audited, or hardened.
- Compiler vendor and version (required when not inferrable from build files): for example GCC 16.2 or Clang 23.1.0.
- Active C standard (required when not inferrable from build flags): the
-std=value or the project-agreed standard. - Existing warning, hardening, sanitizer, and fuzzing configuration (optional):
CMakeLists.txt,meson.build,Makefile, CI job files.
Procedure
Identify the compiler, its version, and the active C standard from build files (
CMakeLists.txt,meson.build,Makefile, compiler command lines) or from the inputs. Record whether the code is C++ anywhere in the tree. When the compiler or standard cannot be determined, report the ambiguity and stop. Done when: compiler, version, and standard are named, or the ambiguity is reported.Select the C standard. C23 (ISO/IEC 9899:2024) is the current published standard. Pin
-std=c23when the compiler accepts it. GCC 16.2 and Clang 23.1.0 both accept-std=c23. Fall back to-std=c17on an older toolchain and list which C23 controls the fallback loses (<stdckdint.h>,static_assertas a keyword,nullptr,[[nodiscard]]and the other standard attributes). Use-std=c23or-std=c17, not thegnuspellings, unless the code needs a GNU extension and a comment names it. Done when: the standard is selected and the compiler's support is recorded.Catalog undefined behavior. Undefined behavior is not implementation-defined and not "works on my machine": it is permission for the optimizer to assume the program never does the thing. A single instance on a reachable path can delete a bounds check or miscompile a loop, and a clean debug build proves nothing. Scan for the high-frequency classes and report each occurrence with
file:line:- Signed integer overflow.
INT_MAX + 1is UB, so the compiler may assumex + 1 > xand remove the overflow check. Unsigned overflow wraps by definition and is not UB, but a wrap that feeds a size is still a bug. - Out-of-bounds access, including one past the end for a dereference.
- Read of an uninitialized automatic variable. Initialize at declaration.
- Null, misaligned, or invalid pointer dereference. The optimizer may delete null checks that follow a dereference.
- Strict aliasing violation: access to an object through a pointer of an incompatible type. Type-pun through
memcpyor through a union;charandunsigned charmay alias anything. - Data race: concurrent access to a non-atomic object with at least one writer and no happens-before relation. Use
<stdatomic.h>or a mutex. - Invalid shift: by a count at or past the width, or by a negative count.
- Modification of a
constobject, a call through a function pointer of the wrong type, and an infinite loop with no side effects. Done when: every occurrence of a cataloged class is listed, or the report states that none was found.
- Signed integer overflow.
Apply the integer and buffer rules. Report each violation with
file:line:- Validate ranges before the arithmetic that feeds an allocation size, an array index, a
memcpylength, or a loop bound. Overflow into a small allocation followed by a large copy is the classic remote code execution shape. - Use checked arithmetic:
ckd_add,ckd_sub, andckd_mulfrom<stdckdint.h>under C23, or__builtin_add_overflow,__builtin_sub_overflow, and__builtin_mul_overflowon GCC and Clang under C17. - Use
size_tfor sizes and indexes. Guard the order of an unsigned subtraction:a - bwraps whenb > aand the result passes every upper-bound check. - Compile with
-Wconversion -Wsign-conversionand fix narrowing and sign changes at the source. - Pass a length with every pointer and check it. Prefer
snprintfand explicit length checks over unbounded copies. Never computeptr + nfrom an attacker-controllednbefore comparingnagainst the real remaining length. - Check every allocation return.
reallocreturningNULLmust not overwrite the original pointer, or the block leaks. Use a temporary. - Pair every
malloc,calloc, orreallocwith exactly onefreeon every exit path, and everyfopenwithfclose. Use the goto-cleanup idiom, a single exit label that releases what was acquired, so an early error return cannot skip a release. - Do not trust an embedded length field from the wire or from a file. Cap it against the remaining buffer before use.
- Replace banned functions on sight:
gets(removed in C11; usefgets),strcpyandstrcat(usesnprintforstrlcpywhere the platform provides it),sprintfandvsprintf(usesnprintfandvsnprintf),scanf("%s")without a width (add a width or parse by hand),systemandpopenwith interpolated input (useposix_spawnorexecvewith an argument vector),strtok(usestrtok_r),allocaand a VLA sized from input (use the heap with a checked size),atoiandatol(usestrtolwith range anderrnochecks). - Never pass user data as a format string.
printf(user)reads and writes memory through%n. Writeprintf("%s", user)and compile with-Wformat -Wformat=2 -Werror=format-security. - Never use
randorrandomfor keys, tokens, IVs, or salts. Usegetrandom(2)orarc4random_bufwhere the platform provides it, or a vetted library. - Zero secrets after use with a wipe the compiler cannot elide (
explicit_bzeroormemset_swhere available). A plainmemsetbefore afreecan be optimized away. assertis compiled out under-DNDEBUG, which the common release presets set. A bounds or validation check written asassertdoes not exist in the shipped binary. Write it as an explicitifthat returns or aborts, and keepassertfor internal invariants.- Canonicalize paths with
realpathbefore checking them, preferopenatwithO_NOFOLLOW, and operate on file descriptors to avoid check-then-use races. Done when: every violation is listed with its replacement.
- Validate ranges before the arithmetic that feeds an allocation size, an array index, a
Recommend the hardening flag set. Report the flags the identified compiler accepts, split by compiler where support differs. Do not edit build files. Both GCC 16.2 and Clang 23.1.0 accept:
- Warnings:
-Wall -Wextra -Wpedantic -Wconversion -Wsign-conversion -Wshadow -Wcast-align -Wnull-dereference -Wdouble-promotion -Wimplicit-fallthrough -Wformat=2 -Wshift-overflow=2 -Wvla -Wbidi-chars=any -Wtrampolines, with-Werrorin CI. A clean-Wall -Wextrabuild is the floor, not the goal. - Fortification and stack:
-O2 -D_FORTIFY_SOURCE=3 -fstack-protector-strong -fstack-clash-protection -fstrict-flex-arrays=3 -ftrivial-auto-var-init=zero. - Control flow on x86-64:
-fcf-protection=full. - Position independence and link:
-fPIE -pie -Wl,-z,relro -Wl,-z,now -Wl,-z,noexecstack, and-Wl,-z,nodlopenfor an executable that no one loads withdlopen. - Reproducibility:
-ffile-prefix-map=<src>=.andSOURCE_DATE_EPOCH, so a path or timestamp does not leak into the binary. Keep-gand ship split debug symbols. GCC only:-fhardened(an umbrella that enables a subset of the above; verify the subset against the GCC version in use) and-fzero-init-padding-bits=all. Clang rejects both. Missing hardening flags on a network-facing or setuid binary is a HIGH finding. Done when: the flag set is listed per compiler with each unsupported flag named.
- Warnings:
Recommend the sanitizer and fuzzing matrix. Sanitizers ship inside GCC 16.2 and Clang 23.1.0 under
-fsanitize=. They are for test and CI builds, never production: production uses the flag set from step 5.- Job one:
-fsanitize=address,undefined -fno-sanitize-recover=all -fsanitize-address-use-after-scope, then the full test suite. Any abort is a CRITICAL or HIGH finding. SetASAN_OPTIONS=detect_leaks=1:strict_string_checks=1andUBSAN_OPTIONS=print_stacktrace=1. - Job two, for threaded code:
-fsanitize=thread. ASan and TSan cannot run in one binary. - Clang only: add
-fsanitize=integerto catch defined-but-suspicious unsigned wrap.-ftrapvis the blunter GCC and Clang alternative for signed overflow. - Valgrind Memcheck is the no-recompile fallback for a binary that cannot be rebuilt. It misses the stack and global overflows ASan catches.
- Do not fix a UBSan report by casting the diagnostic away. Fix the arithmetic or the access.
- Fuzzing: any code that parses untrusted bytes (network, file formats, decoders) gets a fuzz target. Prefer AFL++ v5.03c for a new target. libFuzzer ships inside Clang 23.1.0 via
-fsanitize=fuzzerand receives bug fixes only, so an existing libFuzzer target stays supported but a new one belongs on AFL++. Pair every fuzz build with ASan and UBSan. Keep a seed corpus and a regression corpus in the repository. A new crash is a CRITICAL finding. - Static analysis:
clang-tidywithbugprone-*,cert-*,clang-analyzer-*,misc-*,performance-*, andportability-*, committed in.clang-tidy, and GCC-fanalyzerin a CI job. Commit a.clang-formatand enforce it withclang-format --dry-run --Werror. - Dependencies: one manifest and one lockfile, pinned versions, a generated SBOM, and the standard library preferred over a dependency. Done when: the sanitizer jobs, the fuzz target list, the static analysis configuration, and the dependency policy are in the report.
- Job one:
Run the audit checklist against the tree and report each hit with its severity. Every command below is
grep -rnEover*.cand*.h:- Banned functions, HIGH or CRITICAL:
\b(gets|strcpy|strcat|sprintf|vsprintf|scanf|system|popen|strtok|atoi|atol)\s*\(. allocaand VLAs sized from input, HIGH:\balloca\s*\(, plus a build with-Wvlato list every VLA, then read each hit for the source of its size.- Format string with a non-literal first argument, CRITICAL:
(printf|fprintf|snprintf|syslog|err|warn)\s*\([^"]*\). - Overflow-prone size arithmetic, HIGH:
(malloc|calloc|realloc|alloca)\s*\([^;]*[*+][^;]*\), then check forckd_or__builtin_.*_overflowin the same function. - Check-then-use on paths, HIGH:
\b(access|stat|lstat)\s*\(, then check whether the following open uses the checked name. - Insecure randomness for secrets, HIGH:
\b(rand|random|srand)\s*\(. - Timing leak, MEDIUM:
memcmpon a buffer named like a MAC, token, secret, or digest. - Unchecked allocation, HIGH: each
= (malloc|calloc|realloc)\(hit not followed by aNULLcheck. - Missing hardening in the build, HIGH on a network or setuid binary: no
_FORTIFY_SOURCE,stack-protector,relro,fcf-protection, orPIEin the build files. - Missing sanitizer or fuzz job in CI, HIGH for input-parsing code: no
fsanitize,afl, orfuzzerin the CI configuration. Done when: every checklist line has run and each hit is in the report with a severity.
- Banned functions, HIGH or CRITICAL:
Verify every recommendation against the compiler and standard named in step 1. Drop a flag the compiler rejects and record the drop. Drop every C++ prescription that a source or a prior draft carries as C policy and list it under dropped prescriptions. Done when: every recommendation is verified and the dropped list is complete.
Failure and recovery
| Failure class | Behavior |
|---|---|
| Compiler or standard undetermined | Report the ambiguity with the evidence examined. Do not guess. Guidance is blocked until both are named. |
| C23 unsupported | Fall back to -std=c17 and list the C23 controls that are lost, with the C17 replacement for each (__builtin_*_overflow for <stdckdint.h>, _Static_assert for static_assert). |
| Hardening flag rejected | Omit it, name the compiler that rejects it, and recommend the subset that compiles cleanly. |
| Sanitizer unavailable on the platform | Name the missing sanitizer, recommend Valgrind Memcheck as the fallback, and state what it misses. |
| C++ found in the tree | Report the C++ files and route them to modern-cpp-practices. Apply this baseline to the C files only. |
| Prescription found that is a C++ idiom | Drop it from the report and list it under dropped prescriptions. |
No partial result is claimed complete. When a step cannot finish, the report states which steps succeeded and which are blocked.
Output
A guidance report with these sections:
| Section | Content |
|---|---|
| C standard | C23 or C17, with the compiler and version that justify the choice and the controls a C17 fallback loses. |
| Undefined behavior catalog | Each cataloged class found in the code, with file:line and the sanitizer that traps it. |
| Integer and buffer rules | Each violation with its checked-arithmetic, bounded, or ownership replacement. |
| Hardening flags | The flag set per compiler, with every rejected flag named. |
| Sanitizer and fuzzing matrix | The CI jobs, fuzz targets, static analysis configuration, and dependency policy. |
| Audit findings | Each checklist hit with file:line and severity, CRITICAL first. |
| Dropped prescriptions | Every C++ idiom cut from a source or draft because it does not apply to C. |