C and C++ still run operating systems, network appliances, databases and embedded devices, and their memory safety bugs remain the largest single source of serious vulnerabilities in that software. Chromium's security team reports that around 70% of its high severity security bugs are memory unsafety problems, and the CISA and NSA joint guidance on memory-safe languages asks organisations to reduce that exposure with language protections, safer libraries, tooling and training. Rewriting everything is rarely an option, so this guide covers what teams can do with the C and C++ they already ship: the coding patterns that prevent out-of-bounds access, use-after-free and integer errors, the compiler and linker hardening flags that limit what a surviving bug can do, and the sanitizers and fuzzers that find bugs before attackers do. It is written for C and C++ developers, build engineers and AppSec reviewers; the general principles are in the Secure Coding Guide.
Key takeaways
- Prevent first, harden second. Safe patterns (bounded copies, checked arithmetic, RAII ownership, bounds-checked containers) remove bugs; hardening flags only make the bugs that remain harder to exploit.
- Turn on the OpenSSF baseline flags.
-D_FORTIFY_SOURCE=3,-fstack-protector-strong,-fstack-clash-protection, full RELRO, non-executable stacks, PIE and control-flow protection cost little and are already standard in major Linux distributions. - Enable standard library hardening.
_LIBCPP_HARDENING_MODE_FAST(libc++) or_GLIBCXX_ASSERTIONS(libstdc++) turn out-of-boundsoperator[]and invalid ranges into a clean abort instead of silent corruption. - Sanitizers belong in test, not production. AddressSanitizer, UndefinedBehaviorSanitizer and ThreadSanitizer combined with fuzzing find most memory and concurrency bugs early; they are debugging tools and, apart from UBSan's minimal runtime, should not ship in release builds.
- Have a memory safety roadmap. Contain C and C++ to the components that need it, put parsers of untrusted input first in line for fuzzing, sandboxing or rewriting in a memory-safe language, and measure progress.
The bug classes that matter
Most exploitable C and C++ defects fall into a short list. The Memory Corruption Vulnerabilities Guide explains how each class is abused; this table focuses on what prevents and detects it in code you write.
| Bug class | Typical root cause | Safe pattern | Detected by |
|---|---|---|---|
| Stack or heap buffer overflow | strcpy, sprintf, memcpy with an attacker-controlled length | Length-carrying types (std::string, std::span), snprintf with checked return, explicit bounds checks | ASan, _FORTIFY_SOURCE, stack protector |
| Out-of-bounds read (information leak) | Trusting a length field from the network | Validate declared lengths against actual received bytes before use | ASan, fuzzing |
| Use-after-free, double free | Manual free/delete with unclear ownership; dangling references and iterators | RAII, std::unique_ptr, no raw owning pointers, no references into containers that may reallocate | ASan, libc++ extensive mode |
| Integer overflow and truncation | Size arithmetic (n * size), signed/unsigned mixing, narrowing to int | Checked arithmetic (__builtin_mul_overflow, C23 ckd_mul), size_t for sizes, -Wconversion | UBSan, compiler warnings |
| Uninitialised memory | Stack variables and heap buffers read before write | Initialise at declaration; calloc or value-initialisation | MemorySanitizer (Clang), -ftrivial-auto-var-init=zero as a backstop |
| Format string | User data passed as the format argument | printf("%s", s), never printf(s) | -Wformat=2, -Werror=format-security |
| Data race | Shared state without synchronisation | std::mutex, std::atomic, ownership transfer between threads | ThreadSanitizer |
| Command injection | system() or popen() with concatenated strings | posix_spawn or execve with an argument vector | Code review, static analysis |
The SEI CERT C Coding Standard gives these classes stable rule identifiers that are useful in review comments and static analysis configuration, for example ARR30-C (do not form or use out-of-bounds pointers or array subscripts), STR31-C (guarantee storage for strings has room for the data and the null terminator), MEM30-C (do not access freed memory), INT32-C (signed integer operations must not overflow) and FIO30-C (exclude user input from format strings).
Safe patterns in C
- Carry the length with the pointer: pass buffers as a pointer and a
size_tlength together, and check every copy against the destination size. Wrap this in a small internal buffer type rather than repeating the arithmetic. - Retire unbounded functions: ban
gets,strcpy,strcat,sprintfandvsprintfin new code (a poisoned-identifier header or static analysis rule enforces it). Note thatstrncpydoes not guarantee null termination; prefersnprintfand check its return value for truncation. - Check allocation arithmetic:
malloc(count * size)overflows silently. Usecalloc(count, size), which checks the multiplication, or an explicit overflow check before allocating. - Validate untrusted lengths: any length, offset or count read from a file or packet must be checked against both the bytes actually received and the destination size before it is used to copy, allocate or index.
- Null after free: set pointers to
NULLafterfreeand give each allocation one clear owner. This does not fix aliasing bugs, but it turns many use-after-free bugs into reliable crashes. - Check every return value: unchecked
read,malloc,setuidorsnprintfreturns are a common source of exploitable states. Use__attribute__((warn_unused_result))or C23[[nodiscard]]on internal APIs. - Avoid the shell: replace
system()andpopen()withposix_spawnorexecveand an argument array, with an absolute program path and a cleaned environment. See the Command Injection Guide.
Safe patterns in modern C++
- Ownership through types: use
std::unique_ptrfor single ownership andstd::shared_ptronly where ownership is genuinely shared. Raw pointers and references should be non-owning and must not outlive the object. - Containers and views: use
std::vector,std::arrayandstd::stringinstead of raw arrays, andstd::span(C++20) to pass a pointer and length as one value. Use.at()or a hardened standard library where bounds checks matter. - Lifetime traps:
std::string_viewandstd::spando not own their data. Returning a view into a temporary string, or holding an iterator or reference into astd::vectoracross apush_backthat reallocates, creates use-after-free bugs that look like safe code. - No C-style casts: use
static_castand avoidreinterpret_caston untrusted data; narrowing conversions should be explicit and checked. - Exceptions and RAII together: acquire resources in constructors and release them in destructors so early returns and exceptions cannot leak locks, file handles or memory.
- Follow a written profile: the C++ Core Guidelines (bounds, type and lifetime safety rules) are the most widely used reference and are checked by clang-tidy's
cppcoreguidelines-*checks.
Compiler and linker hardening flags
GCC and binutils do not enable most hardening by default upstream; Linux distributions add flags when packaging, so code built with a plain toolchain can miss protections the distribution packages have. The OpenSSF Compiler Options Hardening Guide for C and C++ is the most complete current reference. Its baseline for GCC and Clang on Linux:
| Flag | Type | What it does | Notes |
|---|---|---|---|
-Wall -Wformat -Wformat=2 -Wconversion -Wimplicit-fallthrough | Compile-time warnings | Flags format, conversion and switch defects | No run-time cost |
-Werror=format-security | Compile-time error | Rejects non-literal format strings with no arguments | Safe to use when distributing source |
-D_FORTIFY_SOURCE=3 (with -U_FORTIFY_SOURCE first) | Run-time check | Bounds checks in glibc memcpy, strcpy, sprintf and similar, including sizes known only as expressions | Needs -O1 or higher (-O2 recommended) and glibc |
-D_GLIBCXX_ASSERTIONS | Run-time check | Precondition checks in libstdc++ (for example operator[] bounds) | Small cost |
-D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_FAST | Run-time check | libc++ checks for out-of-bounds element access and invalid input ranges | Recommended for production by the libc++ project |
-fstrict-flex-arrays=3 | Compile-time behaviour | Only true [] trailing arrays are treated as flexible, so bounds checking covers the rest | GCC 13, Clang 16 or later for level 3 |
-fstack-protector-strong | Run-time check | Stack canaries on functions with local arrays or address-taken locals | Low cost |
-fstack-clash-protection | Run-time check | Probes large stack allocations so they cannot jump the guard page | Low cost |
-fcf-protection=full (x86_64), -mbranch-protection=standard (AArch64) | Run-time protection | Control-flow integrity using CET or branch protection and pointer authentication | Effective on supporting hardware and OS |
-fPIE -pie (executables), -fPIC -shared (libraries) | Code generation | Position-independent code so ASLR applies to the whole binary | Usually default on modern distributions |
-Wl,-z,relro -Wl,-z,now | Linker | Full RELRO: relocation tables read-only after load | Slightly slower start-up |
-Wl,-z,noexecstack, -Wl,-z,nodlopen | Linker | Non-executable stack; prevent the object being loaded with dlopen | nodlopen not for plug-ins |
-fno-delete-null-pointer-checks -fno-strict-overflow -fno-strict-aliasing -ftrivial-auto-var-init=zero | Production code generation | Stops the optimiser removing checks that rely on undefined behaviour, and zero-initialises automatic variables | Some performance cost; measure |
- GCC shortcut: GCC 14 added
-fhardened, a predetermined hardening set for GNU/Linux targets, with-Whardenedwarning when a flag it implies is downgraded. The exact set changes between releases; inspect it with--help=hardened. - -Werror: the OpenSSF guide recommends blanket
-Werrorduring development but not in distributed source, where it ties builds to particular compiler versions. Promote specific warnings with-Werror=instead. - MSVC equivalents:
/sdlenables a superset of the/GSbuffer checks, turns warnings such as C4700 (uninitialised local), C4703 (potentially uninitialised pointer) and C4789 (buffer overrun in CRT calls) into errors, enables strict GS checking, and sanitises pointers afterdelete. Pair it with/guard:cf(Control Flow Guard),/DYNAMICBASE,/HIGHENTROPYVA,/NXCOMPATand/CETCOMPATat link time. - Verify the output, not the makefile: check built binaries for PIE, RELRO, canaries, NX and fortified functions with a binary inspection tool such as checksec or
readelf, because flags are often lost in sub-builds and vendored libraries.
Sanitizers, fuzzing and static analysis
| Tool | Flag | Finds | Where to run |
|---|---|---|---|
| AddressSanitizer | -fsanitize=address | Out-of-bounds access, use-after-free, double free | Unit tests, fuzzing, CI |
| UndefinedBehaviorSanitizer | -fsanitize=undefined | Signed overflow, invalid shifts, misaligned access, null dereference | Unit tests, fuzzing |
| ThreadSanitizer | -fsanitize=thread | Data races | Concurrency tests (separate build from ASan) |
| LeakSanitizer | -fsanitize=leak | Memory leaks | Test runs, long-running service soak tests |
| Coverage-guided fuzzer | libFuzzer (-fsanitize=fuzzer) or AFL++ | Crashes in parsers and decoders, when combined with ASan and UBSan | Continuous fuzzing, OSS-Fuzz for open source |
| Static analysis | clang-tidy, Clang Static Analyzer, GCC -fanalyzer, commercial SAST | Banned functions, CERT and Core Guidelines violations, some lifetime errors | Pull requests (diff-based gating) |
The OpenSSF guide warns that sanitizers are not hardening: AddressSanitizer in particular changes run-time behaviour in ways that have enabled local privilege escalation in setuid programs, so it must not ship in production builds. The one exception the guide names is UndefinedBehaviorSanitizer with a minimal runtime (-fsanitize-minimal-runtime in Clang), which is designed for production use. The Fuzzing Guide covers harness design and corpus management.
How to roll this out
- Inventory and baseline (weeks 1 to 2): list every C and C++ component, its toolchain and its exposure (network-facing parsers first). Run a binary hardening check on current release artefacts to see which protections are actually present.
- Warnings in audit mode (weeks 2 to 4): add the OpenSSF warning flags without
-Werror, count the findings per component, and fix format and conversion warnings in parsers first. Then make new warnings fail the build for changed files. - Run-time hardening in a pilot ring (month 2): enable
_FORTIFY_SOURCE=3, stack protector, stack clash protection, RELRO, PIE and standard library hardening for one service. Run the full test suite and a performance benchmark, deploy to a canary ring, and watch crash rates: new aborts usually reveal real latent bugs. - Sanitizer CI (month 2 to 3): add ASan plus UBSan and a separate TSan build to CI for unit and integration tests; start fuzz targets for every parser of untrusted data.
- Bans and review gates (month 3): enforce the banned-function list and lifetime checks in static analysis, and require a second reviewer on changes to code that parses network or file input.
- Roll back safely: keep hardening flags in one shared build configuration so a problematic flag can be disabled per target with a recorded exception and an expiry date, rather than removed silently.
- Roadmap: decide which components move to a memory-safe language, are sandboxed, or stay in hardened C++. See the Memory-Safe Languages Guide and the Rust Security Guide.
Small teams get most of the value from the OpenSSF flag set (or -fhardened), ASan and UBSan in CI, and fuzzing their input parsers. Large organisations should own a central toolchain configuration, publish a banned-API list, track hardening coverage as a metric, and make memory-safe language choice the default for new components that parse untrusted input. Embedded and real-time teams may not be able to afford every run-time check; keep compile-time warnings and fortify checks, measure the rest, and document exceptions.
Detection and measurement
- Hardening coverage: percentage of shipped binaries with PIE, full RELRO, NX, stack canaries and fortified functions present, measured on artefacts rather than build files.
- Sanitizer and fuzz coverage: share of components with ASan/UBSan CI runs, and number of parsers with active fuzz targets.
- Memory safety defect rate: memory safety bugs found per quarter by stage (fuzzing, CI, review, production, external report); the goal is shifting discovery left.
- Banned API count: remaining calls to
strcpy,sprintf,getsandsystem, tracked to zero. - Production aborts: monitor crash reports for fortify, stack-smashing and hardened-library aborts (messages such as "buffer overflow detected" or "stack smashing detected"). A spike can mean attempted exploitation against a public-facing service as well as a bug.
- New memory-unsafe code: lines of new C and C++ added per quarter in components that a roadmap has marked for migration.
Common failure patterns
- Trusting a length field: code allocates or copies using a size the peer declared rather than the bytes actually processed. The MongoBleed (CVE-2025-14847) 2025 flaw came from length handling in MongoDB's zlib message decompression code, which returned the allocated buffer size instead of the decompressed length and leaked uninitialised heap memory, including secrets, before authentication.
- Leaking leftover memory: an over-read returns stale data that happens to contain tokens and passwords. CitrixBleed 2 (CVE-2025-5777) was described by Citrix as insufficient input validation leading to a memory over-read, and researchers retrieved live session tokens from appliance memory. Validating input lengths, initialising buffers and fuzzing request parsers address this class.
- Hardening that never shipped: flags set in a top-level makefile but lost in vendored libraries or a different release build configuration, so the production binary lacks PIE or RELRO.
- Undefined behaviour removing checks: an overflow check written as
if (a + b < a)on signed integers is legally deleted by the optimiser. Use builtins or C23 checked arithmetic. - Sanitizers mistaken for protection: ASan builds deployed to production as "extra safety", adding attack surface and performance cost instead.
- Safe-looking C++:
string_viewinto a temporary, iterators held across reallocation, and lambdas capturing references that outlive their scope.
Where identity fits
Memory disclosure bugs in C and C++ services are credential theft bugs: the processes that parse network input also hold session tokens, database passwords, API keys and private keys. Keep long-lived secrets out of internet-facing parsing processes where possible (privilege separation, a separate signing service), wipe secrets with explicit_bzero or memset_s where available after use, and plan to rotate every credential a vulnerable service could have held, because patching does not revoke what already leaked. See the Secrets Management Guide.
Framework mapping
| Practice | OWASP ASVS 5.0 | NIST SP 800-53 Rev. 5 | Other |
|---|---|---|---|
| Memory-safe string and copy functions, bounds checks | V1.4.1 | SI-16, SA-8 | SEI CERT ARR30-C, STR31-C |
| Integer overflow prevention | V1.4.2 | SI-10 | SEI CERT INT32-C |
| Freed memory and dangling pointers | V1.4.3 | SI-16 | SEI CERT MEM30-C |
| Thread-safe shared state | V15.4.1 | SA-11 | ThreadSanitizer |
| No shell for OS commands | V1.2.5 | SI-10 | n/a |
| Stack canaries, NX, ASLR, CFI | n/a | SI-16 | MITRE D3FEND D3-SFCV Stack Frame Canary Validation |
| Static analysis, sanitizers, fuzzing | n/a | SA-11, SA-11(1), SA-15 | NIST CSF 2.0 PR.PS-06 |
ISO/IEC 27001:2022 Annex A 8.28 (secure coding) and CIS Controls v8.1 Control 16 (Application Software Security) cover the same ground at policy level. The attacks these controls frustrate are typically MITRE ATT&CK T1190 Exploit Public-Facing Application against servers and appliances, and T1203 Exploitation for Client Execution against client software.
Practitioner checklist
- Apply the OpenSSF baseline flags (or GCC
-fhardened) in one shared build configuration, and/sdl,/guard:cfand/CETCOMPATfor MSVC. - Verify hardening on shipped binaries in CI, not just in build files.
- Enable
_LIBCPP_HARDENING_MODE_FASTor_GLIBCXX_ASSERTIONSin production builds. - Ban
gets,strcpy,strcat,sprintf,vsprintfandsystemin new code and track legacy uses to zero. - Use checked arithmetic for every size calculation and validate every length read from input.
- Run ASan plus UBSan, and a separate TSan build, in CI; never ship full sanitizer builds.
- Fuzz every parser of untrusted data, starting with network-facing services.
- Use RAII and smart pointers for ownership, and review
string_view,spanand iterator lifetimes. - Publish a memory safety roadmap naming which components will be migrated, sandboxed or kept in hardened C++.
- Pre-plan credential rotation for any service whose memory could be disclosed.
Standards and references
- OpenSSF: Compiler Options Hardening Guide for C and C++
- CISA and NSA: Memory Safe Languages: Reducing Vulnerabilities in Modern Software Development
- Carnegie Mellon SEI: SEI CERT C Coding Standard
- LLVM: libc++ Hardening Modes
- Microsoft Learn: /sdl (Enable Additional Security Checks)
- Chromium: Memory Safety
- OWASP: Application Security Verification Standard 5.0
- NIST: SP 800-53 Rev. 5
Related NHI Mgmt Group resources: Memory Corruption Vulnerabilities Guide · Memory-Safe Languages Guide · Go Secure Coding Guide · Fuzzing Guide · Secure Code Review Guide