Safeguard
Application Security

Double-Free Vulnerabilities in C and C++

Double-free bugs let attackers corrupt heap memory and hijack control flow. Here's how they happen in C/C++, real CVEs, and how to catch them early.

Vikram Iyer
Security Researcher
8 min read

In September 2016, a double-free bug tracked as CVE-2016-6309 in OpenSSL's s3_srvr.c let a remote attacker corrupt heap memory during TLS negotiation on any server that had enabled a specific buffer configuration. Nine months earlier, CVE-2015-8126 hit libpng's image-decoding path and rippled into Apple's iOS, macOS, and Safari stacks because so many products statically linked the library. Both bugs trace back to the same root cause: a program called free() on the same pointer twice, and the heap allocator did exactly what it was told.

A double-free vulnerability in C++ (and C) occurs when memory is released more than once without being reallocated in between, corrupting the allocator's internal bookkeeping in a way attackers can steer. Unlike a crash-only bug, a well-placed double-free can be turned into arbitrary code execution. This post breaks down why these bugs keep appearing in production C/C++ code, walks through real CVEs, and explains how Safeguard catches them before they ship.

What Is a Double-Free Vulnerability?

A double-free vulnerability is when a program calls free() (or delete/delete[]) on the same block of heap memory two or more times without an intervening allocation. MITRE catalogs this as CWE-415, defined as calling free() twice on the same address, which "can lead to modification of unexpected memory locations." The danger isn't the second free() call itself — it's what happens to the allocator's metadata. Modern allocators like glibc's malloc store bookkeeping data (chunk size, free-list pointers) directly adjacent to the allocated block. Freeing a chunk twice lets an attacker manipulate those free-list pointers so a later malloc() call returns a pointer into attacker-chosen memory, such as a function pointer table or a return address on the stack.

Double-free is closely related to but distinct from CWE-416 (use-after-free): a UAF happens when freed memory is read or written, while a double-free happens when freed memory is freed again. Both land in the same family — "memory safety" bugs — that Google and Microsoft engineering teams have each separately reported account for roughly 70% of the serious security vulnerabilities they fix in C/C++ codebases every year.

Why Are C and C++ Programs Especially Prone to Double-Free Bugs?

C and C++ are especially prone because they leave heap lifetime management entirely to the developer, with no runtime safety net. There is no garbage collector tracking whether a pointer has already been freed, and the language spec treats calling free() on already-freed memory as undefined behavior rather than a caught exception. In practice, double-frees creep in through a handful of recurring patterns:

  • Duplicate cleanup on error paths. A function allocates a buffer, hits an error, frees it in a cleanup block, then a caller's own error handler frees it again.
  • Aliased pointers. Two variables (or a struct field and a local copy) point to the same allocation, and both get freed independently — common in linked-list and tree deletion code.
  • Missing null-outs. After free(ptr), ptr still holds the old address (a "dangling pointer"). If the code checks if (ptr) free(ptr); later without having set ptr = NULL after the first free, the second call fires.
  • Exception-path double deletes in C++. A delete inside a destructor plus a delete in a catch block, or a raw pointer stored in two objects whose destructors both run during stack unwinding.
  • Reference-counting mistakes. Manual refcount logic that decrements to zero and frees, but a second decrement path (e.g., in a signal handler or thread) also reaches zero and frees again.

The NTP double-free disclosed as CVE-2014-9295 in December 2014 is a textbook case: NTP's crypto_recv(), ctl_putdata(), and configure() functions each had a path that could free the same buffer twice, and the fix required patching four separate functions in one release (ntp-4.2.8) because the pattern had been copy-pasted across the codebase.

How Have Real-World Double-Free Vulnerabilities Caused Damage?

Real-world double-free bugs have caused everything from remote code execution in image libraries to crashes in internet-critical infrastructure. A few concrete examples:

  • CVE-2002-0059 (zlib, 2002). A double-free in zlib's inflate.c affected essentially every application linking the compression library, from web servers to SSH clients, because zlib is one of the most widely embedded C libraries ever written.
  • CVE-2014-9295 (NTP, December 2014). Double-free flaws in ntpd could be triggered remotely by a crafted packet, allowing denial of service and, under some configurations, code execution on time-synchronization servers running across enterprise and government networks.
  • CVE-2015-8126 (libpng, November 2015). A double-free in PNG chunk handling led Apple to ship emergency patches across iOS, OS X, tvOS, and Safari within weeks, since libpng sat underneath image rendering on all of them.
  • CVE-2016-6309 (OpenSSL, September 2016). A double-free in the TLS server code path, reachable when SSL_MODE_RELEASE_BUFFERS was enabled, was rated critical enough that the OpenSSL team shipped 1.1.0a as an out-of-band release just eleven days after 1.1.0.
  • CVE-2018-16509 (Ghostscript, September 2018). A double-free in Ghostscript's PostScript interpreter was exploitable via malicious print files and PDF-to-image conversion pipelines used by countless document-processing backends.

