Back to Blog
critical SEVERITY8 min read

Critical Buffer Overflow in scanner.h: How a Missing Bounds Check Almost Broke Everything

A critical buffer overflow vulnerability was discovered and patched in `common/scanner.h`, where serialization macros wrote scanner state data into caller-supplied buffers without validating available capacity. Left unpatched, a crafted input could corrupt adjacent heap memory, potentially enabling remote code execution or application crashes. This post breaks down how the vulnerability worked, how it was fixed, and what every C/C++ developer should know to avoid similar pitfalls.

O
By Orbis AppSec
•Published May 18, 2026•Reviewed June 3, 2026

Answer Summary

This vulnerability is a critical buffer overflow (CWE-122: Heap-based Buffer Overflow) in C, located in the serialization macros of `common/scanner.h`. The macros wrote scanner state data into caller-supplied buffers without validating available capacity, allowing crafted input to overwrite adjacent heap memory. The fix adds explicit bounds checking before each write operation so that writes are only performed when sufficient buffer space is confirmed. Developers can prevent similar issues by always validating buffer size before writing, using safe bounded APIs, and running static analysis tools that trace tainted data to unsafe write sinks.

Vulnerability at a Glance

cweCWE-122
fixAdded bounds validation before each write in the serialization macros
riskHeap memory corruption, remote code execution, application crash
languageC
root causeSerialization macros in scanner.h wrote to caller-supplied buffers without checking available capacity
vulnerabilityHeap-based Buffer Overflow in serialization macros

Critical Buffer Overflow in scanner.h: How a Missing Bounds Check Almost Broke Everything

Severity: šŸ”“ Critical | CVE Type: Buffer Overflow (CWE-122) | Fixed In: Latest Release


Introduction

In the world of systems programming, few vulnerabilities are as dangerous — or as deceptively simple — as a buffer overflow. They've been responsible for some of the most devastating exploits in computing history, from the Morris Worm of 1988 to modern-day remote code execution chains. Yet they continue to appear in codebases large and small, often hiding in plain sight behind a single missing bounds check.

This post covers a critical buffer overflow vulnerability discovered and patched in common/scanner.h. The root cause? A memcpy call that trusted the caller to provide a sufficiently large buffer — without ever verifying that trust was warranted.

If you write C or C++, work with parsers or serialization code, or simply care about building robust software, this one's for you.


The Vulnerability Explained

What Was Happening

The common/scanner.h file contains serialization macros responsible for writing scanner state to a caller-supplied buffer. This state includes fields like:

  • html_depth
  • cfoutput_depth
  • cfcomponent_depth
  • cffunction_depth
  • Tag contents and accumulated nesting structures

Each field was written into the buffer using memcpy, and a size variable was incremented after each write to track how much data had been written. Here's the critical flaw: there was no check that (size + sizeof(field)) <= buffer_capacity before writing.

In pseudocode, the vulnerable pattern looked something like this:

// āŒ VULNERABLE: No bounds check before writing
#define SERIALIZE_FIELD(buf, size, field)       \
    memcpy((buf) + (size), &(field), sizeof(field)); \
    (size) += sizeof(field);

// Usage in scanner state serialization:
SERIALIZE_FIELD(output_buffer, size, scanner->html_depth);
SERIALIZE_FIELD(output_buffer, size, scanner->cfoutput_depth);
SERIALIZE_FIELD(output_buffer, size, scanner->cfcomponent_depth);
SERIALIZE_FIELD(output_buffer, size, scanner->cffunction_depth);
// ... and so on for tag contents

The size counter dutifully tracked how many bytes had been written, but nothing ever asked the critical question: "Is there still room in the buffer for what we're about to write?"

How Could It Be Exploited?

The vulnerability is triggered by crafting input that causes the scanner to accumulate deeply nested structures or a large number of tags. As the scanner processes this input, its internal state grows. When that state is serialized:

  1. The buffer fills up to its allocated capacity.
  2. memcpy continues writing beyond the buffer's boundary.
  3. Adjacent heap memory is overwritten with attacker-influenced data.

