In February 2019, Microsoft security engineer Matt Miller told the BlueHat IL conference that roughly 70% of the CVE-assigned vulnerabilities Microsoft had patched over the prior twelve years were memory-safety issues, in a codebase that is predominantly C and C++. A year later, the Chromium security team ran its own audit of 912 high- and critical-severity bugs fixed in the Stable channel since 2015, and landed on almost the identical number: about 70% were memory-safety bugs, and roughly half of those were specifically use-after-free. Two unrelated teams, two enormous C/C++ codebases, the same conclusion a year apart — that's not noise, it's a structural property of the language. C and C++ give you manual control over memory layout and lifetime, which is exactly why they still power browsers, kernels, game engines, and embedded firmware, and exactly why a single missed bounds check or a pointer freed twice can turn into remote code execution. This post walks through the vulnerability classes actually responsible for that 70%, and the sanitizers, fuzzers, and compiler hardening that modern C++ teams use to catch them before an attacker does.
What makes buffer overflows still the most common C++ bug?
A buffer overflow happens when code writes past the boundary of an allocated buffer, and C++ has no runtime guardrail that stops this by default — arrays and raw pointers trust the programmer to track their own bounds. CWE-787 (out-of-bounds write) and CWE-121/122 (stack- and heap-based overflow specifically) sit near the top of MITRE's annual CWE Top 25 Most Dangerous Software Weaknesses list nearly every year it's been published, because the pattern recurs everywhere: strcpy into a fixed-size buffer, an off-by-one in a manual loop, or a size calculated from untrusted input that's larger than the destination. A stack overflow can corrupt a saved return address; a heap overflow can corrupt adjacent allocator metadata or another object's fields, and either can be leveraged into arbitrary code execution depending on the surrounding memory layout and platform mitigations. The fix isn't a single tool — it's a combination of switching to bounds-checked containers (std::vector, std::span, std::array with .at()), and running AddressSanitizer in CI so any out-of-bounds access aborts the process with a precise stack trace instead of corrupting memory silently.
Why is use-after-free the single most dangerous C++ bug class?
Use-after-free (CWE-416) and its close relative double-free (CWE-415) occur when code keeps using a pointer after the memory it references has been deallocated, and the Chromium team's finding that it accounts for roughly half of all memory-safety bugs in a codebase that size is the clearest evidence of how easy it is to get wrong in large, long-lived C++ projects with complex object ownership. The danger is that freed memory is frequently reallocated almost immediately for something else, so a dangling pointer that gets dereferenced doesn't crash — it reads or writes into an attacker-influenced object, which is what makes use-after-free bugs disproportionately represented among exploited-in-the-wild browser and kernel CVEs. Modern C++ mitigates this at the language level with RAII and smart pointers — std::unique_ptr for exclusive ownership and std::shared_ptr for shared ownership — which tie an object's lifetime to a scope so there's no raw delete left for a stray pointer to outlive. AddressSanitizer also detects use-after-free directly at runtime by poisoning freed memory regions, catching the bug the moment the dangling access happens rather than however many instructions later a crash actually surfaces.
How does integer overflow turn into a memory-safety bug?
Integer overflow (CWE-190) is dangerous in C++ specifically because signed integer overflow is undefined behavior per the C++ standard, and unsigned overflow silently wraps rather than raising any error — so a size or index computation that overflows doesn't fail loudly, it produces a wrong number that code then trusts. The classic pattern is an allocation size computed as count * sizeof(element): if count is attacker-controlled and large enough, the multiplication wraps to a small value, the allocator hands back a small buffer, and the code that assumed it got a large one writes past the end of it — turning an integer bug into a heap buffer overflow (CWE-122) in the same function. UndefinedBehaviorSanitizer (UBSan), shipped with both Clang and GCC, is built specifically to catch this class by instrumenting arithmetic and aborting on signed overflow, shifts past bit width, and other undefined-behavior patterns at the point they occur, rather than downstream when the corrupted size finally causes a crash somewhere else in the program.
Which compiler and runtime defenses actually stop exploitation?
Compiler sanitizers turn memory-safety bugs from silent corruption into loud, immediate crashes during testing: AddressSanitizer (ASan) instruments every memory access to catch out-of-bounds reads/writes, use-after-free, and double-free; UBSan catches undefined arithmetic and type-punning; and both ship as built-in flags (-fsanitize=address, -fsanitize=undefined) in LLVM/Clang and GCC, making them nearly free to add to an existing build. Beyond sanitizers, static analyzers like the Clang Static Analyzer and CodeQL flag suspicious patterns — unchecked buffer arithmetic, mismatched allocation/deallocation pairs — without executing the code at all. And at the binary level, stack canaries (-fstack-protector), _FORTIFY_SOURCE, Control Flow Integrity (CFI), ASLR, and DEP/NX don't prevent the underlying bug but make it dramatically harder to turn into reliable code execution, which is why production C++ toolchains enable most of these by default today rather than treating them as optional hardening.
What role does fuzzing play in finding these bugs before attackers do?
Fuzzing complements sanitizers by generating the malformed, boundary-pushing inputs that trigger memory-safety bugs in the first place, rather than relying on a human to write a test case that happens to hit the bad path. libFuzzer and AFL++ are the two dominant coverage-guided fuzzers for C/C++, both instrumenting a binary to track which code paths new inputs reach and mutating toward unexplored coverage; paired with ASan and UBSan compiled into the same binary, a fuzzer doesn't just crash on a bug — it reports the exact sanitizer diagnostic and a minimized reproducer. Google's OSS-Fuzz service extends this to the open-source ecosystem at scale, continuously fuzzing hundreds of critical C/C++ projects and reporting bugs directly to maintainers, and is widely credited across the security community with surfacing large numbers of memory-safety bugs in widely used libraries well before public disclosure. This combination — sanitizer instrumentation plus continuous fuzzing — is the closest thing C++ has to the compile-time safety guarantees that memory-safe languages like Rust provide natively, which is also why Miller's and Chromium's own findings both concluded with a recommendation to migrate new, security-critical code to a memory-safe language where practical.
Where does this fit into a broader application security program?
None of these tools replace each other — sanitizers catch bugs at the exact point they occur during testing, static analyzers catch patterns before code ever runs, and fuzzing supplies the inputs that make sanitizer coverage meaningful across code paths a human tester wouldn't think to exercise. Safeguard's own SAST engine currently covers JavaScript/TypeScript, Python, and Java in its first-phase language rollout, and its software composition analysis tracks known-vulnerable packages across npm, PyPI, Maven/Gradle, Go, Cargo, NuGet, Composer, and Bundler — C/C++ and its package managers like Conan and vcpkg aren't in that lineup yet. For teams running C++ today, that means the sanitizer-plus-fuzzing stack described above is the front line, with Safeguard's reachability and findings model well positioned to extend to native code coverage as that support matures.