Back to Blog
critical SEVERITY8 min read

Heap Buffer Overflow in md2html: How Integer Overflow Corrupts Memory

A critical heap buffer overflow vulnerability was discovered and patched in the md2html Markdown-to-HTML converter, where an unchecked integer overflow in the `membuf_grow` function could allow attackers to corrupt heap memory via a crafted Markdown document. This class of vulnerability — CWE-120, a classic buffer overflow — remains one of the most dangerous and exploitable bug patterns in C code. The fix closes a real-world attack vector that could lead to arbitrary code execution or applicatio

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

Answer Summary

A heap buffer overflow vulnerability (CWE-120) in the C-based md2html Markdown converter was caused by an unchecked integer overflow in the `membuf_grow` function. When processing specially crafted Markdown documents, the function could allocate insufficient heap memory, leading to buffer overflow when writing converted HTML. The fix adds integer overflow detection before the `realloc()` call, ensuring the new buffer size calculation doesn't wrap around to a smaller value, preventing heap corruption and potential code execution. #

Vulnerability at a Glance

cweCWE-120 (Buffer Copy without Checking Size of Input)
fixAdd overflow detection before realloc() to validate size calculations don't exceed maximum safe values
riskArbitrary code execution, denial of service, heap memory corruption
languageC
root causeUnchecked integer arithmetic in buffer size calculation allows allocation of insufficient memory
vulnerabilityHeap Buffer Overflow via Integer Overflow in membuf_grow()

Introduction

If you've ever processed user-supplied Markdown content in a C-based application, this vulnerability should get your attention. A critical severity heap buffer overflow was identified and patched in md2html/md2html.c — a C library responsible for converting Markdown documents to HTML. The root cause? A deceptively simple arithmetic mistake: failing to check for integer overflow before calling realloc.

This type of bug, catalogued under CWE-120 (Buffer Copy without Checking Size of Input), has been responsible for some of the most severe exploits in computing history — from the classic gets() disasters to modern browser engine exploits. It's a reminder that in C, the programmer is the last line of defense against memory corruption.

Whether you're a systems programmer, a security engineer reviewing C codebases, or a developer who ships software that processes untrusted documents, understanding this vulnerability is essential.


The Vulnerability Explained

What Is a Heap Buffer Overflow?

A heap buffer overflow occurs when a program writes more data into a heap-allocated buffer than the buffer can hold. Unlike stack overflows (which corrupt return addresses and local variables), heap overflows corrupt adjacent heap metadata or other allocated objects — which can be just as dangerous, and often harder to detect.

The Vulnerable Code: membuf_grow

The vulnerability lives in the membuf_grow function in md2html/md2html.c at line 88. This function is responsible for dynamically growing a memory buffer to accommodate more data. Here's the conceptual shape of the vulnerable logic:

// VULNERABLE CODE (simplified for illustration)
static int membuf_grow(membuf_t *buf, size_t size) {
    size_t new_asize = buf->asize + size;  // ⚠️ No overflow check!

    char *new_data = realloc(buf->data, new_asize);
    if (new_data == NULL) {
        return -1;
    }

    buf->data  = new_data;
    buf->asize = new_asize;

    // Line 103: copies 'size' bytes into potentially undersized buffer
    memcpy(buf->data + buf->size, source, size);
    return 0;
}

The critical flaw is on the line computing new_asize:

size_t new_asize = buf->asize + size;  // Integer overflow possible!

