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
Published May 16, 2026Reviewed 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()

Heap Buffer Overflow in md2html: How Integer Overflow Corrupts Memory

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.


Prevention & Best Practices

This vulnerability is a textbook example of a class of bugs that C programmers must actively guard against. Here's how to prevent similar issues:

1. Always Check for Integer Overflow Before Arithmetic on Sizes

Never compute a buffer size without validating the inputs first. Use the standard idiom:

// Safe addition check for size_t
if (b > SIZE_MAX - a) {
    // overflow would occur — handle error
}
size_t result = a + b;

// Safe multiplication check for size_t
if (a != 0 && b > SIZE_MAX / a) {
    // overflow would occur — handle error
}
size_t result = a * b;

2. Use Safe Integer Libraries

For complex arithmetic on sizes, consider using safe integer libraries:

size_t new_asize;
if (__builtin_add_overflow(buf->asize, size, &new_asize)) {
    return -1;  // Overflow detected
}

3. Enable Compiler and Sanitizer Warnings

Modern tooling can catch these issues at development time:

# AddressSanitizer catches heap overflows at runtime
clang -fsanitize=address,undefined -o myapp myapp.c

# UndefinedBehaviorSanitizer catches integer issues
clang -fsanitize=integer -o myapp myapp.c

# Enable all warnings
gcc -Wall -Wextra -Wconversion -o myapp myapp.c

4. Use Static Analysis Tools

Integrate static analysis into your CI/CD pipeline:

  • Coverity — excellent at finding integer overflow bugs
  • CodeQL — GitHub's semantic code analysis engine
  • Flawfinder — lightweight C/C++ security scanner
  • PVS-Studio — commercial but powerful

5. Fuzz Test Your Parsers

Any code that processes untrusted input — especially document parsers — should be fuzz tested:

# Using AFL++ to fuzz a Markdown parser
afl-fuzz -i corpus/ -o findings/ -- ./md2html @@

Fuzzing is extraordinarily effective at finding exactly this type of bug, because it generates crafted inputs that exercise edge cases like near-SIZE_MAX sizes.

6. Consider Memory-Safe Alternatives

For new projects, consider languages with built-in memory safety:
- Rust — prevents buffer overflows and integer overflows at compile time (with debug builds panicking on overflow)
- Go — garbage collected with bounds checking
- Zig — explicit overflow handling with @addWithOverflow

If you must use C, follow the CERT C Coding Standard, particularly:
- INT30-C: Ensure that unsigned integer operations do not wrap
- MEM35-C: Allocate sufficient memory for an object

Security Standards References

Standard Reference
CWE CWE-120: Buffer Copy without Checking Size of Input
CWE CWE-190: Integer Overflow or Wraparound
OWASP A03:2021 – Injection / Memory Corruption
CERT C INT30-C

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.

Frequently Asked Questions

What is a heap buffer overflow?

A heap buffer overflow occurs when a program writes more data to a dynamically allocated buffer than it can hold, corrupting adjacent heap memory. This can overwrite critical data structures or function pointers, potentially allowing arbitrary code execution.

How do you prevent heap buffer overflow in C?

Always validate buffer sizes before allocation, use safe functions like snprintf() instead of sprintf(), enable compiler protections (stack canaries, ASLR), perform bounds checking before writes, and use static analysis tools to detect unsafe patterns.

What CWE is heap buffer overflow?

Heap buffer overflow falls under CWE-120 (Buffer Copy without Checking Size of Input) and CWE-122 (Heap-based Buffer Overflow). Integer overflow leading to buffer overflow is often tracked as CWE-190 (Integer Overflow or Wraparound).

Is bounds checking enough to prevent heap buffer overflow?

Bounds checking on the write destination is essential, but you must also validate that the allocation itself succeeded and wasn't undersized due to integer overflow. Both the allocation size calculation and the write operations need protection.

Can static analysis detect heap buffer overflow vulnerabilities?

Yes, modern static analysis tools can detect many buffer overflow patterns, including integer overflow in size calculations. Tools like Clang Static Analyzer, Coverity, and Orbis AppSec can identify unsafe buffer operations and allocation vulnerabilities. #

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #330

Related Articles

high

How missing Dependabot cooldown happens in GitHub Actions and how to fix it

A high-severity configuration vulnerability was discovered in a `.github/dependabot.yml` file that lacked a cooldown period for package updates. Without this safeguard, Dependabot could immediately propose updates to newly published package versions—including potentially malicious or unstable releases. The fix adds a simple `cooldown` block with a 7-day waiting period before any new package version is suggested.

high

How Server-Sent Events Injection via Unsanitized Newlines happens in Node.js h3 and how to fix it

A high-severity Server-Sent Events (SSE) injection vulnerability (CVE-2026-33128) was discovered in the h3 HTTP framework, where unsanitized newline characters in event stream fields could allow attackers to inject arbitrary SSE messages. The fix upgrades h3 from version 1.15.5 to 1.15.6 in the frontend's dependency tree, ensuring that newline characters are properly sanitized before being written to event streams.

high

How Memory Exhaustion via Large Comma-Separated Selector Lists happens in Python Soup Sieve and how to fix it

A high-severity memory exhaustion vulnerability (CVE-2026-49476) was discovered in Soup Sieve version 2.8.3, affecting Python applications that parse CSS selectors from user-controlled input. The vulnerability allows attackers to craft malicious selector lists that consume excessive memory, potentially causing denial of service. The fix involves upgrading to soupsieve 2.8.4, which implements proper resource limits on selector parsing.

high

How prototype pollution via `__proto__` key happens in Node.js defu and how to fix it

A high-severity prototype pollution vulnerability (CVE-2026-35209) was discovered in the `defu` package version 6.1.4, which allowed attackers to inject properties into JavaScript's `Object.prototype` via the `__proto__` key in defaults arguments. The fix upgrades `defu` to version 6.1.5 in the frontend's dependency tree, protecting downstream consumers like `c12` and `dotenv` configuration loaders from malicious property injection.

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.

high

How memory exhaustion via large comma-separated selector lists happens in Python soupsieve and how to fix it

A high-severity memory exhaustion vulnerability (CVE-2026-49476) was discovered in soupsieve 2.8.3, a CSS selector library used by BeautifulSoup in Python. An attacker who could influence CSS selector input could craft large comma-separated selector lists to exhaust system memory, causing denial of service. The fix upgrades soupsieve from 2.8.3 to 2.8.4 in the backend's `uv.lock` dependency file.