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
Published May 28, 2026Reviewed 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

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

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.


Prevention & Best Practices

1. Always Clear Pointers After Freeing

This is the most direct lesson. Whether you're writing C or C++, make it a habit:

// C style
free(ptr);
ptr = NULL;

// C++ style (struct/aggregate)
Traits::free(&inner);
inner = {};

// C++ style (raw pointer member)
delete ptr_;
ptr_ = nullptr;

This pattern is sometimes called poison-after-free or null-after-free and is a foundational memory safety discipline.

2. Follow the Rule of Five (or Zero)

In modern C++, if you define any of the following, you should define all five:

  • Destructor
  • Copy constructor
  • Copy assignment operator
  • Move constructor
  • Move assignment operator

The bug here existed in the destructor and move-assignment operator. Auditing all five together would have surfaced the inconsistency earlier.

3. Prefer RAII and Smart Pointers

The safest way to avoid UAF is to not manage raw memory manually. C++ smart pointers encode ownership semantics in the type system:

// std::unique_ptr automatically nulls itself on move
std::unique_ptr<MapInner> inner_;

// After move, inner_ is guaranteed null — no UAF possible

If Traits::free wraps a custom allocator, consider wrapping it in a custom deleter:

struct TraitsDeleter {
    void operator()(MapInner* p) const { Traits::free(&p); }
};
std::unique_ptr<MapInner, TraitsDeleter> inner_;

4. Use Address Sanitizer (ASan) in CI

Google's AddressSanitizer is the most effective tool for catching UAF bugs at runtime:

# Compile with ASan
clang++ -fsanitize=address -fno-omit-frame-pointer -g -o myapp myapp.cpp

# Run your test suite — ASan will report any UAF with a full stack trace
./myapp

ASan detects UAF by poisoning freed memory regions and trapping any subsequent access. It should be a standard part of your CI pipeline for any C/C++ project.

5. Use Static Analysis Tools

Several static analyzers can detect potential UAF patterns without running the code:

Tool Type Notes
Clang Static Analyzer Static Free with LLVM, finds many UAF patterns
Coverity Static Industry standard, excellent UAF detection
CodeQL Static GitHub-native, query-based analysis
Valgrind/Memcheck Dynamic Slower than ASan but very thorough
Heaptrack Dynamic Heap profiling with UAF detection

6. Relevant Security Standards

This vulnerability maps to well-known classifications:

  • CWE-416: Use After Free — the canonical classification for this bug class
  • CWE-825: Expired Pointer Dereference — the broader category of dangling pointer issues
  • OWASP: Memory Management vulnerabilities (relevant to C/C++ secure coding guidelines)
  • SEI CERT C++ Coding Standard: Rule MEM01-C — Store a new value in pointers immediately after free()

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.

Frequently Asked Questions

What is Use-After-Free?

Use-After-Free is a memory corruption vulnerability where a program continues to use a pointer after the memory it references has been freed, potentially allowing attackers to execute arbitrary code or crash the application.

How do you prevent Use-After-Free in C++?

Prevent Use-After-Free by immediately setting pointers to nullptr after freeing them, using smart pointers (unique_ptr, shared_ptr), implementing proper RAII patterns, and zero-initializing structures after deallocation.

What CWE is Use-After-Free?

Use-After-Free is classified as CWE-416: Use After Free, which is categorized under memory safety vulnerabilities and is frequently listed in the CWE Top 25 Most Dangerous Software Weaknesses.

Is using smart pointers enough to prevent Use-After-Free?

Smart pointers significantly reduce Use-After-Free risks by automating memory management, but they don't eliminate all cases—especially when raw pointers are extracted, when dealing with move semantics, or when interfacing with C APIs.

Can static analysis detect Use-After-Free?

Yes, static analysis tools like Coverity, PVS-Studio, and clang-tidy can detect many Use-After-Free patterns, though some cases require dynamic analysis tools like AddressSanitizer or Valgrind for detection.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #1

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

How buffer overflow in memcpy() happens in Node.js N-API bindings and how to fix it

A critical buffer overflow vulnerability was discovered in the GetBufferAsVector() function in examples_nodejs/src/zupt_napi.cpp, where memcpy() copied data from JavaScript Uint8Array buffers without proper bounds validation. This vulnerability could allow attackers to trigger memory corruption by providing maliciously crafted input arrays to the native Node.js module, potentially leading to crashes or arbitrary code execution.