On a 64-bit system, size_t is an unsigned 64-bit integer. If buf->asize is very large and size is also large, their sum can wrap around to a small value — a phenomenon called unsigned integer overflow (or more precisely, unsigned integer wraparound, since it's defined behavior in C but still catastrophically wrong here).

How the Overflow Leads to Heap Corruption

Here's the chain of events in an attack:

  1. Attacker crafts a Markdown document designed to cause the buffer to grow to a size near SIZE_MAX (the maximum value of size_t).
  2. membuf_grow is called with a size argument that, when added to the current buf->asize, wraps around to a small value (e.g., 0x10).
  3. realloc(buf->data, new_asize) is called with that small wrapped value, so it returns a tiny buffer — maybe just 16 bytes.
  4. memcpy at line 103 then copies the original (large) size bytes into this tiny buffer, blasting past its boundaries and corrupting adjacent heap memory.
Before overflow:
  buf->asize = 0xFFFFFFFFFFFFFFF0  (near SIZE_MAX)
  size       = 0x0000000000000020  (32 bytes)
  new_asize  = 0x0000000000000010  (wraps to 16!) ← BUG

realloc returns a 16-byte buffer.
memcpy writes 32 bytes into it.
💥 Heap corruption.

Real-World Impact

Depending on how the application uses the heap and what lives in adjacent memory, this vulnerability could enable:

  • Denial of Service (DoS): The most reliable outcome — corrupting heap metadata causes a crash (SIGABRT, SIGSEGV, or a malloc consistency check failure).
  • Arbitrary Code Execution: A skilled attacker can potentially shape the heap layout to overwrite function pointers, vtable entries, or other critical data, achieving full code execution.
  • Information Disclosure: Heap corruption can sometimes cause the program to read and expose memory it shouldn't.

The attack surface is any code path that processes attacker-controlled Markdown input — which includes web servers, document converters, content management systems, and desktop applications that render user-provided content.


The Fix

What Changed

The fix adds an integer overflow check before computing new_asize, ensuring that if the addition would overflow, the function returns an error instead of proceeding with a corrupted size value.

Here is the corrected logic:

// FIXED CODE (simplified for illustration)
static int membuf_grow(membuf_t *buf, size_t size) {
    // ✅ Check for integer overflow BEFORE the addition
    if (size > SIZE_MAX - buf->asize) {
        return -1;  // Refuse to proceed — overflow would occur
    }

    size_t new_asize = buf->asize + size;  // Now safe

    char *new_data = realloc(buf->data, new_asize);
    if (new_data == NULL) {
        return -1;
    }

    buf->data  = new_data;
    buf->asize = new_asize;

    memcpy(buf->data + buf->size, source, size);
    return 0;
}

Why This Fix Works

The guard condition size > SIZE_MAX - buf->asize is the standard, correct idiom for detecting unsigned integer overflow in C before it happens:

  • SIZE_MAX - buf->asize computes the maximum value that can be safely added to buf->asize without wrapping.
  • If size exceeds that maximum, we know the addition would overflow, and we return an error immediately.
  • Because we check before the addition, the overflow never occurs, and realloc is never called with a dangerously small size.

This is a minimal, surgical fix: no performance impact, no behavioral change for valid inputs, and complete protection against the overflow attack vector.

Why Not Use calloc or Other Alternatives?

Some might suggest restructuring the allocation entirely. That's a valid long-term improvement, but the overflow guard is the correct and direct fix for this specific bug. It follows the principle of least change — patching the exact flaw without introducing unintended side effects.


Conclusion

The membuf_grow heap buffer overflow is a crisp illustration of why arithmetic on sizes in C demands explicit overflow checking. A single missing guard — if (size > SIZE_MAX - buf->asize) — was the difference between safe memory management and a critical, potentially exploitable heap corruption vulnerability.

Key takeaways:

  • ✅ Always validate size arithmetic before calling malloc, realloc, or memcpy.
  • ✅ Use compiler sanitizers (-fsanitize=address,undefined) during development and testing.
  • ✅ Fuzz your parsers — document parsers are a prime target for crafted-input attacks.
  • ✅ Integrate static analysis into your CI pipeline to catch these issues before they ship.
  • ✅ Follow CERT C and CWE guidance for integer and memory safety in C code.

The fix here was small — just a few lines — but its impact is enormous. This is the nature of memory safety bugs in C: the vulnerability is tiny, the consequences are not.

If your project processes untrusted Markdown, HTML, or any structured document format in C or C++, take this as an opportunity to audit your buffer growth and size calculation logic. The next crafted document in your input stream might be looking for exactly this kind of mistake.


This vulnerability was identified and patched by OrbisAI Security. Automated security scanning and remediation helps teams find and fix issues like this before they reach production.

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #330

Related Articles

medium

path-to-regexp 0.1.12 DoS: Catastrophic Backtracking on Malformed URL

path-to-regexp version 0.1.12 contains a regular expression engine vulnerability where maliciously crafted URL parameters trigger exponential backtracking, causing CPU exhaustion and service unavailability. Upgrading to version 0.1.13 eliminates the vulnerable pattern from the parsing logic.

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.

high

MapManager.get() Race Condition Duplicates API Requests

The MapManager's `get(mapUid, cache)` method used a check-then-act pattern that permitted multiple concurrent requests to pass the cache miss check simultaneously, triggering redundant API calls and risking cache corruption. The fix introduces a `_pending` promise map to deduplicate in-flight fetches for identical map UIDs.

critical

Lampa Desktop Auto-Update Heuristic Bypass: Execution of Unverified

Lampa Desktop's auto-update mechanism downloaded JavaScript and CSS from `raw.githubusercontent.com` using only heuristic validation—file size thresholds and string pattern matching—that attackers could trivially satisfy. The fix introduces cryptographic integrity verification by cross-referencing Git blob hashes from the GitHub Contents API, ensuring downloaded code matches the repository's authoritative state before execution.

high

adm-zip 0.6.0 Preserves SUID Bits From ZIPs: CVE-2026-102282

The `adm-zip` dependency resolved to 0.6.0 in this project's dependency tree, a version affected by CVE-2026-102282: during extraction it applies the Unix permission bits stored in each ZIP entry's external file attributes verbatim, including the setuid (`04000`), setgid (`02000`), and sticky bits. An attacker who controls an archive passed to `extractAllTo()` or `extractEntryTo()` can therefore have the extractor create a setuid binary owned by whatever user the extraction process runs as. The

high

requestInput() Type Confusion: NaN and Object Bypass in JavaScript

The `requestInput()` utility function lacked validation on its `type` parameter and failed to handle `NaN` results from float conversions, creating a type confusion weakness. An attacker could supply malformed inputs that propagate unhandled `NaN` values or unexpected object types through the type system. The fix adds explicit guards against `NaN` type parameters and rejects non-primitive type values.