Back to Blog
critical SEVERITY6 min read

How heap buffer overflow happens in C UART response handling and how to fix it

A critical heap buffer overflow vulnerability was discovered in the AT client response handler (`sm_at_client.c`) where incoming UART data was copied into a fixed-size buffer without verifying available capacity. A compromised modem or malicious UART data could trigger arbitrary heap corruption. The fix replaces an assertion-only guard with proper bounds clamping using `MIN()` to ensure writes never exceed the `at_cmd_resp` buffer allocation.

O
By Orbis AppSec
Published June 4, 2026Reviewed June 4, 2026

Answer Summary

This is a heap buffer overflow (CWE-120) in C's `memcpy()` call within the AT client UART response handler (`sm_at_client.c:313`). The `response_handler` function copied remaining data into `at_cmd_resp` at offset `resp_len` without checking that the copy length wouldn't exceed the buffer's declared size. The fix calculates the remaining capacity (`sizeof(at_cmd_resp) - resp_len`) and uses `MIN()` to clamp the copy length, ensuring the buffer boundary is never exceeded.

Vulnerability at a Glance

cweCWE-120
fixClamp copy length to MIN(data_remaining, buffer_remaining) before memcpy
riskArbitrary heap corruption leading to code execution or device compromise
languageC (embedded/Zephyr RTOS)
root causememcpy copies UART data without verifying destination buffer has sufficient remaining capacity
vulnerabilityHeap buffer overflow via unchecked memcpy

How Heap Buffer Overflow Happens in C UART Response Handling and How to Fix It

Introduction

The lib/sm_at_client/sm_at_client.c file handles AT command responses received over UART from a cellular modem, but a flaw in the response_handler function at line 313 created a critical security risk. The function uses memcpy to copy incoming data into the at_cmd_resp buffer at offset resp_len, but never verifies that resp_len + copy_length stays within the buffer's allocated size (CONFIG_SM_AT_CLIENT_AT_CMD_RESP_MAX_SIZE).

This is a textbook CWE-120 vulnerability — a buffer copy without checking the size of input — but what makes it particularly dangerous is the attack surface: any entity that can inject data onto the UART line (a compromised modem, a hardware implant, or a man-in-the-middle on the serial bus) can trigger heap corruption on the host MCU.

The Vulnerability Explained

The AT client processes modem responses in a streaming fashion. When data arrives over UART, the response_handler function accumulates bytes in the static at_cmd_resp buffer. The vulnerable code path occurs when a response contains a terminator (like \r\n) partway through the received data — the function processes the first portion, then copies the remaining bytes into the buffer for the next response:

/* Copy the possibly remaining data to the buffer. */
if (copy_len < len) {
    assert((sizeof(at_cmd_resp) - resp_len) >= (len - copy_len));

    memcpy(at_cmd_resp + resp_len, data + copy_len, len - copy_len);
    resp_len += len - copy_len;
}

The critical problems:

  1. assert() is not a security control. In production builds compiled with NDEBUG, assertions are stripped entirely. The assert on line 310 provides zero runtime protection in release firmware.

  2. No bounds clamping. Even if the assertion fires in debug builds, it aborts the program rather than gracefully handling the overflow. There's no code path that limits len - copy_len to the available buffer space.

  3. Attacker-controlled input. The data and len parameters come directly from UART receive callbacks. A compromised cellular modem — or an attacker with physical access to the UART lines — controls both the content and length of this data.

Attack scenario:

An attacker sends a crafted AT response where the total payload exceeds CONFIG_SM_AT_CLIENT_AT_CMD_RESP_MAX_SIZE (256 bytes). For example:

  1. The attacker sends a legitimate-looking response prefix that fills most of at_cmd_resp (e.g., 250 bytes).
  2. In the same UART transfer, after a \r\n terminator, the attacker includes 200+ bytes of "remaining" data.
  3. The code calculates len - copy_len = 200, but only 256 - 0 = 256 bytes (or less) are available.
  4. memcpy writes past the heap buffer boundary, corrupting adjacent heap metadata or application data.