This is a classic heap buffer overflow, and it opens the door to several attack primitives:

  • Heap metadata corruption: Overwriting allocator bookkeeping data to manipulate future malloc/free behavior.
  • Adjacent object corruption: Overwriting fields in neighboring heap objects, potentially hijacking control flow.
  • Remote Code Execution (RCE): In the worst case, a sophisticated attacker can use heap grooming techniques to place a sensitive object (like a function pointer or vtable) adjacent to the vulnerable buffer, then overwrite it with controlled data.

Real-World Attack Scenario

Imagine a web application that uses this scanner to parse user-submitted HTML or ColdFusion markup before storing or rendering it. An attacker submits a document with an absurd level of tag nesting:

<div><div><div><div><div><div>... (thousands of levels deep) ...</div></div></div></div></div></div>

Or a document with thousands of tags, each contributing to the accumulated scanner state. When the application attempts to serialize the scanner's state (perhaps to cache it, log it, or pass it between components), the buffer overflows. Depending on heap layout, this could crash the server — or worse, hand the attacker the keys to the kingdom.

Why This Is Rated Critical

Buffer overflows in parsing code are particularly dangerous because:

  1. Parsers process untrusted input by design — that's their entire job.
  2. Heap overflows are exploitable — unlike some stack overflows, heap corruption can be leveraged even with modern mitigations like stack canaries.
  3. The attack surface is wide — any code path that serializes scanner state after processing untrusted input is affected.

This earns the Critical severity rating without hesitation.


The Fix

What Changed

The fix adds bounds checking to the serialization macros before every memcpy call. The principle is simple: before writing, verify there is enough space remaining in the buffer.

The corrected pattern looks like this:

// āœ… FIXED: Bounds check before every write
#define SERIALIZE_FIELD(buf, size, capacity, field)                    \
    do {                                                                \
        if ((size) + sizeof(field) > (capacity)) {                     \
            return SERIALIZE_ERROR_BUFFER_TOO_SMALL;                   \
        }                                                               \
        memcpy((buf) + (size), &(field), sizeof(field));               \
        (size) += sizeof(field);                                        \
    } while(0)

// Usage now safely checks capacity at every step:
SERIALIZE_FIELD(output_buffer, size, buffer_capacity, scanner->html_depth);
SERIALIZE_FIELD(output_buffer, size, buffer_capacity, scanner->cfoutput_depth);
SERIALIZE_FIELD(output_buffer, size, buffer_capacity, scanner->cfcomponent_depth);
SERIALIZE_FIELD(output_buffer, size, buffer_capacity, scanner->cffunction_depth);

How It Solves the Problem

The fix introduces three key improvements:

  1. Pre-write capacity check: Before each memcpy, the macro verifies that size + sizeof(field) <= capacity. If this check fails, the serialization is aborted with an error code rather than overflowing.

  2. Explicit capacity parameter: The buffer's capacity is now threaded through the serialization logic, making it impossible to call the macro without considering buffer limits.

  3. Fail-safe error handling: Rather than silently continuing (and corrupting memory), the code now returns a well-defined error that callers can detect and handle gracefully.

The do { ... } while(0) idiom is a C best practice for multi-statement macros — it ensures the macro behaves correctly in all syntactic contexts (e.g., inside if statements without braces).

A Note on Defense in Depth

While the bounds check is the primary fix, production-grade code should also consider:

// Consider using safer alternatives where possible
// Instead of raw memcpy, consider wrapper functions with built-in bounds checking:

static inline int serialize_field_safe(
    uint8_t *buf,
    size_t *size,
    size_t capacity,
    const void *field,
    size_t field_size
) {
    if (*size + field_size > capacity) {
        return -1; // ENOBUFS or custom error code
    }
    memcpy(buf + *size, field, field_size);
    *size += field_size;
    return 0;
}

Using a function instead of a macro provides better type safety, easier debugging, and cleaner stack traces when something goes wrong.


Prevention & Best Practices

1. Never Trust Buffer Sizes — Verify Them

