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

critical

How arbitrary code execution via injected protobuf definition type fields happens in Node.js and how to fix it

A critical arbitrary code execution vulnerability (CVE-2026-41242) was discovered in protobufjs versions prior to 7.5.5 and 8.0.1, allowing attackers to inject malicious code through crafted protobuf definition type fields. The fix upgrades the dependency from the vulnerable version 6.11.4 to 7.5.5, which properly sanitizes type field inputs during protobuf parsing. This vulnerability is especially dangerous because protobufjs is widely used in Node.js applications for serialization, meaning a s

high

How command injection happens in Node.js child_process and how to fix it

A high-severity command injection vulnerability was discovered in `bootstrap.js` at line 34, where the `exec()` function was used to run shell commands constructed from a `folder` variable. By replacing `exec()` with `execFile()` and passing arguments as an array, the fix eliminates the shell interpolation entirely, closing the door on command injection attacks that could have affected all downstream consumers of this Node.js library.

high

How unsafe pickle deserialization happens in NumPy's np.load() and how to fix it

A high-severity arbitrary code execution vulnerability was discovered in `tools/ardy-engine/retarget.py` where `np.load()` was called with `allow_pickle=True`, enabling attackers to embed malicious pickle payloads in `.npz` files. The fix was a single-character change—switching `allow_pickle=True` to `allow_pickle=False`—that eliminates the deserialization attack vector while preserving the file's legitimate array data loading functionality.

high

How SSRF and Credential Leakage via Absolute URLs happens in axios and how to fix it

A high-severity vulnerability (CVE-2025-27152) in axios versions prior to 1.8.2 allowed Server-Side Request Forgery (SSRF) attacks and credential leakage when making HTTP requests with absolute URLs. This vulnerability was fixed by upgrading from axios 1.7.4 to 1.8.2 in both package.json and package-lock.json, eliminating the attack vector that could have exposed authentication tokens and enabled unauthorized server-side requests.

high

How pickle-based arbitrary code execution happens in PyTorch and how to fix it

A high-severity arbitrary code execution vulnerability was discovered in `scripts/export_joyvasa_audio.py` where `torch.load()` was called with `weights_only=False`, allowing any pickle-serialized Python object — including malicious code — to execute during checkpoint loading. The fix switches to `weights_only=True` and explicitly allowlists only the two non-standard classes the checkpoint actually requires: `argparse.Namespace` and `pathlib.PosixPath`. This closes a real code execution path tha

critical

How WebSocket protocol length header abuse happens in Node.js and how to fix it

A critical vulnerability (CVE-2026-54466) in websocket-driver versions prior to 0.7.5 allows attackers to corrupt WebSocket messages by manipulating protocol length headers. This can lead to data integrity issues, denial of service, or potentially arbitrary code execution in affected Node.js applications. The fix involves upgrading the websocket-driver dependency to version 0.7.5.