This 2-step exploitation chain (fill buffer + overflow on remainder) can achieve arbitrary write primitives on embedded systems with predictable heap layouts.

The Fix

The fix replaces the dangerous assert-then-copy pattern with proper bounds clamping:

Before (vulnerable):

/* Copy the possibly remaining data to the buffer. */
if (copy_len < len) {
    assert((sizeof(at_cmd_resp) - resp_len) >= (len - copy_len));

    memcpy(at_cmd_resp + resp_len, data + copy_len, len - copy_len);
    resp_len += len - copy_len;
}

After (fixed):

/* Copy the possibly remaining data to the buffer. */
if (copy_len < len) {
    size_t remaining = sizeof(at_cmd_resp) - resp_len;
    size_t copy2_len = MIN(len - copy_len, remaining);

    memcpy(at_cmd_resp + resp_len, data + copy_len, copy2_len);
    resp_len += copy2_len;
}

What changed and why:

  1. size_t remaining = sizeof(at_cmd_resp) - resp_len; — Explicitly calculates how many bytes the buffer can still accept. This makes the available capacity a first-class value in the logic.

  2. size_t copy2_len = MIN(len - copy_len, remaining); — Clamps the actual copy length to the lesser of "data available" and "space available." This is the core security invariant: buffer writes never exceed the declared length.

  3. Removed assert() — The assertion provided no protection in production. The MIN() macro provides deterministic, always-active protection regardless of build configuration.

  4. Uses copy2_len in both memcpy and resp_len update — Ensures the buffer position tracker stays consistent with what was actually written.

This is a minimal, surgical fix: 4 lines changed, no behavioral change for valid inputs, and complete protection against oversized inputs.

Prevention & Best Practices

For embedded C developers working with UART/serial data:

  1. Never trust peripheral input lengths. Data arriving from UART, SPI, I2C, or any external interface must be treated as attacker-controlled. Always validate lengths before copying.

  2. Don't use assert() for security checks. Assertions are debugging aids, not security controls. They're compiled out in release builds. Use explicit if checks with proper error handling.

  3. Adopt the MIN() clamping pattern. When copying variable-length data into fixed buffers:
    c size_t safe_len = MIN(input_len, sizeof(dest_buf) - current_offset); memcpy(dest_buf + current_offset, src, safe_len);

  4. Audit all memcpy calls with external data. Search your codebase for memcpy where the length parameter derives from external input without a preceding bounds check.

  5. Use compiler sanitizers in CI. AddressSanitizer (-fsanitize=address) catches heap overflows at runtime during testing, even when assertions are disabled.

  6. Consider safe buffer abstractions. Libraries like Zephyr's net_buf or custom ring buffers with built-in capacity tracking can eliminate entire classes of overflow bugs.

Key Takeaways

  • assert() is not a security boundary — the original code at line 310 of sm_at_client.c relied on an assertion that vanishes in production builds, leaving the memcpy completely unguarded.
  • UART response handlers are a real attack surface — a compromised modem can send arbitrarily long responses, making the AT client's response_handler a 2-step exploitation target.
  • The MIN(data_to_copy, space_available) pattern before memcpy is the canonical fix for CWE-120 in streaming data handlers.
  • The same vulnerable pattern existed at lines 285, 305, and 313 — when one unchecked memcpy is found, always audit nearby code for the same pattern.
  • Truncation is safer than corruption — dropping excess bytes (as the fix does) may cause a protocol error, but heap corruption can cause arbitrary code execution.

How Orbis AppSec Detected This

  • Source: UART receive callback providing data and len parameters from external modem hardware
  • Sink: memcpy(at_cmd_resp + resp_len, data + copy_len, len - copy_len) in lib/sm_at_client/sm_at_client.c:313
  • Missing control: No validation that len - copy_len does not exceed sizeof(at_cmd_resp) - resp_len; the existing assert() is stripped in production builds
  • CWE: CWE-120 (Buffer Copy without Checking Size of Input)
  • Fix: Replace assertion with MIN() clamping to ensure copy length never exceeds remaining buffer capacity

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.