The golden rule of C/C++ buffer management: always check before you write. This applies to memcpy, strcpy, sprintf, and any other function that writes to a caller-supplied buffer.

// āŒ Don't do this
memcpy(dest, src, len);

// āœ… Do this
if (dest_offset + len > dest_capacity) {
    return ERROR_BUFFER_OVERFLOW;
}
memcpy(dest + dest_offset, src, len);

2. Use Bounded Alternatives

The C standard library offers safer alternatives for many common operations:

Unsafe Safer Alternative
strcpy strncpy, strlcpy
sprintf snprintf
gets fgets
memcpy (unchecked) Wrapper with bounds check

3. Consider Modern C++ or Memory-Safe Languages

If you're writing new code, consider:

  • C++: Use std::vector, std::array, and std::span (C++20) which carry size information and support bounds-checked access.
  • Rust: The borrow checker and ownership model make this entire class of vulnerability impossible by default.
// C++20 with std::span — bounds checking built in
void serialize_scanner_state(std::span<uint8_t> buffer, const ScannerState& state) {
    size_t offset = 0;
    auto write_field = [&](const auto& field) -> bool {
        if (offset + sizeof(field) > buffer.size()) return false;
        std::memcpy(buffer.data() + offset, &field, sizeof(field));
        offset += sizeof(field);
        return true;
    };
    // Now every write is bounds-checked
}

4. Use Static Analysis Tools

Several tools can catch this class of vulnerability automatically:

  • Clang Static Analyzer (scan-build) — free, catches many buffer overflows
  • Coverity — commercial, industry-standard
  • CodeQL — GitHub's semantic analysis engine, excellent for C/C++
  • PVS-Studio — powerful static analyzer with buffer overflow detection
  • AddressSanitizer (ASan) — runtime detection, invaluable during testing

Add these to your CI pipeline:

# Example: Enable AddressSanitizer in CMake builds
cmake -DCMAKE_C_FLAGS="-fsanitize=address,undefined" \
      -DCMAKE_CXX_FLAGS="-fsanitize=address,undefined" \
      -DCMAKE_BUILD_TYPE=Debug ..

5. Fuzz Your Parsers

Parsers are high-value fuzzing targets. Tools like libFuzzer and AFL++ are excellent at finding exactly this type of vulnerability:

// libFuzzer harness for scanner code
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
    Scanner scanner;
    scanner_init(&scanner);
    scanner_parse(&scanner, (const char*)data, size);

    uint8_t output[1024];
    size_t written = 0;
    scanner_serialize(&scanner, output, sizeof(output), &written);

    scanner_destroy(&scanner);
    return 0;
}

Run this with AddressSanitizer enabled, and the fuzzer would have found this exact vulnerability.

6. Security Standards & References

This vulnerability maps to several well-known security standards:

  • CWE-122: Heap-based Buffer Overflow
  • CWE-120: Buffer Copy without Checking Size of Input ("Classic Buffer Overflow")
  • OWASP: Buffer Overflow
  • SEI CERT C: ARR38-C — Guarantee that library functions do not form invalid pointers
  • MITRE ATT&CK: T1203 — Exploitation for Client Execution

Conclusion

This vulnerability is a textbook example of why defensive programming matters in systems code. A single missing bounds check in a serialization macro — something that might seem trivial — created a critical heap overflow that could corrupt memory and potentially enable remote code execution.

The fix is equally instructive: it's not complex or clever. It's just a straightforward check that should have been there from the start. This is the essence of secure coding — not exotic techniques, but disciplined application of fundamentals.

Key takeaways:

  • āœ… Always validate buffer capacity before writing — no exceptions, no "the caller should handle it."
  • āœ… Parsers are high-risk code — they process untrusted input and deserve extra scrutiny.
  • āœ… Use static analysis and fuzzing — automated tools catch what code review misses.
  • āœ… Fail safely — when a buffer is too small, return an error; don't silently overflow.
  • āœ… Consider memory-safe abstractions — std::span, std::vector, or Rust can eliminate this class of bug entirely.

Security vulnerabilities like this one are found and fixed every day in open-source and commercial software alike. What matters is that we learn from each one — building better habits, better tooling, and better code.

