Back to Blog
critical SEVERITY8 min read

Use-After-Free in zmap.h: How a Missing NULL Assignment Nearly Opened the Door to Arbitrary Code Execution

A critical use-after-free vulnerability was discovered and patched in `zmap.h`, where freed memory pointers were not reset to a safe state after deallocation in the `map` destructor and move-assignment operator. This oversight allowed subsequent code paths — including destructors, iterators, and concurrent threads — to access memory that had already been returned to the allocator, creating a condition exploitable for arbitrary code execution. The fix, a two-line change adding `inner = {};` after

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published May 28, 2026•Reviewed June 3, 2026

Answer Summary

A use-after-free vulnerability (CWE-416) was discovered in C++ code within `zmap.h`, where the `map` destructor and move-assignment operator freed memory without resetting the `inner` pointer to a safe state. This allowed subsequent code paths to access deallocated memory, potentially enabling arbitrary code execution. The fix involves adding `inner = {};` after each deallocation to zero-initialize the pointer structure, preventing dangling pointer access.

Vulnerability at a Glance

cweCWE-416
fixAdd `inner = {};` after freeing memory in destructor and move-assignment operator
riskArbitrary code execution through dangling pointer exploitation
languageC++
root causeFreed memory pointers not reset to safe state after deallocation
vulnerabilityUse-After-Free

Introduction

Memory safety bugs are among the oldest and most dangerous classes of vulnerability in systems programming. Despite decades of tooling improvements, static analyzers, and community awareness, use-after-free (UAF) vulnerabilities continue to appear in production codebases — and when they do, the consequences can be severe: heap corruption, information disclosure, or full arbitrary code execution.

This post breaks down a critical use-after-free vulnerability discovered and patched in zmap.h, a C++ header implementing a custom hash map abstraction. The root cause was deceptively simple: after freeing an internal resource, the pointer was never cleared. What followed from that omission was a textbook UAF condition with real exploitation potential.

Whether you write C++ daily or just want to understand why memory discipline matters, this post will walk you through exactly what went wrong, how it was fixed, and what you can do to prevent similar issues in your own code.


What Is a Use-After-Free Vulnerability?

A use-after-free (UAF) vulnerability occurs when a program:

  1. Frees a block of memory (returning it to the allocator),
  2. Retains a pointer to that memory without clearing it, and
  3. Accesses the memory again through that stale pointer.

After step 1, the allocator is free to give that memory to any other allocation request. If step 3 happens — whether through a destructor running twice, an iterator walking freed state, or a concurrent thread racing on the same pointer — the program is reading or writing memory it no longer owns.

From an attacker's perspective, this is a powerful primitive. By carefully timing or shaping allocations (a technique called heap grooming), an attacker can arrange for the freed region to be reused by attacker-controlled data. When the vulnerable code then accesses that memory, it may execute attacker-supplied function pointers, corrupt adjacent heap metadata, or leak sensitive information.


The Vulnerability Explained

Where It Lived

The bug resided in zmap.h at lines 324 and 331, inside the z_map::map class template — specifically in two critical lifecycle methods:

  • The destructor (~map()), which is called when a map object goes out of scope or is explicitly destroyed.
  • The move-assignment operator (operator=(map&&)), which is called when ownership of a map is transferred from one variable to another.

The Vulnerable Code

// BEFORE — Vulnerable destructor
~map()
{
    Traits::free(&inner);
    // ⚠️ inner still holds the old (now freed) pointer value
}

// BEFORE — Vulnerable move-assignment operator
map &operator=(map &&other) noexcept
{
    if (this != &other)
    {
        Traits::free(&inner);
        // ⚠️ inner still holds the old (now freed) pointer value
        inner = other.inner;
        other.inner = Traits::init(inner.hash_func, inner.cmp_func, inner.load_factor);
    }
    return *this;
}

After Traits::free(&inner) is called, the underlying memory is released. But inner — the struct or pointer that was just freed — is not reset. It continues to hold the address of the memory that was just handed back to the allocator.

How This Becomes Exploitable

Several realistic code paths can trigger the UAF:

1. Double-Free via Destructor

If a map object is destroyed twice (e.g., due to a bug in object lifetime management, or an exception thrown during construction of a containing object), Traits::free() will be called on an already-freed pointer. Depending on the allocator, this can corrupt heap metadata and lead to controlled writes.

2. Iterator or Method Access After Move

Consider this pattern:

z_map::map<K, V> a = build_map();
z_map::map<K, V> b = std::move(a); // move-assignment triggers the bug
// 'a.inner' now points to freed memory
use(a); // UAF: accessing freed memory through 'a'

After the move, a.inner still holds the old address. Any subsequent access to a — even seemingly innocuous reads — is a UAF.

3. Concurrent Thread Access

In multithreaded environments, a thread may be iterating over the map while another thread destroys it. Without the pointer being cleared, the iterating thread has no way to detect that the memory it's traversing is no longer valid. The result is a data race on freed memory — a condition that can be exploited to redirect execution.

Real-World Impact

Use-after-free vulnerabilities of this class have historically been assigned CVSS scores of 9.0+ and have been exploited in browsers, kernels, and network daemons to achieve:

  • Arbitrary code execution (ACE) by overwriting freed memory with a fake vtable or function pointer
  • Privilege escalation in kernel-mode UAFs
  • Information disclosure by reading heap contents that now belong to another allocation
  • Denial of service through heap corruption causing crashes

The z_map context — a hash map used in what appears to be a network scanning tool — makes this particularly sensitive. If the map stores configuration data, connection state, or callback pointers, a UAF here could be leveraged by a malicious peer or local attacker to hijack control flow.