The common thread across all five: none of these were exotic zero-days requiring nation-state resources. Each was a logic error in cleanup code, reachable with a normal input, and each shipped to production for months or years before discovery.

Why Is a Double-Free Bug So Dangerous to Exploit?

A double-free is dangerous because it hands an attacker a primitive to corrupt allocator metadata, and allocator metadata sits right next to attacker-influenced data. Classic exploitation techniques like "fastbin dup" and "House of Einherjar" abuse the fact that glibc's fastbin and tcache free-lists are singly linked lists stored inline in freed chunks. Freeing chunk A twice inserts it into the free-list twice; a subsequent malloc() of the right size returns chunk A, and the attacker writes attacker-controlled data into it — including a forged "next" pointer. The next malloc() call then returns a pointer wherever the attacker forged it to point, which can be a GOT entry, a C++ vtable, or a stack return address, turning a memory-management bug into full control-flow hijacking.

This is exactly why glibc 2.29, released in February 2019, added an explicit tcache double-free check that aborts the process with free(): double free detected in tcache 2 — a mitigation added specifically because double-free-based exploitation had become common enough in CTF and real-world attacks to justify slowing down every allocation with a safety check. Mitigations like this raise the bar but don't eliminate the bug class; they just make naive double-frees crash loudly instead of silently corrupting memory, which is still a denial-of-service risk on its own.

How Can Teams Detect and Prevent Double-Free Vulnerabilities?

Teams catch double-frees most reliably by combining runtime instrumentation, static analysis, and modern C++ ownership patterns — no single technique catches every case. Concretely:

  • AddressSanitizer (ASan). Compiling with -fsanitize=address and running your existing test suite catches double-frees at the exact line they occur, with a full stack trace of both the original free and the duplicate. This is the single highest-signal, lowest-effort step most C/C++ teams can add to CI today.
  • Valgrind's Memcheck. Slower than ASan but useful for catching double-frees in third-party binaries or environments where recompiling with sanitizers isn't practical.
  • Static analysis (Coverity, CodeQL, clang-tidy). These tools trace pointer lifetimes across function boundaries and flag paths where a pointer can reach free() twice, including across error-handling branches ASan won't exercise unless a test happens to trigger them.
  • RAII and smart pointers in C++. std::unique_ptr and std::shared_ptr tie deallocation to object lifetime instead of manual delete calls, which eliminates the vast majority of double-free patterns by construction. Code review should treat any raw new/delete pair outside of a smart-pointer implementation as a flag for extra scrutiny.
  • Null-out-after-free discipline in C. Setting ptr = NULL immediately after every free(ptr) turns a silent double-free into a harmless free(NULL) no-op, since free() on a null pointer is explicitly defined to do nothing.
  • Fuzzing error paths specifically. Because so many double-frees (like CVE-2014-9295) live in cleanup and error-handling code, fuzzers need corpus inputs designed to trigger allocation failures and malformed-input error branches, not just the happy path.

How Safeguard Helps

Safeguard scans C and C++ codebases as part of your software supply chain, flagging double-free patterns like duplicate free()/delete calls, unguarded cleanup paths, and aliased-pointer deletions before they reach a release build — including in vendored and third-party source you don't maintain directly. Because bugs like CVE-2015-8126 and CVE-2018-16509 spread through the dependency graph rather than staying contained to one project, Safeguard maps which of your components pull in vulnerable versions of libraries with known memory-safety CVEs and alerts you the moment a new advisory lands, rather than waiting for a manual audit cycle.

For teams still shipping manual memory management, Safeguard's policy engine can enforce guardrails, such as requiring smart-pointer usage in new C++ code or blocking merges that introduce raw delete calls in security-sensitive modules, so the fix happens at the pull-request stage instead of the incident-response stage. Combined with SBOM tracking across your build pipeline, this gives security and platform teams a single place to see not just that a double-free CVE exists somewhere in the ecosystem, but whether it's reachable in your actual production binaries — turning a CVE feed into an prioritized, actionable remediation list.

Never miss an update

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