Join our Newsletter — 33% off our NHI Course
Home› Guides› C and C++ Secure Coding Guide: Memory Safety,…
Guide Application Security

C and C++ Secure Coding Guide: Memory Safety, Hardening Flags and Safe Patterns

← All guides
By NHI Mgmt Group, edited by Lalit Choda Updated 26 October 2026 15 min read
On this page

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-bounds operator[] 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 classTypical root causeSafe patternDetected by
Stack or heap buffer overflowstrcpy, sprintf, memcpy with an attacker-controlled lengthLength-carrying types (std::string, std::span), snprintf with checked return, explicit bounds checksASan, _FORTIFY_SOURCE, stack protector
Out-of-bounds read (information leak)Trusting a length field from the networkValidate declared lengths against actual received bytes before useASan, fuzzing
Use-after-free, double freeManual free/delete with unclear ownership; dangling references and iteratorsRAII, std::unique_ptr, no raw owning pointers, no references into containers that may reallocateASan, libc++ extensive mode
Integer overflow and truncationSize arithmetic (n * size), signed/unsigned mixing, narrowing to intChecked arithmetic (__builtin_mul_overflow, C23 ckd_mul), size_t for sizes, -WconversionUBSan, compiler warnings
Uninitialised memoryStack variables and heap buffers read before writeInitialise at declaration; calloc or value-initialisationMemorySanitizer (Clang), -ftrivial-auto-var-init=zero as a backstop
Format stringUser data passed as the format argumentprintf("%s", s), never printf(s)-Wformat=2, -Werror=format-security
Data raceShared state without synchronisationstd::mutex, std::atomic, ownership transfer between threadsThreadSanitizer
Command injectionsystem() or popen() with concatenated stringsposix_spawn or execve with an argument vectorCode 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_t length 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, sprintf and vsprintf in new code (a poisoned-identifier header or static analysis rule enforces it). Note that strncpy does not guarantee null termination; prefer snprintf and check its return value for truncation.
  • Check allocation arithmetic: malloc(count * size) overflows silently. Use calloc(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 NULL after free and 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, setuid or snprintf returns are a common source of exploitable states. Use __attribute__((warn_unused_result)) or C23 [[nodiscard]] on internal APIs.
  • Avoid the shell: replace system() and popen() with posix_spawn or execve and 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_ptr for single ownership and std::shared_ptr only 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::array and std::string instead of raw arrays, and std::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_view and std::span do not own their data. Returning a view into a temporary string, or holding an iterator or reference into a std::vector across a push_back that reallocates, creates use-after-free bugs that look like safe code.
  • No C-style casts: use static_cast and avoid reinterpret_cast on 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:

FlagTypeWhat it doesNotes
-Wall -Wformat -Wformat=2 -Wconversion -Wimplicit-fallthroughCompile-time warningsFlags format, conversion and switch defectsNo run-time cost
-Werror=format-securityCompile-time errorRejects non-literal format strings with no argumentsSafe to use when distributing source
-D_FORTIFY_SOURCE=3 (with -U_FORTIFY_SOURCE first)Run-time checkBounds checks in glibc memcpy, strcpy, sprintf and similar, including sizes known only as expressionsNeeds -O1 or higher (-O2 recommended) and glibc
-D_GLIBCXX_ASSERTIONSRun-time checkPrecondition checks in libstdc++ (for example operator[] bounds)Small cost
-D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_FASTRun-time checklibc++ checks for out-of-bounds element access and invalid input rangesRecommended for production by the libc++ project
-fstrict-flex-arrays=3Compile-time behaviourOnly true [] trailing arrays are treated as flexible, so bounds checking covers the restGCC 13, Clang 16 or later for level 3
-fstack-protector-strongRun-time checkStack canaries on functions with local arrays or address-taken localsLow cost
-fstack-clash-protectionRun-time checkProbes large stack allocations so they cannot jump the guard pageLow cost
-fcf-protection=full (x86_64), -mbranch-protection=standard (AArch64)Run-time protectionControl-flow integrity using CET or branch protection and pointer authenticationEffective on supporting hardware and OS
-fPIE -pie (executables), -fPIC -shared (libraries)Code generationPosition-independent code so ASLR applies to the whole binaryUsually default on modern distributions
-Wl,-z,relro -Wl,-z,nowLinkerFull RELRO: relocation tables read-only after loadSlightly slower start-up
-Wl,-z,noexecstack, -Wl,-z,nodlopenLinkerNon-executable stack; prevent the object being loaded with dlopennodlopen not for plug-ins
-fno-delete-null-pointer-checks -fno-strict-overflow -fno-strict-aliasing -ftrivial-auto-var-init=zeroProduction code generationStops the optimiser removing checks that rely on undefined behaviour, and zero-initialises automatic variablesSome performance cost; measure
  • GCC shortcut: GCC 14 added -fhardened, a predetermined hardening set for GNU/Linux targets, with -Whardened warning when a flag it implies is downgraded. The exact set changes between releases; inspect it with --help=hardened.
  • -Werror: the OpenSSF guide recommends blanket -Werror during development but not in distributed source, where it ties builds to particular compiler versions. Promote specific warnings with -Werror= instead.
  • MSVC equivalents: /sdl enables a superset of the /GS buffer 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 after delete. Pair it with /guard:cf (Control Flow Guard), /DYNAMICBASE, /HIGHENTROPYVA, /NXCOMPAT and /CETCOMPAT at 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

ToolFlagFindsWhere to run
AddressSanitizer-fsanitize=addressOut-of-bounds access, use-after-free, double freeUnit tests, fuzzing, CI
UndefinedBehaviorSanitizer-fsanitize=undefinedSigned overflow, invalid shifts, misaligned access, null dereferenceUnit tests, fuzzing
ThreadSanitizer-fsanitize=threadData racesConcurrency tests (separate build from ASan)
LeakSanitizer-fsanitize=leakMemory leaksTest runs, long-running service soak tests
Coverage-guided fuzzerlibFuzzer (-fsanitize=fuzzer) or AFL++Crashes in parsers and decoders, when combined with ASan and UBSanContinuous fuzzing, OSS-Fuzz for open source
Static analysisclang-tidy, Clang Static Analyzer, GCC -fanalyzer, commercial SASTBanned functions, CERT and Core Guidelines violations, some lifetime errorsPull 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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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, gets and system, 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_view into 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

PracticeOWASP ASVS 5.0NIST SP 800-53 Rev. 5Other
Memory-safe string and copy functions, bounds checksV1.4.1SI-16, SA-8SEI CERT ARR30-C, STR31-C
Integer overflow preventionV1.4.2SI-10SEI CERT INT32-C
Freed memory and dangling pointersV1.4.3SI-16SEI CERT MEM30-C
Thread-safe shared stateV15.4.1SA-11ThreadSanitizer
No shell for OS commandsV1.2.5SI-10n/a
Stack canaries, NX, ASLR, CFIn/aSI-16MITRE D3FEND D3-SFCV Stack Frame Canary Validation
Static analysis, sanitizers, fuzzingn/aSA-11, SA-11(1), SA-15NIST 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:cf and /CETCOMPAT for MSVC.
  • Verify hardening on shipped binaries in CI, not just in build files.
  • Enable _LIBCPP_HARDENING_MODE_FAST or _GLIBCXX_ASSERTIONS in production builds.
  • Ban gets, strcpy, strcat, sprintf, vsprintf and system in 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, span and 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

Related NHI Mgmt Group resources: Memory Corruption Vulnerabilities Guide · Memory-Safe Languages Guide · Go Secure Coding Guide · Fuzzing Guide · Secure Code Review Guide

Free weekly newsletter

Subscribe to the NHI & AI Identity Journal

The latest on NHI and Agentic AI security – articles, research, breaches, news and events every week.

Bonus 33% off our NHI Course when you subscribe.

NHIMG Editorial Note
Researched and written by the NHI Mgmt Group editorial team using AI tools, under the editorial direction of Lalit Choda. Facts are drawn from the sources listed on this page and checked by our automated quality process. Last updated 26 October 2026. Spotted an error? Use our contact page.
NHI Mgmt Group — the #1 independent authority on Non-Human Identity, IAM, and Agentic AI security. nhimg.org