Stay safe out there. And always check your bounds. šŸ›”ļø


This vulnerability was identified and fixed by OrbisAI Security. Automated security scanning helps catch issues like this before they reach production.

Frequently Asked Questions

What is a buffer overflow in C serialization macros?

A buffer overflow occurs when code writes more data into a fixed-size buffer than it can hold. In serialization macros, this happens when the macro writes scanner state fields into a caller-supplied buffer without first checking that enough space remains, causing writes to spill into adjacent memory.

How do you prevent buffer overflow in C serialization code?

Always validate that the destination buffer has sufficient remaining capacity before writing. Use bounded write functions, track a running offset and remaining-bytes counter, and return an error (or truncate safely) when the buffer would overflow. Static analysis tools can also flag unbounded writes at compile time.

What CWE is buffer overflow?

A heap-based buffer overflow is classified as CWE-122. Generic buffer overflows fall under CWE-120 (Classic Buffer Overflow) and CWE-787 (Out-of-bounds Write). The specific variant in scanner.h is CWE-122 because the destination buffers are heap-allocated by callers.

Is input validation alone enough to prevent buffer overflow in C?

No. Input validation reduces risk but is not sufficient on its own. You must also enforce destination buffer size limits at every write site. A crafted input that passes initial validation can still trigger overflow if the serialization logic doesn't track remaining buffer capacity.

Can static analysis detect buffer overflow in C macros?

Yes. Tools like Semgrep, Coverity, CodeQL, and AddressSanitizer (ASan) can detect unbounded writes in macros. Semgrep rules that trace tainted data to unsafe write sinks are particularly effective for catching this pattern in header-defined macros that inline across many translation units.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #29

Related Articles

high

How insecure string copy functions happen in C calculations.c and how to fix it

A high-severity buffer overflow vulnerability was discovered in `src/calculations.c` at line 37, where a two-step `strncpy` + manual null-termination pattern left the door open for subtle memory safety bugs when copying string data into the `entry->type` field. The fix replaces both lines with a single `snprintf` call that handles bounds and null-termination atomically, eliminating the risk entirely. This is a common C pitfall that affects production CLI tools and can be exploited when attacker-

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 integer overflow in buffer size calculation happens in C and how to fix it

A critical integer overflow vulnerability was discovered in the `nsh_setvar()` function in `nshlib/nsh_vars.c`, where the buffer size calculation `newsize = pstate->varsz + varlen` could wrap around, causing a heap buffer overflow. The fix adds overflow checking before the addition, preventing attackers with shell access from corrupting memory by setting variables with crafted names and values.

high

How buffer overflow via sprintf() happens in C networking code and how to fix it

A high-severity buffer overflow vulnerability was discovered in `profile.c` where `sprintf()` was used to format server addresses without any bounds checking. An attacker who could influence the `SERVER_BASE_PORT` value or trigger integer overflow in the port calculation could write beyond the `server_address` buffer. The fix replaces `sprintf()` with `snprintf()` using explicit buffer size limits at both call sites (lines 99 and 220).

critical

How integer overflow happens in C reliable.c and how to fix it

A critical integer overflow vulnerability was discovered in `reliable.c` at line 1299, where the `packet_buffer_size` calculation used signed `int` arithmetic that could wrap to a negative or undersized value when large `fragment_size` values were involved. By casting each operand to `size_t` before multiplication, the fix eliminates the overflow risk entirely and ensures the allocated buffer is always large enough to hold the reassembled packet data.

high

How insecure string copy functions happen in C (cyw43.c) and how to fix it

Three unsafe string copy calls in `src/cyw43.c` — including a bare `strcpy()` and two `strncpy()` calls — created buffer overflow risks in a CYW43 Wi-Fi driver emulation layer. The fix replaces all three with `snprintf()`, which enforces buffer size limits and guarantees null-termination in a single, consistent operation. Left unaddressed, these vulnerabilities could allow an attacker controlling input like a TAP interface name or SSID to corrupt adjacent memory and potentially execute arbitrary