The Fix

The patch is elegantly minimal — two lines of code, one in each affected method:

// AFTER — Fixed destructor
~map()
{
    Traits::free(&inner);
    inner = {};  // ✅ Zero-initialize: clears the dangling pointer
}

// AFTER — Fixed move-assignment operator
map &operator=(map &&other) noexcept
{
    if (this != &other)
    {
        Traits::free(&inner);
        inner = {};  // ✅ Zero-initialize before overwrite
        inner = other.inner;
        other.inner = Traits::init(inner.hash_func, inner.cmp_func, inner.load_factor);
    }
    return *this;
}

Why inner = {} Works

In C++, inner = {} performs value-initialization (or aggregate zero-initialization, depending on the type). For a struct containing pointers and primitive values, this sets all members to zero/null. The result:

  • Any subsequent access to inner will encounter null/zero values rather than a stale heap address.
  • A null pointer dereference — while still a crash — is deterministic and non-exploitable, unlike a UAF where the freed region may contain attacker-controlled data.
  • If any defensive checks exist (e.g., if (inner.ptr != nullptr)), they now function correctly after the free.

The Diff at a Glance

 ~map()
 {
     Traits::free(&inner);
+    inner = {};
 }

 map &operator=(map &&other) noexcept
 {
     if (this != &other)
     {
         Traits::free(&inner);
+        inner = {};
         inner = other.inner;
         other.inner = Traits::init(inner.hash_func, inner.cmp_func, inner.load_factor);
     }

Two lines. Both inserted immediately after a free call. Both prevent the dangling pointer from persisting into any subsequent code path.


Conclusion

A single missing line of code — inner = {} — was the difference between a secure destructor and a critical use-after-free vulnerability. This is a pattern that repeats itself across the history of systems programming: the bug is not complex, but its consequences are severe.

The key takeaways from this vulnerability:

  • Always clear pointers after freeing them. A null pointer crash is far preferable to a UAF exploitation chain.
  • Audit all lifecycle methods together. Destructors and move operators share memory ownership semantics — bugs in one often appear in the other.
  • Use tooling. ASan, static analyzers, and fuzzers exist precisely to catch what code review misses.
  • Prefer safer abstractions. Smart pointers and RAII patterns make entire classes of memory bugs structurally impossible.

Security is not about perfection — it's about reducing the attack surface systematically. Fixes like this one demonstrate that even the most dangerous vulnerability classes can be resolved with disciplined, minimal changes. The automated detection and patching of this issue before it reached production is exactly the kind of proactive security posture every engineering team should strive for.


This vulnerability was identified and patched by OrbisAI Security. Automated security scanning, LLM-assisted code review, and verified re-scanning confirmed the fix prior to merge.

Prevention and further reading

Related Articles

high

How integer truncation heap overflow happens in C++ UEFI ACPI parsing and how to fix it

A high-severity integer truncation vulnerability was discovered in `Mobility.Uefi.Acpi.cpp` where heap allocation sizes were stored in a 16-bit integer (`MO_UINT16`), causing silent truncation when the computed size exceeded 65535 bytes. This led to undersized heap allocations followed by out-of-bounds writes, exploitable by an attacker who can influence ACPI SRAT table contents in virtualized environments. The fix promotes the size variable to `MO_UINTN` (platform-native width) to prevent trunc

critical

How buffer overflow via sprintf happens in C++ fuzzer code and how to fix it

A critical buffer overflow vulnerability was discovered in `prog/fuzzing/recog_basic_fuzzer.cc` where `sprintf` writes to a fixed 256-byte buffer without bounds checking. An attacker providing crafted fuzzer input could exploit this to corrupt memory. The fix replaces `sprintf` with `snprintf`, enforcing the buffer size limit and preventing overflow.

critical

SQL Injection via SQLite's %s Format Specifier in LR2_statlong.cpp ReadPlayerScore()

A critical SQL injection vulnerability was discovered in `LR2/LR2_statlong.cpp` at line 42, where `sqlite3_snprintf` used the `%s` format specifier instead of `%q` to interpolate a player ID into a SQL query. This single-character difference meant that single quotes in the player ID were inserted verbatim, allowing an attacker to break out of the SQL string literal and inject arbitrary commands. The fix changes `%s` to `%q`, which doubles all single quotes to properly escape them.

critical

Integer Overflow to Heap Buffer Overflow: A Critical CVE in OpenCV Image Processing

A critical integer overflow vulnerability was discovered and patched in opencv_functions.cpp, where width × height calculations on 32-bit embedded systems could silently overflow, causing heap buffer overflows that enable arbitrary code execution. This fix eliminates a dangerous attack vector that could be triggered by maliciously crafted image metadata. Understanding this class of vulnerability is essential for any developer working with image processing, embedded systems, or untrusted user inp

critical

Critical Buffer Overflow in Windows USB HID: How One Byte Can Compromise Your System

A critical buffer overflow vulnerability was discovered and patched in the Windows USB HID host library, where four unsafe `memcpy` calls copied data using device-reported sizes without validating destination buffer capacity. The most dangerous instance could overflow a heap buffer by as little as one byte — enough to corrupt heap metadata and potentially allow arbitrary code execution. This post breaks down how the vulnerability works, why it matters, and how to write safer memory operations in

critical

CVE-2026-59873: node-tar 7.5.11 DoS via Crafted Gzip Bomb

node-tar versions 7.5.11 through 7.5.18 are vulnerable to a denial-of-service attack through maliciously crafted gzip archives that decompress to disproportionately large sizes. An attacker can exploit this to exhaust memory and CPU resources by submitting a small, highly compressed archive that expands beyond configured limits during extraction.