Safeguard
Application Security

Memory-Safety Vulnerabilities in C/C++: What Static and Dynamic Analysis Actually Catch

Roughly 70% of the CVEs Microsoft and Google's Chrome team assign each year trace to memory-unsafe C/C++ code — how static analysis, sanitizers, and fuzzers each catch a different slice of it.

Safeguard Research Team
Research
7 min read

On April 7, 2014, researchers at Codenomicon and Google Security disclosed CVE-2014-0160 — Heartbleed — a single missing bounds check before a memcpy() call in OpenSSL's heartbeat extension. The bug let an attacker request up to 64KB of process memory per message, leaking private keys, session tokens, and credentials from roughly half a million TLS-protected servers before OpenSSL 1.0.1g shipped a fix. It is one bug, in one function, and it is a memory-safety bug — a category that has dominated the highest-severity vulnerabilities in C and C++ software for over a decade. Microsoft security engineering reported at BlueHat IL 2019 that memory-safety issues accounted for roughly 70% of the CVEs it assigned annually over the prior twelve years. Google's Chromium security team found the same pattern independently: of 912 high- and critical-severity Chrome security bugs analyzed since 2015, about 70% were memory-unsafety issues, and roughly half of those were use-after-free bugs specifically. Neither team was cherry-picking — they were describing the default failure mode of manual memory management at scale. This post walks through the actual vulnerability classes behind that number and how static analysis, sanitizers, and fuzzers each catch a different slice of them.

What are the core memory-safety vulnerability classes in C/C++?

C and C++ give programmers direct control over memory allocation and pointer arithmetic, and that control is also the attack surface. The recurring classes are: stack and heap buffer overflows/overreads (writing or reading past an allocated region's bounds); use-after-free (accessing memory after it has been deallocated); double-free (freeing the same allocation twice, corrupting allocator metadata); null-pointer dereference; uninitialized memory reads (using a variable before it's assigned); integer overflows that cause an undersized allocation, which then overflows when filled; dangling pointers left over after an object's lifetime ends; and data races, a concurrency bug class that's memory-safety-adjacent because two threads touching the same memory without synchronization can produce the same corruption as a buffer overflow. Heartbleed is a buffer overread. A double-free or use-after-free is often worse in practice, because an attacker can sometimes reallocate the freed memory with attacker-controlled content before the dangling pointer is used again, turning a crash into arbitrary code execution — which is why Chromium's data shows use-after-free bugs alone account for roughly half of its memory-safety CVEs.

How would static analysis have caught a bug like Heartbleed?

Static analysis tools read source code without executing it, building a model of every possible path through a function to flag operations that look unsafe on at least one path. Tools like the Clang Static Analyzer, Coverity, Cppcheck, and PVS-Studio each do this differently — some via symbolic execution of branches, some via pattern-matching against known-dangerous API call shapes — but they share the same core technique: dataflow analysis that tracks a buffer's declared size against every read or write into it. The Heartbleed bug is a textbook case for this class of tool: the vulnerable code read a 2-byte length field from an attacker-controlled heartbeat message and passed it directly to memcpy() without validating that the claimed length matched the actual payload size. A dataflow-aware bounds check — tracking "this length came from untrusted input and was never validated against the buffer it indexes" — is exactly the shape of bug static analyzers are built to flag, though OpenSSL's own contemporaneous static-analysis tooling did not catch it before release, underscoring that static analysis is only as good as the checks it actually runs and the code paths it's pointed at.

Why do sanitizers catch bugs that static analysis misses?

Static analysis reasons about code without running it, so it has to approximate what happens at runtime — and approximation produces both false positives and missed bugs on paths the analyzer can't fully model, like state built up across multiple function calls or complex pointer aliasing. Sanitizers close that gap by instrumenting the actual compiled binary and watching real execution. AddressSanitizer (ASan), built into LLVM/Clang and GCC, inserts redzones around every heap and stack allocation and poisons freed memory, so a read or write one byte past a buffer's bounds — or any access to freed memory — crashes immediately with an exact stack trace, instead of silently corrupting adjacent data the way it would in production. MemorySanitizer catches uninitialized-memory reads; UndefinedBehaviorSanitizer catches integer overflow and other undefined-behavior triggers; ThreadSanitizer catches data races. Because these tools observe the program actually running, they need real test inputs to exercise the vulnerable path — which is exactly the gap fuzzers are built to fill.

How do fuzzers find memory-safety bugs that developers never think to test?

A fuzzer generates large volumes of malformed or unexpected input and feeds it to a program, watching for crashes — and it becomes dramatically more effective when the binary is compiled with a sanitizer attached, because a bug that would otherwise silently corrupt memory instead crashes on the exact input that triggered it. AFL and libFuzzer are the two most widely used engines for this; both use coverage feedback, mutating inputs that reach new code paths so the search concentrates on unexplored logic rather than re-testing the same lines. Google's OSS-Fuzz project, launched in 2016, runs this combination continuously against hundreds of open-source C/C++ projects and has been credited with finding thousands of memory-safety bugs across the projects it covers, including in OpenSSL itself — the same codebase Heartbleed shipped in. A length field validated incorrectly, like the one behind Heartbleed, is precisely the kind of malformed-input case a fuzzer generates automatically, without a human having to think to write that specific test case first.

What does Valgrind add that compiler-based sanitizers don't?

Valgrind's Memcheck tool performs dynamic memory analysis at the binary level using an emulator, rather than requiring the source to be recompiled with sanitizer instrumentation. That makes it useful when you can't rebuild a binary — testing a third-party library shipped only as a compiled artifact, for instance — but it comes at a real cost: Memcheck typically slows execution by roughly 10-50x compared to native speed, versus the much lower overhead of a compiled-in sanitizer like ASan, which is usually cited around 2x. The tradeoff in practice: teams building from source in CI generally prefer compiler sanitizers for continuous fuzzing and test runs, and reach for Valgrind for one-off deep investigation of a specific crash or a binary they don't control the build for.

Where is the industry heading on memory safety?

The Open Source Security Foundation published its "Memory Safety Continuum" framework in April 2025, describing memory-safety mitigation as a graduated set of strategies rather than a single fix: compiler-level hardening flags first, then sanitizer and fuzzing coverage in CI, then partial migration of the highest-risk components to memory-safe languages like Rust, rather than an all-or-nothing rewrite. That framing matches what the Chromium and Microsoft data both point to — the fix isn't one tool, it's layering static analysis to catch bugs before code ships, sanitizers and fuzzers to catch what static analysis misses during testing, and, increasingly, rewriting the highest-risk, most attacker-reachable components in a language where the compiler enforces bounds and lifetime rules instead of relying on every engineer to get memcpy() right every time.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.