C hardening baseline
SkillDev toolsUse when C code is written or audited and needs a pure-C baseline: standard, undefined behavior, integer and buffer safety, sanitizers, fuzzing, build flags. Not for C++: use modern-cpp-practices.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the C hardening baseline skill
What this skill tells your AI
The instructions your AI receives, as published by outlinedriven/outline-driven-development in .devin/skills/c-hardening-baseline/SKILL.md and read by ahel’s review.
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. |
Signals
- GitHub stars
- 52
- Forks
- 9
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
c-hardening-baseline- Source
- github.com/outlinedriven/outline-driven-development