Conclusion

This vulnerability demonstrates a pattern that's alarmingly common in embedded C code: using assert() as the sole guard against buffer overflow, then shipping production firmware with assertions disabled. The fix is elegant in its simplicity — four lines that enforce the invariant "buffer writes never exceed declared length" — but the security impact is profound. For any developer working with streaming data over hardware interfaces, the lesson is clear: validate every copy length against available capacity, every time, with code that survives all build configurations.

References

Frequently Asked Questions

What is a heap buffer overflow?

A heap buffer overflow occurs when a program writes data beyond the allocated boundaries of a heap buffer, corrupting adjacent memory structures and potentially enabling arbitrary code execution.

How do you prevent heap buffer overflow in C?

Always validate that the number of bytes to copy does not exceed the destination buffer's remaining capacity before calling memcpy, memmove, or similar functions. Use explicit bounds checks or safe wrapper functions.

What CWE is heap buffer overflow?

CWE-120 (Buffer Copy without Checking Size of Input), which is a child of CWE-119 (Improper Restriction of Operations within the Bounds of a Memory Buffer).

Is using assert() enough to prevent buffer overflow?

No. Assertions are typically compiled out in release/production builds (NDEBUG), leaving no runtime protection. Proper bounds clamping or error handling must be used instead.

Can static analysis detect heap buffer overflow?

Yes. Static analysis tools can detect patterns where memcpy is called without prior bounds validation, especially when the copy length is derived from external input like UART data.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #339

Related Articles

high

How Denial of Service via Infinite Loop Happens in JavaScript Dependencies and How to Fix It

CVE-2026-67213 is a high-severity denial of service vulnerability in nanoid before version 5.1.6 that triggers an infinite loop during random ID generation when processing specially crafted input. We upgraded nanoid across the entire dependency tree to patch this flaw and prevent attackers from freezing application threads. This fix ensures that ID generation remains resilient even when handling adversarial input patterns.

high

How Sensitive Data Exposure happens in Zotero plugins and how to fix it

A high-severity data exposure vulnerability in `Zotero.ts` automatically transmitted complete document metadata—including private notes, attachment paths, and tags—to external LLM services without user consent. The fix replaces broad `item.toJSON()` serialization with explicit field selection, sending only essential bibliographic data.

high

How missing dependency update cooldowns happen in GitHub Dependabot configurations and how to fix it

A semgrep scan flagged `.github/dependabot.yml` for lacking a cooldown period, meaning Dependabot would immediately propose updates to brand-new package versions across npm, Bundler, and Docker ecosystems. The fix adds a `cooldown: default-days: 7` block to every `package-ecosystem` entry, forcing a one-week waiting period before newly published releases are considered — reducing exposure to malicious or unstable package drops.

high

How dependabot-missing-cooldown happens in GitHub Actions/Node.js and how to fix it

The repository's `.github/dependabot.yml` had no cooldown period configured, meaning Dependabot could immediately propose updates to newly published package versions with zero time for the community to flag malware or instability. The fix adds a `cooldown` block with `default-days: 7` to both the `npm` and `github-actions` ecosystems, forcing a 7-day waiting period before new releases are surfaced as update PRs.

high

How Path Traversal Happens in TensorFlow's Data Service and How to Fix It

TensorFlow's data service dispatcher validated dataset IDs against forward-slash traversal attacks but overlooked backslash characters on non-Windows platforms, allowing attackers to escape the root directory. A targeted fix adds explicit backslash validation across all platforms, closing a high-severity path traversal vulnerability in the snapshot management system.

critical

How Unbounded WebSocket Message Handling Causes Resource Exhaustion in Node.js and How to Fix It

The WebSocketCrossServerAdapter class in a popular Node.js WebSocket library lacked any rate limiting on inbound messages, allowing attackers to flood Redis nodes and WebSocket servers with high-volume traffic. The fix introduces a configurable `rateLimit` option that caps messages per connection per second, preventing resource exhaustion while preserving legitimate functionality.