C standards
Criteria verified as of August 2026. Re-verify on the web before committing to anything (§8).
1. Scope and triggers
Applies to all work in C: .c files and C headers, choice of -std=, compiler and linker flags, Makefile/CMakeLists.txt/meson.build for C targets, compile_commands.json, sanitizer configuration, static analysis (-fanalyzer, clang-tidy, cppcheck), fuzzing, MISRA C / CERT C conformance, unit tests and hardening of the produced binary. Covers firmware/embedded, user-space drivers, system libraries, CLIs and high-performance code. It sets criteria: what is used, what is vetoed and what must be verified. It is not a C tutorial.
Central axis of this skill: in C the compiler manages neither resources nor bounds checking, so the lifecycle of every resource and undefined behavior are design requirements, not implementation details. Everything else (build, tests, CI) exists to make that discipline verifiable.
Not applicable: see cpp-standards (everything it decides about C++: RAII, templates, the C++ standard library, new/delete, exceptions, target-oriented CMake for C++ and the memory safety debate in the ISO C++ committee — no C++ here). Arbitration criteria for code that compiles in both: the standard the target is compiled under wins (-std=c23 → this skill; -std=c++23 → cpp-standards), not the file extension nor the code style; a header consumed from both worlds is designed under c-standards (C subset, no C++-only constructs) and its extern "C" wrapper is a shared boundary: the ABI guarantee is set by this skill, consumption from C++ is set by cpp-standards. See also rust-standards (language choice for new systems code — C is not the default by inertia — and the Rust side of the FFI: bindgen/cbindgen, #[repr(C)], unsafe), bash-linux-scripting-standards (build scripts, wrappers and shell automation), linux-hardening-standards (hardening of the system: kernel, sysctl, MAC, systemd; hardening of the binary — RELRO, PIE, stack protector, CFI — belongs to this skill), kernel-drivers-standards (reciprocity already declared from its §1: when a rule of this skill clashes with the kernel, the kernel wins — there is no libc, there is no malloc, style and sanitizers are the tree's own), appsec-standards (threat modelling and the AppSec process), vulnerability-management-standards (CVE triage, patching SLA, VEX), gpu-computing-standards (CUDA/HIP, kernels and GPU toolchain), cicd-standards (pipeline design and gates; here only which tool runs and with which flags), offensive-security-standards (exploitation), kubernetes-standards (OCI image of the binary), linux-storage-standards and observability-standards for what belongs to them. Neighbouring languages: zig-standards (already written) for the modern alternative to C — and for the special case of zig cc: using Zig as a cross-compilation toolchain is their decision, but the C it compiles remains subject to this skill—, nim-standards (already written: Nim generates C and compiles it with a C compiler; the Nim is theirs, the generated C and the flags it is compiled with fall here), lua-standards (already written: the Lua C API —stack, lua_State, luaL_*, value lifecycle against the collector— is a shared boundary; the C of the native extension and its correctness belong to this skill, the contract with the interpreter is theirs), assembly-standards (already written) for inline assembly and intrinsics — the asm inside a C function is a shared boundary: the compiler constraint and the correctness of the asm are theirs, the rest of the function is ours—, objective-c-standards (already written: Objective-C is a superset of C, so the C contained in a .m remains subject to this skill; what is specific to the Objective-C runtime is theirs).
2. Default toolchain
Verify the latest version on the web before pinning it in a real project (§8). The following is the state verified as of Aug 2026.
| Decision |
Default |
Justifiable alternative |
Reason |
| Standard |
C17 (-std=gnu17) for portable production code; C23 (-std=gnu23) if the project's minimum toolchain is GCC ≥ 15 / Clang ≥ 18 and there are no vendor compilers involved |
C11 in legacy codebases or frozen vendor toolchains |
GCC states "GCC has support for ISO C23, the 2023 revision of the ISO C standard (published in 2024)" and "C23 mode is the default since GCC 15"; Clang accepts -std=c23 since Clang 18 but its C23 support is partial, paper by paper (see clang.llvm.org/c_status.html). C99 is the absolute minimum: forbidden C89/K&R in new code |
| Compiler |
GCC 16.x (16.1, 2026-04-30) or Clang/LLVM 22.x (22.1.8, 2026-06-16) |
Vendor toolchain when the silicon forces it |
Compile with both in CI: each diagnoses what the other stays silent about |
| Build |
CMake ≥ 4.4 for libraries/projects others consume; Meson ≥ 1.11 when the project is Linux-centric and readability is the priority |
Make only in single-binary projects with no external dependencies |
Hand-written Make does not scale to cross-compilation, sanitizer builds and compile_commands.json |
| Static analysis |
clang-tidy (same release as the Clang in use) + gcc -fanalyzer |
cppcheck 2.21.x (GPL-3.0) as a third opinion |
-fanalyzer is the most mature for C: in C++ it is best-effort; in C it is usable as a gate |
| Tests |
Unity 2.7.0 (MIT, 2026-07) in embedded and code without a full libc; Criterion 2.4.3 (2025-10) on Linux/POSIX when per-process isolation is wanted |
CMocka 2.0.x when already in use (mocking by link-time wrapping) |
Verify maintenance before pinning: the CMocka mirror on GitHub is frozen at 1.1.5 (2019/2022); the authoritative source is cmocka.org / cryptomilk's git, which announces the 2.0 series |
| Fuzzing |
libFuzzer for a fast in-process harness + AFL++ 5.02c (2026-06) for long campaigns |
Honggfuzz |
libFuzzer is deprecated in LLVM upstream in the sense that it receives no new features: verify its state in the release notes of the specific version before basing a whole programme on it |
| Dynamic memory |
The target libc's malloc |
Custom allocator / arenas only with measured justification |
A custom allocator shifts the burden of proof: it demands its own ASan+fuzzing |
Toolchain rules:
- No implicit compiler flags: the full compilation line lives in the build system and is versioned.
CFLAGS injected by the environment are appended, not substituted.
compile_commands.json always generated (CMAKE_EXPORT_COMPILE_COMMANDS=ON / Meson generates it by itself). Without it there is no clang-tidy, no clangd and no reproducible analysis.
- Cross-compilation via a versioned toolchain file (CMake) or cross file (Meson) — forbidden to detect the cross-compiler with ad-hoc conditionals.
- Minimum warning set, and it is a gate:
-std=gnu17 -Wall -Wextra -Werror
-Wshadow -Wconversion -Wsign-conversion -Wdouble-promotion
-Wformat=2 -Werror=format-security
-Wcast-qual -Wcast-align -Wpointer-arith -Wwrite-strings
-Wstrict-prototypes -Wold-style-definition -Wmissing-prototypes -Wmissing-declarations
-Wvla -Walloca -Wstack-protector
-Wnull-dereference -Wimplicit-fallthrough
-Werror=implicit-function-declaration -Werror=incompatible-pointer-types -Werror=int-conversion
- The four
-Werror= on the last line are non-negotiable: in C17 and earlier those three constructs are diagnostics that many compilers accepted with a warning and are a direct source of memory corruption. C23 turns them into language errors; forcing them beforehand avoids the surprise on migration.
-Wconversion/-Wsign-conversion are noisy in existing code: they are enabled per new module and extended from there; they are never switched off globally to "fix the build".
-Werror always in CI; in the developer's local build it may stay as a warning so as not to break the flow, but merging requires the CI build.
3. Structure, conventions and resource lifecycle
Layout
src/ implementation, include/<project>/ public headers (always with a project directory: the consumer's #includes are <project/foo.h>), tests/, fuzz/, cmake/ or meson/.
- One public header = one contract. Everything else is
static or lives in internal headers outside include/. Visibility enforced in the linker: -fvisibility=hidden + an explicit export macro in shared libraries.
- Include guards
#pragma once if every compiler in the project supports it; if there is a dubious vendor toolchain, classic guards with a unique name PROJECT_MODULE_H.
- Project prefix mandatory on every symbol with external linkage (
px_buffer_new): C has no namespaces and the linker resolves collisions silently with absurd results.
- Public headers: types and functions only, zero unnecessary
#includes (forward declaration where it suffices), zero unprefixed macros, and extern "C" guarded by #ifdef __cplusplus in every header a C++ consumer might include.
Resource lifecycle — the axis of the language
- Explicit ownership rule: every pointer crossing an API boundary has documented in the header who frees it and with which function. Without that line, the API is incomplete.
- Paired constructor/destructor per type:
T *t_new(...) / void t_free(T *), and t_free(NULL) is a no-op (like free). Forbidden to spread the freeing across the caller field by field.
- A single exit point for cleanup in functions with several acquisitions: cascading
goto fail_* pattern in reverse order. It is the correct idiom in C, not a forbidden goto — MISRA C:2025 withdraws the single-exit-point rule, but the project criteria remain: centralised cleanup, in reverse order.
- After freeing, the pointer is nulled in the owning structure (
p = NULL) if its life continues: use-after-free becomes a null deref, which is a deterministic failure.
- Ownership is not shared by default. If sharing is needed, explicit refcount with the policy documented and a concurrency test; never "it gets freed at some point".
alloca and VLAs: forbidden (-Walloca -Wvla). Stack size is a finite and uncheckable resource; a VLA with a size derived from input is a remote stack overflow.
- Buffers with input-derived size: check the calculation before multiplying (
__builtin_mul_overflow / ckd_mul from <stdckdint.h> in C23), not afterwards.
- Closing descriptors and
FILE*: same pattern as memory; check the return of fclose/close when there was a write (I/O errors show up there, not in write).
Undefined behavior as a first-class bug
A UB is not "something that usually works": the optimiser assumes it does not happen and deletes the code that checks for it. It is treated as a high-severity defect even if the current binary works.
- Categories reviewed in every code review: out-of-bounds access, use-after-free/double-free, reading uninitialised memory, signed integer overflow, shift ≥ the type's width or with a negative operand, strict aliasing violation, misaligned pointers,
memcpy with overlapping pointers (memmove is what applies), pointer arithmetic outside the object (including ptr + n beyond "one past the end"), comparison of pointers to distinct objects, division by zero, NULL passed to <string.h> functions even with length 0, and data races.
- The arithmetic ones are caught at runtime with UBSan; the aliasing ones are not: if the project does type punning, it is done with
memcpy or with a union, never with a pointer cast, or it is compiled with -fno-strict-aliasing declared and justified in the build (a legitimate option, but a project decision, not a silent patch).
-fwrapv / -fno-strict-overflow as a safety net in inherited codebases: acceptable and explicit, but it does not replace fixing the overflow; and it is not portable to vendor compilers.
Integers
size_t for sizes and indices; never int for lengths. Fixed-width types (<stdint.h>) for binary formats, protocols and hardware registers; int/long only for local arithmetic.
- Implicit promotions and conversions are the most frequent bug in the language:
-Wconversion -Wsign-conversion enabled and zero casts "to silence the warning". A cast is an assertion that the range has been checked; if it has not, it is a bug wearing makeup.
- Comparing
signed with unsigned is vetoed (-Wsign-compare comes with -Wextra); in C23 there is <stdckdint.h> (ckd_add/ckd_sub/ckd_mul) — verify real availability in the target's libc before depending on it; in C17, GCC/Clang's __builtin_*_overflow.
- Signed integer overflow is UB; unsigned overflow is defined but is usually just as much of a bug (size truncation). Both are checked.
char without an explicit signed/unsigned has implementation-defined signedness: for bytes, uint8_t or unsigned char.
Forbidden functions and their real replacement
| ❌ Forbidden |
Replacement |
Note |
gets |
fgets + check the \n |
Removed from the language since C11 |
strcpy, strcat |
Explicit checked length: measure and memcpy; snprintf for composition |
strncpy is not the replacement: it does not guarantee termination |
strncpy |
snprintf, or memcpy with manual termination |
Pads with zeros and truncates without warning |
sprintf, vsprintf |
snprintf/vsnprintf checking the return (>= size is truncation, and truncating is usually a logic bug) |
|
atoi, atol, atof |
strtol/strtoll/strtod with errno set to 0 beforehand and endptr checked |
atoi does not distinguish error from zero: UB on overflow |
alloca, VLA |
malloc + limit, or a fixed sized buffer |
|
rand, random |
The OS CSPRNG (getrandom, arc4random_buf, BCryptGenRandom) for any security use |
|
system, popen with a composed string |
posix_spawn/fork+execve with a vectorised argv |
Concatenating input into a command is injection |
strtok |
strtok_r/strsep |
Global state: not reentrant |
gmtime, localtime, ctime, asctime |
The _r variants |
Shared static buffer |
memcpy over overlapping regions |
memmove |
UB even when it "works" |
scanf("%s") |
fgets + parsing, or %<n>s with a width |
|
Annex K (strcpy_s, _s) |
Do not adopt it as a strategy |
Real support is almost non-existent outside MSVC and questioned by WG14: it is not the portable solution |
Mechanical enforcement: a list of vetoed symbols in clang-tidy (bugprone-unsafe-functions, cert-*) and, in high-assurance projects, --defsym/wrap or a linker check on the imported symbol.
Concurrency
- One threading model per project:
pthreads on POSIX or C11 threads (<threads.h>) if the toolchain really has them (verify: glibc exposes them since 2.28; some libcs do not). Do not mix.
- Every shared variable is protected by a mutex or is
_Atomic with an explicit and justified memory order; volatile is not a concurrency primitive (it is for MMIO and sig_atomic_t, nothing more).
- Lock acquisition order documented and global; no nested locks without a hierarchy.
- Signal handlers: only async-signal-safe functions and
volatile sig_atomic_t; the correct pattern is to write to a pipe/eventfd (self-pipe) and process it in the main loop.
- Data race = UB. TSan is the gate.
4. Quality: formatting, analysis, tests and CI gates
Formatting
clang-format with a versioned .clang-format (LLVM or GNU base, tuned once and not re-debated). clang-format --dry-run --Werror in CI.
- Forbidden to reformat en masse alongside functional changes: the formatting commit goes separately and is recorded in
.git-blame-ignore-revs.
Static analysis (in order of increasing cost)
- Compiler warnings with
-Werror (already covered, zero cost).
gcc -fanalyzer over the C tree: catches double free, use-after-free, leaks, NULL derefs and descriptor misuse. Noisy in macros; it gets triaged, not switched off. (Verified state: its C++ support is incomplete/best-effort — irrelevant here, this is the C skill.)
clang-tidy with a versioned .clang-tidy. Starting set: bugprone-*, cert-*, clang-analyzer-*, misc-*, performance-*, portability-*, readability-* (trimming the style ones that clash with clang-format), and -checks=-readability-magic-numbers only if the project has its own constants policy. WarningsAsErrors for the stabilised part.
cppcheck --enable=warning,style,performance,portability --error-exitcode=1 with versioned suppressions (suppressions.txt) — a third opinion, a different family of heuristics. Verify the current licence before pinning it as a corporate default (as of Aug 2026 the open project is GPL-3.0 and there is also a commercial Premium edition: they are different products).
- CodeQL in the repo (or the provider's equivalent) for data-flow queries across translation units, scheduled as well as on PR.
No // NOLINT without a specific lint and a reason; no #pragma GCC diagnostic ignored without push/pop and a comment.
Sanitizers (separate builds, never a single one "with everything")
- ASan + UBSan + LSan in the same build:
-fsanitize=address,undefined -fno-omit-frame-pointer -fno-sanitize-recover=all -g -O1. LSan comes with ASan on Linux. This is the default test build in CI.
- TSan in a separate build:
-fsanitize=thread. Incompatible with ASan: the runtimes assume different memory maps and the compiler emits a hard error when combining them. It implies PIE and requires instrumenting all the code.
- MSan in a separate build and Clang/Linux only (
-fsanitize=memory -fPIE -pie -fno-omit-frame-pointer -fno-optimize-sibling-calls -O1). It requires all dependencies, including libc++/libc as the case may be, to be instrumented: without that it produces false positives. If that cost is not bearable, coverage of uninitialised memory reads is covered with Valgrind Memcheck, which remains the practicable route in that gap.
-fno-sanitize-recover=all mandatory: a finding must abort the test, not print and continue. UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1, ASAN_OPTIONS=detect_stack_use_after_return=1:strict_string_checks=1:detect_leaks=1.
- Valgrind still has its place, and for concrete reasons, not out of nostalgia: it works on already compiled binaries (third-party code, blobs, already-published artifacts) and covers uninitialised memory without recompiling the world. In exchange it does not see overflows in local or global variables nor use-after-return, and its cost is an order of magnitude over ASan. Criteria: ASan+UBSan on every PR, TSan in a separate job, Valgrind on a periodic run or to triage a binary that cannot be recompiled.
- Production is never deployed with sanitizers active (they are debugging tools and they widen the attack surface:
ASAN_OPTIONS is read from the environment).
Fuzzing
- Every parser, decoder, deserialiser or untrusted input entry point has a fuzzing harness. It is not optional: it is where the bugs appear that the fuzzer finds in minutes and human review does not see in years.
LLVMFuzzerTestOneInput harness (compatible with libFuzzer and AFL++ via afl-clang-lto), compiled with ASan+UBSan, versioned and minimised corpus (-merge=1), dictionary when the format has one.
- CI: a short run (60–300 s) of the corpus on every PR as a regression test; a long scheduled campaign (nightly/weekly) with AFL++.
- Relevant open source project: integrate it into OSS-Fuzz (Google, active — daily commits as of Aug 2026). Every crash reported by the fuzzer enters as a bug with a regression test in the corpus.
- The fuzzed binary is not the release binary: different builds.
Testing
- Framework as per §2; each test is an isolated case, with no shared global state, and the runner reports in a CI-consumable format (TAP/JUnit).
- Mandatory coverage of edges and errors, not the happy path: length 0, maximum length,
NULL, boundary values of each integer type, malloc failure (fault injection via wrapper or LD_PRELOAD), truncation, unterminated input.
- Every fixed bug leaves a regression test that fails before the fix; if it came from a fuzzer, it also enters the corpus.
- Coverage as a signal:
--coverage/llvm-cov published, focused on error branches. A percentage is not a target.
- Deterministic tests: no dependence on timing, hash order or the network. A flaky test is fixed or deleted.
Minimum CI gate (everything breaks the build)
1. clang-format --dry-run --Werror
2. build GCC -Wall -Wextra -Werror (+ set from §2)
3. build Clang -Wall -Wextra -Werror (+ set from §2)
4. clang-tidy over compile_commands.json (diff or whole tree)
5. gcc -fanalyzer
6. tests under ASan+UBSan (-fno-sanitize-recover=all)
7. tests under TSan (separate job, if there are threads)
8. short fuzz corpus over each harness
9. dependency SCA + SBOM of the artifact
10. clean release build with the hardening flags from §5 and verification of the binary
Main always green. Nothing is merged with CI red.
5. Stack security and binary hardening
Hardening flags (release build)
Base aligned with the OpenSSF Compiler Options Hardening Guide for C and C++ (a living document: re-verify before freezing it in a project):
-O2 -Wall -Wformat -Wformat=2 -Wconversion -Wimplicit-fallthrough
-Werror=format-security
-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3
-fstrict-flex-arrays=3
-fstack-clash-protection -fstack-protector-strong
-fPIE -pie
-Wl,-z,relro -Wl,-z,now
-Wl,-z,noexecstack -Wl,-z,nodlopen
-Wl,--as-needed -Wl,--no-copy-dt-needed-entries
-U_FORTIFY_SOURCE before -D_FORTIFY_SOURCE=3 matters: many distros already define it and redefining it without undefining warns or is ignored. _FORTIFY_SOURCE requires -O1 or higher and depends on the libc (glibc implements it; on musl it is essentially inoperative — verify on the real target, do not assume it).
- Also add (with impact measurement):
-ftrivial-auto-var-init=zero: eliminates the whole class of "reading an uninitialised local" in production. Low cost; verify support in the project's specific GCC/Clang version.
-fcf-protection=full (x86-64) and -mbranch-protection=standard (AArch64) for hardware CFI.
-fsanitize=cfi with LTO (Clang) or -fsanitize=undefined -fsanitize-minimal-runtime -fsanitize-trap=undefined as a minimal-cost UBSan in production to turn detectable UB into a deterministic trap — a project decision, measure first.
-fno-delete-null-pointer-checks and -fno-strict-aliasing in inherited codebases whose correctness cannot be fully audited (declared and justified).
- Verify the produced binary, do not trust the flags:
checksec, hardening-check or readelf -d in CI, checking PIE, full RELRO, NX, stack canary and the absence of unsafe RPATH/RUNPATH. A flag being in CFLAGS does not mean it reached the link.
- Symbol strip on the release artifact, with build-id and debug symbols archived separately (
objcopy --only-keep-debug) so crashes can be symbolised.
Supply chain and dependencies
- Every C dependency is code that will run with the process's privileges: it is justified in writing. Prefer the libc and what is already present over adding a library for one utility.
- Dependencies by exact pin (tag + hash), never
master. Vendoring (submodule/subproject with a pinned revision) is acceptable and often preferable to a package manager in C; what is not acceptable is copying code without a record of origin, version and licence.
- SBOM (SPDX or CycloneDX) generated in the build and published with the artifact; SCA against CVEs of the embedded libraries. Vendored code is exactly where CVEs get lost.
- Licences verified in the project's real
LICENSE, not in whatever an aggregator says.
Code
- All external input is validated at the boundary (length, range, termination, encoding) before touching the rest of the programme. A binary format parser always assumes hostile input.
- Check every return:
malloc, realloc (never p = realloc(p, n) — the original pointer is lost if it fails), snprintf, read/write (partials), fclose. Ignoring a return is a decision that gets marked ((void) + comment), not an oversight.
- Secrets: never in code or in logs; erase sensitive material with
explicit_bzero/memset_explicit (C23) — a normal memset gets removed by the optimiser. Consider mlock for keys and prevent them reaching core dumps (prctl(PR_SET_DUMPABLE, 0) where applicable).
- Crypto: use an audited library (libsodium, OpenSSL 3.x, BoringSSL/mbedTLS depending on the target). No rolling your own crypto, nor comparing secrets with
memcmp (constant time: sodium_memcmp/CRYPTO_memcmp).
- Path and file comparison:
openat/O_NOFOLLOW/O_CLOEXEC and temporary files with mkstemp; no check-then-open (TOCTOU).
O_CLOEXEC/SOCK_CLOEXEC by default on every descriptor: leaking descriptors to child processes is a silent escalation.
MISRA C and CERT C — when they apply
- CERT C: applicable to any C project with a security surface; its rules overlap with clang-tidy
cert-*. It is adopted as a set of automated checks, not as a document to read.
- MISRA C: mandatory only when the domain requires it (automotive/ISO 26262, IEC 61508, avionics/DO-178C, medical devices/IEC 62304). Current edition verified: MISRA C:2025 (published March 2025), successor to MISRA C:2023; covers C90/C99/C11/C18, ~225 active guidelines, and withdraws the historical single-exit-point rule (although IEC 61508 / ISO 26262 may still require similar practices on their own). It treats AI-generated code the same as hand-written code for conformance purposes.
- MISRA requires MISRA Compliance:2020 as its framework: guideline matrix, documented and approved deviations, and evidence. Adopting MISRA "in name" without that framework is not conformance, it is theatre.
- Do not apply MISRA to a project that does not need it: it penalises legitimate constructs and consumes review budget that pays off better in sanitizers and fuzzing.
6. Performance, ABI, portability and operability
Performance
- Measure before optimising:
perf, flamegraph, cachegrind. A performance change with no measured before/after number does not get merged.
- Reproducible benchmarks (fixed frequency, CPU pinning, several repetitions with reported spread); no comparing timings from a single run.
-O2 is the default; -O3 only if the benchmark justifies it on that specific binary. -march=native forbidden in distributable artifacts (it produces binaries that fail with SIGILL on another machine); runtime dispatch or function multiversioning if AVX-512 is needed.
- LTO (
-flto=thin on Clang, -flto on GCC) by default in release if build time allows; verify that it does not break with alias symbols/__attribute__((used)).
- Hand micro-optimisations are the last resort; the 2026 compiler wins almost always. The real gain is in the algorithm and in the memory access pattern.
ABI and interoperability
- The ABI is a contract: changing the size or layout of a public struct, the order of an enum, the prototype of an exported function or the calling convention is a breaking change even if the API compiles.
- Shared libraries: versioned
SONAME, -fvisibility=hidden + export macro, and a version script (--version-script) to control the exported set. abi-compliance-checker or abidiff (libabigail) in the CI of libraries with an ABI contract.
- Public structs: prefer opaque types (
typedef struct px_ctx px_ctx;) so they can evolve without breaking the ABI. If the struct must be public, no fields are added in the middle and the padding is documented.
- FFI: the C boundary is the lowest common denominator for Rust/Python/Go/C++. Rules: no platform-dependent types in the signature (use
<stdint.h>), fixed-size integers, ownership documented on every parameter, no propagating errno as a contract across languages, and a *_free function exported for every object the library returns (the consumer cannot call another libc's free — on Windows it is a straight failure).
- Consumption from C++: header with
#ifdef __cplusplus extern "C" {, no bool from <stdbool.h> in the signature if old compilers are involved, no flexible array members in types C++ must define, and no names that are reserved words in C++ (class, new, template, operator...). The C++ side of the contract is set by cpp-standards.
Portability
- Explicitly declare the set of supported targets (architecture, libc, OS, compiler and minimum version) in the README and test them in CI. What is not tested is not supported.
- Assume:
char may be signed or unsigned; integers may be 32 or 64 bits (long is 32 on Windows and 64 on Linux — use <stdint.h>); byte order varies; misaligned accesses abort on some architectures; the evaluation order of a function's arguments is unspecified.
- Feature test macros (
_POSIX_C_SOURCE, _GNU_SOURCE) defined in the build system, not scattered across the .c files.
- No dependence on the behaviour of a specific compiler version;
__builtin_* and __attribute__ are wrapped in macros with a fallback.
Operability
- Structured logging to
stderr (or syslog/journald depending on the deployment) with a configurable level; no debug printf in production code.
- Exit codes with meaning; errors via
errno-like or a documented custom enum — never magic codes without a table.
- Clean exit on SIGTERM/SIGINT: self-pipe, draining of work in progress, resource release. A daemon that only dies by SIGKILL is not deployable with a rolling update.
- Crashes:
build-id, archived symbols, core_pattern/coredumpctl or a crash collector; a crash without a symbolisable trace is wasted time.
assert is not error handling and disappears with NDEBUG: never put side effects inside it, nor use it to validate external input. For invariants that must hold in release, an explicit check with abort() or a static_assert (C11+) if it is compile-time.
7. Sustainability: upgrades and prohibitions
Cadence: upgrade the compiler at least once a year (GCC publishes one major annually, ~April; LLVM every ~6 months) and test the next one in CI before it becomes mandatory — a three-version jump accumulates new warnings and optimisation changes that expose latent UB. A new failure after a compiler upgrade is, by default, your own bug, not a compiler regression. Language standard: review the migration to C23 when the project's minimum toolchain allows it; C2y/C29 (working draft in progress, publication expected at the end of the decade) is not used in production.
Deprecation: in libraries with an ABI contract, deprecate with __attribute__((deprecated("use X"))), a window of at least one major version, and a new SONAME when the ABI breaks. In internal binaries, compatibility flags are not accumulated "just in case".
Conscious debt: every shortcut leaves /* TODO(user): reason — issue #N */. Every lint/sanitizer suppression comes with a reason and a review date.
FORBIDDEN (requires written justification and approval to make an exception):
- ❌ The functions in the §3 table (
gets, strcpy, strcat, strncpy, sprintf, atoi, alloca, strtok, rand for security, system with a composed string...).
- ❌ VLAs and
alloca with an input-derived size; stack buffers sized "with margin".
- ❌ Ignoring the return of
malloc/realloc/snprintf/read/write/fclose; p = realloc(p, n).
- ❌ Pointer casts for type punning (strict aliasing violation); casts to silence
-Wconversion.
- ❌
-Werror disabled in CI; #pragma GCC diagnostic ignored without push/pop and a reason; // NOLINT without a specific lint.
- ❌ Combining ASan with TSan/MSan in the same build (the compiler rejects it); deploying production with sanitizers.
- ❌
-march=native in distributable artifacts; -O3 without a benchmark; -ffast-math in code that compares or validates floats.
- ❌
volatile as a synchronisation mechanism between threads; shared global variables without a lock or _Atomic.
- ❌ Pointer arithmetic outside the object; comparing pointers of distinct objects;
memcpy with overlap.
- ❌ Public headers without a project prefix, without an include guard or dragging in unnecessary
#includes; external symbols without a prefix.
- ❌ Mutable global state in libraries (breaks reentrancy and tests); functions with a static return buffer.
- ❌ Macros that do what a
static inline function would do; multi-statement macros without do { } while (0); macros that evaluate an argument twice.
- ❌ Home-made crypto, comparing secrets with
memcmp, memset to erase secrets.
- ❌ Copying third-party code without a record of origin, version and licence; dependencies pointing at a branch instead of at tag+hash.
- ❌ An untrusted input parser without a fuzzing harness.
- ❌ Annex K (
*_s) as a portability strategy.
- ❌ Declaring "it's only a warning": a compiler warning in C is a bug until proven otherwise.
- ❌ Including in this skill or in the code it produces: exploits, ROP gadgets, concrete mitigation bypasses, shellcode or payloads. This skill is defensive: it describes vulnerability classes (overflow, use-after-free, format string, TOCTOU, type confusion) in order to prevent them, never to exploit them. Offensive work →
offensive-security-standards, with written scope and authorisation.
8. Mandatory web verification
Before pinning versions, flags or asserting the state of the ecosystem, verify on the web (never from memory):
- Real C23 state per compiler: https://gcc.gnu.org/projects/c-status.html and https://clang.llvm.org/c_status.html (paper-by-paper table). Do not assume parity between GCC and Clang: as of Aug 2026 GCC declares C23 support and default since GCC 15; Clang accepts
-std=c23 since 18 with partial support.
- Versions and lifecycle: https://gcc.gnu.org/develop.html + https://gcc.gnu.org/releases.html (GCC publishes no formal EOL table: it maintains ~3 branches and closes the oldest after a final release — a datum to confirm each time), LLVM releases via
https://github.com/llvm/llvm-project/releases.atom, and the MSVC notes if the project supports it.
- Real support in the target libc for whatever is used from C23 (
<stdckdint.h>, memset_explicit, <threads.h>) — glibc, musl and newlib are not at the same level; check in the libc documentation, not the compiler's.
- OpenSSF Compiler Options Hardening Guide (living document, it changes): https://best.openssf.org/Compiler-Hardening-Guides/ — reread before freezing the flag set.
- Maintenance state and licence of every tool before pinning it as a default: precedents in the catalogue (Trivy changed licence; gitleaks declared itself feature complete and its action requires a commercial licence for organisations since v2). Check via the releases Atom feed (
/releases.atom) and via the raw LICENSE from raw.githubusercontent.com, not via what an aggregator says. Special attention to: CMocka (GitHub mirror frozen; real source cmocka.org), cppcheck (open source GPL-3.0 vs. commercial Premium edition) and libFuzzer (no active feature development in LLVM).
- MISRA: current edition and addenda at https://misra.org.uk/publications/ (verified: MISRA C:2025, March 2025). Verify which edition the contracted analysis tool actually supports — it usually lags behind.
- CERT C: the SEI's official wiki for the specific rule before citing it as a standard.
- Sanitizers: the supported combinations and the exact flags change between releases — Clang/GCC documentation for the version in use, not articles.
Declared gaps (not verified as of Aug 2026, verify before using as a standard):
- Version and support state of MSVC for C (its C frontend has lagged far behind on C11/C17/C23): not verified.
- Formal EOL dates of the GCC 14/15/16 branches and of the LLVM releases: not verified (GCC publishes no EOL table; the ~3 active branches pattern is description, not commitment).
- Exact state of
-ftrivial-auto-var-init=zero and of -fsanitize=cfi in the project's specific GCC/Clang version: not verified per version.
- Verbatim licences of Criterion, CMocka and GSL/other dependencies mentioned in passing: not verified verbatim (Unity → MIT is, verified in its
LICENSE.txt; cppcheck → GPL-3.0 according to the project, confirm in the file).
- Real C23 coverage in each target's libc (glibc/musl/newlib): not verified.
If the web contradicts this document, the web wins — flag the discrepancy.
1---2name: c-standards3description: C language engineering standards (staff-level). Trigger on .c files and C-only headers, -std=c11/c17/c23/gnu23, gcc/clang C invocations, Makefile/CMakeLists.txt/meson.build building C targets, compile_commands.json, .clang-tidy, .clang-format, cppcheck, -fanalyzer, -fsanitize=address/undefined/memory/thread, valgrind/memcheck, libFuzzer/AFL++/OSS-Fuzz harnesses, MISRA C or CERT C compliance, Unity/CMocka/Criterion tests, glibc/musl/newlib targets, _FORTIFY_SOURCE and binary hardening flags, or extern "C" ABI headers.4---56# C standards78Criteria verified as of **August 2026**. Re-verify on the web before committing to anything (§8).910## 1. Scope and triggers1112Applies to all work in C: `.c` files and C headers, choice of `-std=`, compiler and linker flags, `Makefile`/`CMakeLists.txt`/`meson.build` for C targets, `compile_commands.json`, sanitizer configuration, static analysis (`-fanalyzer`, clang-tidy, cppcheck), fuzzing, MISRA C / CERT C conformance, unit tests and hardening of the produced binary. Covers firmware/embedded, user-space drivers, system libraries, CLIs and high-performance code. It sets **criteria**: what is used, what is vetoed and what must be verified. It is not a C tutorial.1314**Central axis of this skill**: in C the compiler manages neither resources nor bounds checking, so **the lifecycle of every resource and undefined behavior are design requirements**, not implementation details. Everything else (build, tests, CI) exists to make that discipline verifiable.1516**Not applicable**: see `cpp-standards` (everything it decides about C++: RAII, templates, the C++ standard library, `new`/`delete`, exceptions, target-oriented CMake for C++ and the memory safety debate in the ISO C++ committee — **no** C++ here). **Arbitration criteria** for code that compiles in both: the **standard the target is compiled under** wins (`-std=c23` → this skill; `-std=c++23` → `cpp-standards`), **not** the file extension nor the code style; a header consumed from both worlds is designed under `c-standards` (C subset, no C++-only constructs) and its `extern "C"` wrapper is a shared boundary: the ABI guarantee is set by this skill, consumption from C++ is set by `cpp-standards`. See also `rust-standards` (language choice for **new systems code** — C is not the default by inertia — and the Rust side of the FFI: `bindgen`/`cbindgen`, `#[repr(C)]`, `unsafe`), `bash-linux-scripting-standards` (build scripts, wrappers and shell automation), `linux-hardening-standards` (hardening of the **system**: kernel, sysctl, MAC, systemd; hardening of the **binary** — RELRO, PIE, stack protector, CFI — belongs to this skill), `kernel-drivers-standards` (**reciprocity already declared from its §1: when a rule of this skill clashes with the kernel, the kernel wins** — there is no libc, there is no `malloc`, style and sanitizers are the tree's own), `appsec-standards` (threat modelling and the AppSec process), `vulnerability-management-standards` (CVE triage, patching SLA, VEX), `gpu-computing-standards` (CUDA/HIP, kernels and GPU toolchain), `cicd-standards` (pipeline design and gates; here only which tool runs and with which flags), `offensive-security-standards` (exploitation), `kubernetes-standards` (OCI image of the binary), `linux-storage-standards` and `observability-standards` for what belongs to them. Neighbouring languages: `zig-standards` (**already written**) for the modern alternative to C — **and for the special case of `zig cc`: using Zig as a cross-compilation toolchain is their decision, but the C it compiles remains subject to this skill**—, `nim-standards` (**already written**: Nim **generates C** and compiles it with a C compiler; the Nim is theirs, **the generated C and the flags it is compiled with fall here**), `lua-standards` (**already written**: the **Lua C API** —stack, `lua_State`, `luaL_*`, value lifecycle against the collector— is a shared boundary; the C of the native extension and its correctness belong to this skill, the contract with the interpreter is theirs), `assembly-standards` (**already written**) for inline assembly and intrinsics — **the `asm` inside a C function is a shared boundary**: the compiler constraint and the correctness of the `asm` are theirs, the rest of the function is ours—, `objective-c-standards` (**already written**: Objective-C is a superset of C, so **the C contained in a `.m` remains subject to this skill**; what is specific to the Objective-C runtime is theirs).1718## 2. Default toolchain1920> **Verify the latest version on the web before pinning it in a real project** (§8). The following is the state verified as of **Aug 2026**.2122| Decision | Default | Justifiable alternative | Reason |23|---|---|---|---|24| Standard | **C17** (`-std=gnu17`) for portable production code; **C23** (`-std=gnu23`) if the project's minimum toolchain is GCC ≥ 15 / Clang ≥ 18 and there are no vendor compilers involved | C11 in legacy codebases or frozen vendor toolchains | GCC states *"GCC has support for ISO C23, the 2023 revision of the ISO C standard (published in 2024)"* and *"C23 mode is the default since GCC 15"*; Clang accepts `-std=c23` since Clang 18 but its C23 support is **partial, paper by paper** (see `clang.llvm.org/c_status.html`). C99 is the absolute minimum: **forbidden** C89/K&R in new code |25| Compiler | **GCC 16.x** (16.1, 2026-04-30) or **Clang/LLVM 22.x** (22.1.8, 2026-06-16) | Vendor toolchain when the silicon forces it | Compile **with both** in CI: each diagnoses what the other stays silent about |26| Build | **CMake ≥ 4.4** for libraries/projects others consume; **Meson ≥ 1.11** when the project is Linux-centric and readability is the priority | Make only in single-binary projects with no external dependencies | Hand-written Make does not scale to cross-compilation, sanitizer builds and `compile_commands.json` |27| Static analysis | `clang-tidy` (same release as the Clang in use) + `gcc -fanalyzer` | `cppcheck` 2.21.x (GPL-3.0) as a third opinion | `-fanalyzer` is the most mature **for C**: in C++ it is *best-effort*; in C it is usable as a gate |28| Tests | **Unity** 2.7.0 (MIT, 2026-07) in embedded and code without a full libc; **Criterion** 2.4.3 (2025-10) on Linux/POSIX when per-process isolation is wanted | CMocka 2.0.x when already in use (mocking by *link-time wrapping*) | Verify maintenance before pinning: the **CMocka mirror on GitHub is frozen at 1.1.5 (2019/2022)**; the authoritative source is `cmocka.org` / cryptomilk's git, which announces the 2.0 series |29| Fuzzing | **libFuzzer** for a fast in-process harness + **AFL++** 5.02c (2026-06) for long campaigns | Honggfuzz | libFuzzer is **deprecated in LLVM upstream** in the sense that it receives no new features: verify its state in the release notes of the specific version before basing a whole programme on it |30| Dynamic memory | The target libc's `malloc` | Custom allocator / arenas only with measured justification | A custom allocator shifts the burden of proof: it demands its own ASan+fuzzing |3132Toolchain rules:33- **No implicit compiler flags**: the full compilation line lives in the build system and is versioned. `CFLAGS` injected by the environment are **appended**, not substituted.34- `compile_commands.json` always generated (`CMAKE_EXPORT_COMPILE_COMMANDS=ON` / Meson generates it by itself). Without it there is no clang-tidy, no clangd and no reproducible analysis.35- Cross-compilation via a versioned *toolchain file* (CMake) or *cross file* (Meson) — **forbidden** to detect the cross-compiler with ad-hoc conditionals.36- Minimum warning set, and **it is a gate**:3738```39-std=gnu17 -Wall -Wextra -Werror40-Wshadow -Wconversion -Wsign-conversion -Wdouble-promotion41-Wformat=2 -Werror=format-security42-Wcast-qual -Wcast-align -Wpointer-arith -Wwrite-strings43-Wstrict-prototypes -Wold-style-definition -Wmissing-prototypes -Wmissing-declarations44-Wvla -Walloca -Wstack-protector45-Wnull-dereference -Wimplicit-fallthrough46-Werror=implicit-function-declaration -Werror=incompatible-pointer-types -Werror=int-conversion47```48- The four `-Werror=` on the last line are **non-negotiable**: in C17 and earlier those three constructs are diagnostics that many compilers accepted with a warning and are a direct source of memory corruption. C23 turns them into language errors; forcing them beforehand avoids the surprise on migration.49- `-Wconversion`/`-Wsign-conversion` are noisy in existing code: they are enabled **per new module** and extended from there; they are never switched off globally to "fix the build".50- `-Werror` **always in CI**; in the developer's local build it may stay as a warning so as not to break the flow, but merging requires the CI build.5152## 3. Structure, conventions and resource lifecycle5354### Layout55- `src/` implementation, `include/<project>/` public headers (always with a project directory: the consumer's `#include`s are `<project/foo.h>`), `tests/`, `fuzz/`, `cmake/` or `meson/`.56- **One public header = one contract**. Everything else is `static` or lives in internal headers outside `include/`. Visibility enforced in the linker: `-fvisibility=hidden` + an explicit export macro in shared libraries.57- Include guards `#pragma once` if every compiler in the project supports it; if there is a dubious vendor toolchain, classic guards with a unique name `PROJECT_MODULE_H`.58- Project prefix **mandatory** on every symbol with external linkage (`px_buffer_new`): C has no namespaces and the linker resolves collisions silently with absurd results.59- Public headers: types and functions only, zero unnecessary `#include`s (forward declaration where it suffices), zero unprefixed macros, and `extern "C"` guarded by `#ifdef __cplusplus` in every header a C++ consumer might include.6061### Resource lifecycle — the axis of the language62- **Explicit ownership rule**: every pointer crossing an API boundary has documented in the header who frees it and with which function. Without that line, the API is incomplete.63- **Paired constructor/destructor per type**: `T *t_new(...)` / `void t_free(T *)`, and `t_free(NULL)` is a *no-op* (like `free`). Forbidden to spread the freeing across the caller field by field.64- **A single exit point for cleanup** in functions with several acquisitions: cascading `goto fail_*` pattern in reverse order. It is the correct idiom in C, not a forbidden *goto* — MISRA C:2025 **withdraws** the single-exit-point rule, but the project criteria remain: centralised cleanup, in reverse order.65- After freeing, **the pointer is nulled** in the owning structure (`p = NULL`) if its life continues: use-after-free becomes a null deref, which is a deterministic failure.66- **Ownership is not shared by default**. If sharing is needed, explicit refcount with the policy documented and a concurrency test; never "it gets freed at some point".67- `alloca` and VLAs: **forbidden** (`-Walloca -Wvla`). Stack size is a finite and uncheckable resource; a VLA with a size derived from input is a remote stack overflow.68- Buffers with input-derived size: check the calculation **before** multiplying (`__builtin_mul_overflow` / `ckd_mul` from `<stdckdint.h>` in C23), not afterwards.69- Closing descriptors and `FILE*`: same pattern as memory; check the return of `fclose`/`close` when there was a write (I/O errors show up there, not in `write`).7071### Undefined behavior as a first-class bug72A UB is not "something that usually works": the optimiser **assumes it does not happen** and deletes the code that checks for it. It is treated as a high-severity defect even if the current binary works.73- Categories reviewed in every code review: out-of-bounds access, use-after-free/double-free, reading uninitialised memory, **signed** integer overflow, shift ≥ the type's width or with a negative operand, *strict aliasing* violation, misaligned pointers, `memcpy` with overlapping pointers (`memmove` is what applies), pointer arithmetic outside the object (including `ptr + n` beyond "one past the end"), comparison of pointers to distinct objects, division by zero, `NULL` passed to `<string.h>` functions even with length 0, and *data races*.74- The arithmetic ones are caught at runtime with UBSan; the aliasing ones are not: if the project does *type punning*, it is done with `memcpy` or with a union, **never** with a pointer cast, or it is compiled with `-fno-strict-aliasing` **declared and justified in the build** (a legitimate option, but a project decision, not a silent patch).75- `-fwrapv` / `-fno-strict-overflow` as a safety net in inherited codebases: acceptable and explicit, but it does not replace fixing the overflow; and it is not portable to vendor compilers.7677### Integers78- `size_t` for sizes and indices; **never** `int` for lengths. Fixed-width types (`<stdint.h>`) for binary formats, protocols and hardware registers; `int`/`long` only for local arithmetic.79- **Implicit promotions and conversions** are the most frequent bug in the language: `-Wconversion -Wsign-conversion` enabled and zero casts "to silence the warning". A cast is an assertion that the range has been checked; if it has not, it is a bug wearing makeup.80- Comparing `signed` with `unsigned` is vetoed (`-Wsign-compare` comes with `-Wextra`); in C23 there is `<stdckdint.h>` (`ckd_add`/`ckd_sub`/`ckd_mul`) — verify real availability in the target's libc before depending on it; in C17, GCC/Clang's `__builtin_*_overflow`.81- **Signed** integer overflow **is UB**; unsigned overflow is defined but is usually just as much of a bug (size truncation). Both are checked.82- `char` without an explicit `signed`/`unsigned` has *implementation-defined* signedness: for bytes, `uint8_t` or `unsigned char`.8384### Forbidden functions and their real replacement85| ❌ Forbidden | Replacement | Note |86|---|---|---|87| `gets` | `fgets` + check the `\n` | Removed from the language since C11 |88| `strcpy`, `strcat` | Explicit checked length: measure and `memcpy`; `snprintf` for composition | `strncpy` is **not** the replacement: it does not guarantee termination |89| `strncpy` | `snprintf`, or `memcpy` with manual termination | Pads with zeros and truncates without warning |90| `sprintf`, `vsprintf` | `snprintf`/`vsnprintf` **checking the return** (`>= size` is truncation, and truncating is usually a logic bug) | |91| `atoi`, `atol`, `atof` | `strtol`/`strtoll`/`strtod` with `errno` set to 0 beforehand and `endptr` checked | `atoi` does not distinguish error from zero: UB on overflow |92| `alloca`, VLA | `malloc` + limit, or a fixed sized buffer | |93| `rand`, `random` | The OS CSPRNG (`getrandom`, `arc4random_buf`, `BCryptGenRandom`) for any security use | |94| `system`, `popen` with a composed string | `posix_spawn`/`fork`+`execve` with a vectorised `argv` | Concatenating input into a command is injection |95| `strtok` | `strtok_r`/`strsep` | Global state: not reentrant |96| `gmtime`, `localtime`, `ctime`, `asctime` | The `_r` variants | Shared static buffer |97| `memcpy` over overlapping regions | `memmove` | UB even when it "works" |98| `scanf("%s")` | `fgets` + parsing, or `%<n>s` with a width | |99| Annex K (`strcpy_s`, `_s`) | Do not adopt it as a strategy | Real support is almost non-existent outside MSVC and questioned by WG14: it is **not** the portable solution |100101Mechanical enforcement: a list of vetoed symbols in clang-tidy (`bugprone-unsafe-functions`, `cert-*`) and, in high-assurance projects, `--defsym`/wrap or a linker check on the imported symbol.102103### Concurrency104- One threading model per project: `pthreads` on POSIX or C11 threads (`<threads.h>`) if the toolchain really has them (verify: glibc exposes them since 2.28; some libcs do not). **Do not mix**.105- Every shared variable is protected by a mutex or is `_Atomic` with an **explicit and justified** memory order; `volatile` is **not** a concurrency primitive (it is for MMIO and `sig_atomic_t`, nothing more).106- Lock acquisition order documented and global; no nested locks without a hierarchy.107- Signal handlers: only *async-signal-safe* functions and `volatile sig_atomic_t`; the correct pattern is to write to a pipe/eventfd (*self-pipe*) and process it in the main loop.108- Data race = UB. TSan is the gate.109110## 4. Quality: formatting, analysis, tests and CI gates111112### Formatting113- `clang-format` with a versioned `.clang-format` (LLVM or GNU base, tuned once and not re-debated). `clang-format --dry-run --Werror` in CI.114- **Forbidden** to reformat en masse alongside functional changes: the formatting commit goes separately and is recorded in `.git-blame-ignore-revs`.115116### Static analysis (in order of increasing cost)1171. Compiler warnings with `-Werror` (already covered, zero cost).1182. `gcc -fanalyzer` over the C tree: catches double free, use-after-free, leaks, NULL derefs and descriptor misuse. Noisy in macros; it gets triaged, not switched off. *(Verified state: its C++ support is incomplete/best-effort — irrelevant here, this is the C skill.)*1193. `clang-tidy` with a versioned `.clang-tidy`. Starting set: `bugprone-*`, `cert-*`, `clang-analyzer-*`, `misc-*`, `performance-*`, `portability-*`, `readability-*` (trimming the style ones that clash with clang-format), and `-checks=-readability-magic-numbers` only if the project has its own constants policy. `WarningsAsErrors` for the stabilised part.1204. `cppcheck --enable=warning,style,performance,portability --error-exitcode=1` with versioned suppressions (`suppressions.txt`) — a third opinion, a different family of heuristics. Verify the current licence before pinning it as a corporate default (as of Aug 2026 the open project is GPL-3.0 and there is also a commercial *Premium* edition: they are different products).1215. **CodeQL** in the repo (or the provider's equivalent) for data-flow queries across translation units, scheduled as well as on PR.122123No `// NOLINT` without a specific lint and a reason; no `#pragma GCC diagnostic ignored` without `push`/`pop` and a comment.124125### Sanitizers (separate builds, never a single one "with everything")126- **ASan + UBSan + LSan** in the same build: `-fsanitize=address,undefined -fno-omit-frame-pointer -fno-sanitize-recover=all -g -O1`. LSan comes with ASan on Linux. This is the default test build in CI.127- **TSan** in a **separate** build: `-fsanitize=thread`. **Incompatible with ASan**: the runtimes assume different memory maps and the compiler emits a hard error when combining them. It implies PIE and requires instrumenting all the code.128- **MSan** in a **separate** build and Clang/Linux only (`-fsanitize=memory -fPIE -pie -fno-omit-frame-pointer -fno-optimize-sibling-calls -O1`). It requires **all** dependencies, including libc++/libc as the case may be, to be instrumented: without that it produces false positives. If that cost is not bearable, coverage of uninitialised memory reads is covered with **Valgrind Memcheck**, which remains the practicable route in that gap.129- `-fno-sanitize-recover=all` mandatory: a finding must **abort** the test, not print and continue. `UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1`, `ASAN_OPTIONS=detect_stack_use_after_return=1:strict_string_checks=1:detect_leaks=1`.130- **Valgrind still has its place**, and for concrete reasons, not out of nostalgia: it works on **already compiled** binaries (third-party code, blobs, already-published artifacts) and covers uninitialised memory without recompiling the world. In exchange it does not see overflows in local or global variables nor *use-after-return*, and its cost is an order of magnitude over ASan. Criteria: ASan+UBSan on every PR, TSan in a separate job, Valgrind on a periodic run or to triage a binary that cannot be recompiled.131- Production is **never** deployed with sanitizers active (they are debugging tools and they widen the attack surface: `ASAN_OPTIONS` is read from the environment).132133### Fuzzing134- **Every parser, decoder, deserialiser or untrusted input entry point has a fuzzing harness**. It is not optional: it is where the bugs appear that the fuzzer finds in minutes and human review does not see in years.135- `LLVMFuzzerTestOneInput` harness (compatible with libFuzzer and AFL++ via `afl-clang-lto`), compiled with ASan+UBSan, **versioned** and minimised corpus (`-merge=1`), dictionary when the format has one.136- CI: a short run (60–300 s) of the corpus on every PR as a regression test; a long scheduled campaign (nightly/weekly) with AFL++.137- Relevant open source project: integrate it into **OSS-Fuzz** (Google, active — daily commits as of Aug 2026). Every *crash* reported by the fuzzer enters as a bug with a regression test in the corpus.138- The fuzzed binary is **not** the release binary: different builds.139140### Testing141- Framework as per §2; each test is an isolated case, with no shared global state, and the runner reports in a CI-consumable format (TAP/JUnit).142- Mandatory coverage of **edges and errors**, not the happy path: length 0, maximum length, `NULL`, boundary values of each integer type, `malloc` failure (fault injection via wrapper or `LD_PRELOAD`), truncation, unterminated input.143- **Every fixed bug leaves a regression test** that fails before the fix; if it came from a fuzzer, it also enters the corpus.144- Coverage as a signal: `--coverage`/`llvm-cov` published, focused on error branches. A percentage is not a target.145- Deterministic tests: no dependence on timing, hash order or the network. A flaky test is fixed or deleted.146147### Minimum CI gate (everything breaks the build)148```1491. clang-format --dry-run --Werror1502. build GCC -Wall -Wextra -Werror (+ set from §2)1513. build Clang -Wall -Wextra -Werror (+ set from §2)1524. clang-tidy over compile_commands.json (diff or whole tree)1535. gcc -fanalyzer1546. tests under ASan+UBSan (-fno-sanitize-recover=all)1557. tests under TSan (separate job, if there are threads)1568. short fuzz corpus over each harness1579. dependency SCA + SBOM of the artifact15810. clean release build with the hardening flags from §5 and verification of the binary159```160Main always green. Nothing is merged with CI red.161162## 5. Stack security and binary hardening163164### Hardening flags (release build)165Base aligned with the *OpenSSF Compiler Options Hardening Guide for C and C++* (a living document: re-verify before freezing it in a project):166167```168-O2 -Wall -Wformat -Wformat=2 -Wconversion -Wimplicit-fallthrough169-Werror=format-security170-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3171-fstrict-flex-arrays=3172-fstack-clash-protection -fstack-protector-strong173-fPIE -pie174-Wl,-z,relro -Wl,-z,now175-Wl,-z,noexecstack -Wl,-z,nodlopen176-Wl,--as-needed -Wl,--no-copy-dt-needed-entries177```178- `-U_FORTIFY_SOURCE` before `-D_FORTIFY_SOURCE=3` **matters**: many distros already define it and redefining it without undefining warns or is ignored. `_FORTIFY_SOURCE` requires `-O1` or higher and depends on the libc (glibc implements it; on musl it is essentially inoperative — **verify on the real target**, do not assume it).179- **Also add** (with impact measurement):180 - `-ftrivial-auto-var-init=zero`: eliminates the whole class of "reading an uninitialised local" in production. Low cost; verify support in the project's specific GCC/Clang version.181 - `-fcf-protection=full` (x86-64) and `-mbranch-protection=standard` (AArch64) for hardware CFI.182 - `-fsanitize=cfi` with LTO (Clang) or `-fsanitize=undefined -fsanitize-minimal-runtime -fsanitize-trap=undefined` as a minimal-cost UBSan **in production** to turn detectable UB into a deterministic *trap* — a project decision, measure first.183 - `-fno-delete-null-pointer-checks` and `-fno-strict-aliasing` in inherited codebases whose correctness cannot be fully audited (declared and justified).184- **Verify the produced binary**, do not trust the flags: `checksec`, `hardening-check` or `readelf -d` in CI, checking PIE, full RELRO, NX, stack canary and the absence of unsafe `RPATH`/`RUNPATH`. A flag being in `CFLAGS` does not mean it reached the link.185- Symbol strip on the release artifact, with **build-id and debug symbols archived separately** (`objcopy --only-keep-debug`) so crashes can be symbolised.186187### Supply chain and dependencies188- **Every C dependency is code that will run with the process's privileges**: it is justified in writing. Prefer the libc and what is already present over adding a library for one utility.189- Dependencies by exact *pin* (tag + hash), never `master`. Vendoring (submodule/subproject with a pinned revision) is acceptable and often preferable to a package manager in C; what is not acceptable is copying code without a record of origin, version and licence.190- **SBOM** (SPDX or CycloneDX) generated in the build and published with the artifact; SCA against CVEs of the embedded libraries. Vendored code is exactly where CVEs get lost.191- Licences verified in the project's real `LICENSE`, not in whatever an aggregator says.192193### Code194- **All external input is validated at the boundary** (length, range, termination, encoding) before touching the rest of the programme. A binary format parser always assumes hostile input.195- Check **every** return: `malloc`, `realloc` (never `p = realloc(p, n)` — the original pointer is lost if it fails), `snprintf`, `read`/`write` (partials), `fclose`. Ignoring a return is a decision that gets marked (`(void)` + comment), not an oversight.196- Secrets: never in code or in logs; erase sensitive material with `explicit_bzero`/`memset_explicit` (C23) — a normal `memset` gets removed by the optimiser. Consider `mlock` for keys and prevent them reaching *core dumps* (`prctl(PR_SET_DUMPABLE, 0)` where applicable).197- Crypto: use an audited library (libsodium, OpenSSL 3.x, BoringSSL/mbedTLS depending on the target). **No rolling your own crypto**, nor comparing secrets with `memcmp` (constant time: `sodium_memcmp`/`CRYPTO_memcmp`).198- Path and file comparison: `openat`/`O_NOFOLLOW`/`O_CLOEXEC` and temporary files with `mkstemp`; no check-then-open (TOCTOU).199- `O_CLOEXEC`/`SOCK_CLOEXEC` by default on every descriptor: leaking descriptors to child processes is a silent escalation.200201### MISRA C and CERT C — when they apply202- **CERT C**: applicable to any C project with a security surface; its rules overlap with clang-tidy `cert-*`. It is adopted as a set of automated checks, not as a document to read.203- **MISRA C**: mandatory only when the domain requires it (automotive/ISO 26262, IEC 61508, avionics/DO-178C, medical devices/IEC 62304). Current edition verified: **MISRA C:2025** (published March 2025), successor to MISRA C:2023; covers C90/C99/C11/C18, ~225 active guidelines, and **withdraws** the historical single-exit-point rule (although IEC 61508 / ISO 26262 may still require similar practices on their own). It treats AI-generated code the same as hand-written code for conformance purposes.204- MISRA requires **MISRA Compliance:2020** as its framework: guideline matrix, documented and approved deviations, and evidence. Adopting MISRA "in name" without that framework is not conformance, it is theatre.205- **Do not** apply MISRA to a project that does not need it: it penalises legitimate constructs and consumes review budget that pays off better in sanitizers and fuzzing.206207## 6. Performance, ABI, portability and operability208209### Performance210- Measure before optimising: `perf`, `flamegraph`, `cachegrind`. A performance change with no measured before/after number does not get merged.211- Reproducible benchmarks (fixed frequency, CPU *pinning*, several repetitions with reported spread); no comparing timings from a single run.212- `-O2` is the default; `-O3` only if the benchmark justifies it on that specific binary. `-march=native` **forbidden** in distributable artifacts (it produces binaries that fail with SIGILL on another machine); *runtime dispatch* or *function multiversioning* if AVX-512 is needed.213- LTO (`-flto=thin` on Clang, `-flto` on GCC) by default in release if build time allows; verify that it does not break with alias symbols/`__attribute__((used))`.214- Hand micro-optimisations are the last resort; the 2026 compiler wins almost always. The real gain is in the algorithm and in the memory access pattern.215216### ABI and interoperability217- **The ABI is a contract**: changing the size or layout of a public struct, the order of an enum, the prototype of an exported function or the *calling convention* is a **breaking change** even if the API compiles.218- Shared libraries: versioned `SONAME`, `-fvisibility=hidden` + export macro, and a **version script** (`--version-script`) to control the exported set. `abi-compliance-checker` or `abidiff` (libabigail) in the CI of libraries with an ABI contract.219- Public structs: prefer **opaque types** (`typedef struct px_ctx px_ctx;`) so they can evolve without breaking the ABI. If the struct must be public, no fields are added in the middle and the padding is documented.220- FFI: the C boundary is the lowest common denominator for Rust/Python/Go/C++. Rules: no platform-dependent types in the signature (use `<stdint.h>`), fixed-size integers, ownership documented on every parameter, **no propagating `errno` as a contract** across languages, and a `*_free` function exported for every object the library returns (the consumer cannot call another libc's `free` — on Windows it is a straight failure).221- Consumption from C++: header with `#ifdef __cplusplus extern "C" {`, no `bool` from `<stdbool.h>` in the signature if old compilers are involved, no *flexible array members* in types C++ must define, and no names that are reserved words in C++ (`class`, `new`, `template`, `operator`...). The C++ side of the contract is set by `cpp-standards`.222223### Portability224- Explicitly declare the set of supported targets (architecture, libc, OS, compiler and minimum version) in the README **and test them in CI**. What is not tested is not supported.225- Assume: `char` may be signed or unsigned; integers may be 32 or 64 bits (`long` is 32 on Windows and 64 on Linux — use `<stdint.h>`); byte order varies; misaligned accesses abort on some architectures; the evaluation order of a function's arguments is unspecified.226- Feature test macros (`_POSIX_C_SOURCE`, `_GNU_SOURCE`) defined in the build system, not scattered across the `.c` files.227- No dependence on the behaviour of a specific compiler version; `__builtin_*` and `__attribute__` are wrapped in macros with a fallback.228229### Operability230- Structured logging to `stderr` (or syslog/journald depending on the deployment) with a configurable level; **no debug `printf`** in production code.231- Exit codes with meaning; errors via `errno`-like or a documented custom enum — never magic codes without a table.232- Clean exit on SIGTERM/SIGINT: *self-pipe*, draining of work in progress, resource release. A daemon that only dies by SIGKILL is not deployable with a rolling update.233- Crashes: `build-id`, archived symbols, `core_pattern`/coredumpctl or a crash collector; a crash without a symbolisable trace is wasted time.234- `assert` is not error handling and **disappears with `NDEBUG`**: never put side effects inside it, nor use it to validate external input. For invariants that must hold in release, an explicit check with `abort()` or a `static_assert` (C11+) if it is compile-time.235236## 7. Sustainability: upgrades and prohibitions237238**Cadence**: upgrade the compiler at least once a year (GCC publishes one major annually, ~April; LLVM every ~6 months) and test the next one in CI **before** it becomes mandatory — a three-version jump accumulates new warnings and optimisation changes that expose latent UB. A new failure after a compiler upgrade is, by default, your own bug, not a compiler regression. Language standard: review the migration to C23 when the project's minimum toolchain allows it; C2y/C29 (working draft in progress, publication expected at the end of the decade) is **not** used in production.239240**Deprecation**: in libraries with an ABI contract, deprecate with `__attribute__((deprecated("use X")))`, a window of at least one major version, and a new `SONAME` when the ABI breaks. In internal binaries, compatibility flags are not accumulated "just in case".241242**Conscious debt**: every shortcut leaves `/* TODO(user): reason — issue #N */`. Every lint/sanitizer suppression comes with a reason and a review date.243244**FORBIDDEN** (requires written justification and approval to make an exception):245- ❌ The functions in the §3 table (`gets`, `strcpy`, `strcat`, `strncpy`, `sprintf`, `atoi`, `alloca`, `strtok`, `rand` for security, `system` with a composed string...).246- ❌ VLAs and `alloca` with an input-derived size; stack buffers sized "with margin".247- ❌ Ignoring the return of `malloc`/`realloc`/`snprintf`/`read`/`write`/`fclose`; `p = realloc(p, n)`.248- ❌ Pointer casts for *type punning* (strict aliasing violation); casts to silence `-Wconversion`.249- ❌ `-Werror` disabled in CI; `#pragma GCC diagnostic ignored` without `push`/`pop` and a reason; `// NOLINT` without a specific lint.250- ❌ Combining ASan with TSan/MSan in the same build (the compiler rejects it); deploying production with sanitizers.251- ❌ `-march=native` in distributable artifacts; `-O3` without a benchmark; `-ffast-math` in code that compares or validates floats.252- ❌ `volatile` as a synchronisation mechanism between threads; shared global variables without a lock or `_Atomic`.253- ❌ Pointer arithmetic outside the object; comparing pointers of distinct objects; `memcpy` with overlap.254- ❌ Public headers without a project prefix, without an include guard or dragging in unnecessary `#include`s; external symbols without a prefix.255- ❌ Mutable global state in libraries (breaks reentrancy and tests); functions with a static return buffer.256- ❌ Macros that do what a `static inline` function would do; multi-statement macros without `do { } while (0)`; macros that evaluate an argument twice.257- ❌ Home-made crypto, comparing secrets with `memcmp`, `memset` to erase secrets.258- ❌ Copying third-party code without a record of origin, version and licence; dependencies pointing at a branch instead of at tag+hash.259- ❌ An untrusted input parser **without a fuzzing harness**.260- ❌ Annex K (`*_s`) as a portability strategy.261- ❌ Declaring "it's only a warning": a compiler warning in C is a bug until proven otherwise.262- ❌ **Including in this skill or in the code it produces: exploits, ROP gadgets, concrete mitigation bypasses, shellcode or payloads.** This skill is **defensive**: it describes vulnerability classes (overflow, use-after-free, format string, TOCTOU, type confusion) **in order to prevent them**, never to exploit them. Offensive work → `offensive-security-standards`, with written scope and authorisation.263264## 8. Mandatory web verification265266Before pinning versions, flags or asserting the state of the ecosystem, **verify on the web** (never from memory):2671. **Real C23 state per compiler**: https://gcc.gnu.org/projects/c-status.html and https://clang.llvm.org/c_status.html (paper-by-paper table). Do not assume parity between GCC and Clang: as of Aug 2026 GCC declares C23 support and default since GCC 15; Clang accepts `-std=c23` since 18 with **partial** support.2682. **Versions and lifecycle**: https://gcc.gnu.org/develop.html + https://gcc.gnu.org/releases.html (GCC publishes no formal EOL table: it maintains ~3 branches and closes the oldest after a final release — **a datum to confirm each time**), LLVM releases via `https://github.com/llvm/llvm-project/releases.atom`, and the MSVC notes if the project supports it.2693. **Real support in the target libc** for whatever is used from C23 (`<stdckdint.h>`, `memset_explicit`, `<threads.h>`) — glibc, musl and newlib are **not** at the same level; check in the libc documentation, not the compiler's.2704. **OpenSSF Compiler Options Hardening Guide** (living document, it changes): https://best.openssf.org/Compiler-Hardening-Guides/ — reread before freezing the flag set.2715. **Maintenance state and licence** of every tool before pinning it as a default: precedents in the catalogue (Trivy changed licence; gitleaks declared itself *feature complete* and its action requires a commercial licence for organisations since v2). Check via the releases Atom feed (`/releases.atom`) and via the raw `LICENSE` from `raw.githubusercontent.com`, not via what an aggregator says. Special attention to: **CMocka** (GitHub mirror frozen; real source `cmocka.org`), **cppcheck** (open source GPL-3.0 vs. commercial *Premium* edition) and **libFuzzer** (no active feature development in LLVM).2726. **MISRA**: current edition and addenda at https://misra.org.uk/publications/ (verified: MISRA C:2025, March 2025). Verify which edition the contracted analysis tool actually supports — it usually lags behind.2737. **CERT C**: the SEI's official wiki for the specific rule before citing it as a standard.2748. **Sanitizers**: the supported combinations and the exact flags change between releases — Clang/GCC documentation for the version in use, not articles.275276**Declared gaps (not verified as of Aug 2026, verify before using as a standard)**:277- Version and support state of **MSVC** for C (its C frontend has lagged far behind on C11/C17/C23): **not verified**.278- Formal EOL dates of the GCC 14/15/16 branches and of the LLVM releases: **not verified** (GCC publishes no EOL table; the ~3 active branches pattern is description, not commitment).279- Exact state of `-ftrivial-auto-var-init=zero` and of `-fsanitize=cfi` in the project's specific GCC/Clang version: **not verified per version**.280- Verbatim licences of Criterion, CMocka and GSL/other dependencies mentioned in passing: **not verified verbatim** (Unity → MIT is, verified in its `LICENSE.txt`; cppcheck → GPL-3.0 according to the project, confirm in the file).281- Real C23 coverage in each target's libc (glibc/musl/newlib): **not verified**.282283If the web contradicts this document, **the web wins** — flag the discrepancy.