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),ptrstill holds the old address (a "dangling pointer"). If the code checksif (ptr) free(ptr);later without having setptr = NULLafter the first free, the second call fires. - Exception-path double deletes in C++. A
deleteinside a destructor plus adeletein acatchblock, 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.caffected 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
ntpdcould 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_BUFFERSwas 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=addressand 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_ptrandstd::shared_ptrtie deallocation to object lifetime instead of manualdeletecalls, which eliminates the vast majority of double-free patterns by construction. Code review should treat any rawnew/deletepair outside of a smart-pointer implementation as a flag for extra scrutiny. - Null-out-after-free discipline in C. Setting
ptr = NULLimmediately after everyfree(ptr)turns a silent double-free into a harmlessfree(NULL)no-op, sincefree()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.