Back to Blog
critical SEVERITY9 min read

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

A critical buffer overflow vulnerability was discovered in `sm_at_sms.c`, where three consecutive unsafe string operations — `sprintf()`, `strcpy()`, and `strcat()` — wrote SMS payload data into a fixed-size buffer without any bounds checking. An attacker capable of crafting an oversized SMS message could overflow `sms_ctx.concat_rsp_buf`, corrupting adjacent stack or heap memory. The fix replaces all three unsafe calls with their bounds-aware counterparts: `snprintf()` and `strcat_s()`.

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

Answer Summary

This is a classic C buffer overflow vulnerability (CWE-121: Stack-based Buffer Overflow) in `sm_at_sms.c`, caused by using `sprintf()`, `strcpy()`, and `strcat()` to write SMS payload data into a fixed-size buffer (`sms_ctx.concat_rsp_buf`) without any length limits. An attacker who can send a crafted oversized SMS can overflow the buffer and corrupt adjacent memory. The fix replaces all three unsafe calls with `snprintf()` (with explicit size limits) and `strcat_s()` (with a computed `buf_size`), ensuring writes are bounded by the actual allocated buffer size.

Vulnerability at a Glance

cweCWE-121 (Stack-based Buffer Overflow) / CWE-120 (Buffer Copy without Checking Size of Input)
fixReplace sprintf() with snprintf(), strcpy() with snprintf("%s"), and strcat()/strncat() with strcat_s() using a computed buffer size
riskAttacker-controlled SMS payload can overflow sms_ctx.concat_rsp_buf, enabling memory corruption, code execution, or denial of service
languageC
root causesprintf(), strcpy(), and strcat() used to write SMS payloads into a fixed-size buffer with no length bounds enforced
vulnerabilityStack/Heap Buffer Overflow via Unbounded String Operations

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

Introduction

The sm_at_sms.c file is responsible for handling incoming SMS messages in an embedded modem application — including the tricky case of concatenated SMS (long messages split across multiple parts). The sms_concat_handle() function assembles these parts into a single response buffer, sms_ctx.concat_rsp_buf. But a flaw in how three consecutive string operations wrote into that buffer created a critical, exploitable memory corruption vulnerability.

At line 127, sprintf() wrote the SMS header and first payload segment with no length limit. At line 137, strcpy() copied subsequent payload segments with no bounds check. At line 149, strcat() concatenated all segments together — again, without knowing whether the destination buffer had room. Any one of these would be a problem. Together, they compounded the overflow risk dramatically.

For developers working on embedded C, modem firmware, or any code that processes external message payloads, this is a textbook example of how "it works in testing" can mask a real-world attack surface.


The Vulnerability Explained

What Went Wrong

The vulnerable code in sms_concat_handle() used three unsafe C string functions in sequence to build a response buffer from incoming SMS data:

1. Unbounded sprintf() at line 127:

// VULNERABLE — no size limit on the write
sprintf(sms_ctx.concat_rsp_buf,
    "\r\n#XSMS: \"%02d-%02d-%02d %02d:%02d:%02d "
    "UTC%+03d:%02d\",\"%s\",\"%s",
    header->time.year, header->time.month, header->time.day,
    header->time.hour, header->time.minute, header->time.second,
    header->time.utc_offset_h, header->time.utc_offset_m,
    header->originating_address.address_str,
    data->payload);

sprintf() writes into sms_ctx.concat_rsp_buf without knowing its size. If data->payload is larger than expected, the write overflows the buffer.

2. Unbounded strcpy() at line 137:

// VULNERABLE — copies payload with no length check
strcpy(sms_ctx.concat_rsp_buf + SM_SMS_AT_HEADER_INFO_MAX_LEN +
       (header->concatenated.seq_number - 1) * SMS_MAX_PAYLOAD_LEN_CHARS,
       data->payload);

For subsequent SMS segments (sequence number > 1), this strcpy() writes data->payload at a computed offset into the buffer. If data->payload exceeds SMS_MAX_PAYLOAD_LEN_CHARS, it overflows into whatever memory follows.

3. Unbounded strcat() at line 149:

// VULNERABLE — concatenates without knowing remaining buffer space
for (int i = 1; i < (sms_ctx.total_msgs); i++) {
    strncat(sms_ctx.concat_rsp_buf,
            sms_ctx.concat_rsp_buf + SM_SMS_AT_HEADER_INFO_MAX_LEN +
            i * SMS_MAX_PAYLOAD_LEN_CHARS,
            SMS_MAX_PAYLOAD_LEN_CHARS);
}
strcat(sms_ctx.concat_rsp_buf, "\"\r\n");

Even the strncat() here is dangerous — its third argument limits how many bytes are appended, but does not account for how full the destination buffer already is. And the final strcat() has no limit at all.

How an Attacker Could Exploit This

SMS protocols allow message payloads up to specific sizes, but a malicious or malformed SMS source (or a compromised radio stack) could send a payload that exceeds SMS_MAX_PAYLOAD_LEN_CHARS. Because none of the three string operations validate the incoming length against the buffer capacity, an attacker who can inject an oversized payload into the SMS processing pipeline could:

  1. Overflow sms_ctx.concat_rsp_buf by sending a first SMS segment with a payload longer than the allocated space.
  2. Corrupt adjacent stack or heap memory — depending on where sms_ctx lives, this could overwrite return addresses, function pointers, or other critical data structures.
  3. Achieve arbitrary code execution on the embedded device, or at minimum cause a crash (denial of service) in the modem application.

The three consecutive unsafe operations are particularly dangerous: even if the first sprintf() doesn't overflow, the strcpy() for a later segment or the strcat() loop during reassembly can still trigger corruption.

Real-World Impact

In an embedded modem context, this kind of vulnerability is especially serious. Modem firmware often runs with elevated privileges and minimal OS-level memory protections. A successful exploit could allow an attacker to:

  • Hijack the modem's execution flow
  • Exfiltrate data processed by the device
  • Brick the device through memory corruption
  • Use the modem as a pivot point in a larger attack against connected systems

The Fix

The fix replaces all three unsafe operations with their bounds-aware counterparts, and introduces an explicit buf_size calculation to ensure every write is constrained to the actual allocated buffer.

Before and After

Fix 1: sprintf()snprintf() with explicit size

// BEFORE
sprintf(sms_ctx.concat_rsp_buf,
    "\r\n#XSMS: \"%02d-%02d-%02d %02d:%02d:%02d "
    "UTC%+03d:%02d\",\"%s\",\"%s",
    ...
    data->payload);

// AFTER
snprintf(sms_ctx.concat_rsp_buf,
    SM_SMS_AT_HEADER_INFO_MAX_LEN + SMS_MAX_PAYLOAD_LEN_CHARS,
    "\r\n#XSMS: \"%02d-%02d-%02d %02d:%02d:%02d "
    "UTC%+03d:%02d\",\"%s\",\"%s",
    ...
    data->payload);

snprintf() accepts a maximum number of bytes to write (including the null terminator), so even an oversized data->payload cannot overflow the buffer.

Fix 2: strcpy()snprintf("%s") with size limit

// BEFORE
strcpy(sms_ctx.concat_rsp_buf + SM_SMS_AT_HEADER_INFO_MAX_LEN +
       (header->concatenated.seq_number - 1) * SMS_MAX_PAYLOAD_LEN_CHARS,
       data->payload);

// AFTER
snprintf(sms_ctx.concat_rsp_buf + SM_SMS_AT_HEADER_INFO_MAX_LEN +
         (header->concatenated.seq_number - 1) * SMS_MAX_PAYLOAD_LEN_CHARS,
         SMS_MAX_PAYLOAD_LEN_CHARS,
         "%s", data->payload);

Using snprintf() with SMS_MAX_PAYLOAD_LEN_CHARS as the size limit ensures each segment slot in the buffer can only receive exactly as much data as was allocated for it. Note the use of "%s" as the format string — this is important to avoid format string injection if data->payload ever contains % characters.

Fix 3: strncat()/strcat()strcat_s() with computed buf_size

// BEFORE
for (int i = 1; i < (sms_ctx.total_msgs); i++) {
    strncat(sms_ctx.concat_rsp_buf,
            sms_ctx.concat_rsp_buf + SM_SMS_AT_HEADER_INFO_MAX_LEN +
            i * SMS_MAX_PAYLOAD_LEN_CHARS,
            SMS_MAX_PAYLOAD_LEN_CHARS);
}
strcat(sms_ctx.concat_rsp_buf, "\"\r\n");

// AFTER
uint16_t buf_size = SM_SMS_AT_HEADER_INFO_MAX_LEN +
    SMS_MAX_PAYLOAD_LEN_CHARS * sms_ctx.total_msgs;

for (int i = 1; i < (sms_ctx.total_msgs); i++) {
    strcat_s(sms_ctx.concat_rsp_buf, buf_size,
             sms_ctx.concat_rsp_buf + SM_SMS_AT_HEADER_INFO_MAX_LEN +
             i * SMS_MAX_PAYLOAD_LEN_CHARS);
}
strcat_s(sms_ctx.concat_rsp_buf, buf_size, "\"\r\n");

The key improvement here is the introduction of buf_size, computed as the total allocated size of concat_rsp_buf based on the header size plus one slot per expected message. strcat_s() uses this total size to prevent any write that would exceed the buffer's capacity — unlike strncat(), which only limits the appended bytes and ignores how full the destination already is.

Why Each Change Matters

Unsafe Function Problem Safe Replacement Why It's Better
sprintf() No write limit snprintf() with explicit size Truncates output if payload exceeds limit
strcpy() Copies until null byte snprintf("%s", ...) with size Limits copy to per-slot allocation
strncat() Limits appended bytes, not total strcat_s() with buf_size Limits based on total buffer capacity
strcat() No limit whatsoever strcat_s() with buf_size Same as above

Prevention & Best Practices

Never Use These Functions with Untrusted Input

The C standard library functions sprintf(), strcpy(), strcat(), and gets() are sometimes called the "C hall of shame" — they have no concept of buffer size and should be treated as deprecated in any security-sensitive context:

// ❌ Never use these with external data
sprintf(buf, fmt, user_data);
strcpy(dst, user_data);
strcat(dst, user_data);
gets(buf);

// ✅ Always use bounds-checked alternatives
snprintf(buf, sizeof(buf), fmt, user_data);
snprintf(dst, sizeof(dst), "%s", user_data);  // or strncpy + explicit null
strcat_s(dst, sizeof(dst), user_data);        // C11 Annex K
fgets(buf, sizeof(buf), stdin);

Compute Buffer Sizes Explicitly

In the fixed code, buf_size is computed as:

uint16_t buf_size = SM_SMS_AT_HEADER_INFO_MAX_LEN +
    SMS_MAX_PAYLOAD_LEN_CHARS * sms_ctx.total_msgs;

This pattern — calculating the expected maximum size from known constants and runtime values — should be standard practice whenever you're building a buffer from multiple variable-length pieces.

Use sizeof() Where Possible

For stack-allocated buffers, prefer sizeof(buf) over a hardcoded constant:

char buf[256];
snprintf(buf, sizeof(buf), "%s", input);  // sizeof automatically tracks the size

Enable Compiler Warnings and Sanitizers

Modern compilers and sanitizers can catch many of these issues at build time or runtime:

# GCC/Clang: enable relevant warnings
gcc -Wall -Wformat -Wformat-overflow -Wformat-truncation ...

# AddressSanitizer: catches buffer overflows at runtime during testing
gcc -fsanitize=address ...

# For embedded: consider using a static analysis tool like cppcheck
cppcheck --enable=all sm_at_sms.c

Relevant Standards

  • CWE-120: Buffer Copy without Checking Size of Input ("Classic Buffer Overflow")
  • CWE-121: Stack-based Buffer Overflow
  • OWASP: Input Validation and Output Encoding are foundational controls
  • SEI CERT C Coding Standard: Rules STR31-C (guarantee storage for strings) and STR07-C (use TR 24731 for remediation)

Key Takeaways

  • sprintf() with a user-controlled format argument or payload is never safe — in sms_concat_handle(), the SMS payload flowed directly into sprintf() with no size limit, making every incoming SMS a potential overflow trigger.
  • Three consecutive unsafe operations compound risk — even if one operation doesn't overflow on its own, the combination of sprintf() + strcpy() + strcat() in the same function creates multiple attack surfaces that are harder to reason about.
  • strncat() is not as safe as it looks — its third argument limits appended bytes, not total buffer usage; strcat_s() with the full buffer size is the correct replacement.
  • Always compute buf_size from constants you control — the fix's introduction of buf_size = SM_SMS_AT_HEADER_INFO_MAX_LEN + SMS_MAX_PAYLOAD_LEN_CHARS * sms_ctx.total_msgs makes the buffer's capacity explicit and auditable.
  • Embedded and modem firmware deserves the same security rigor as application code — the lack of OS memory protections in these environments makes buffer overflows more exploitable, not less.

How Orbis AppSec Detected This

  • Source: Incoming SMS message payload (data->payload) received from the radio/modem stack and passed into sms_concat_handle() in app/src/sm_at_sms.c
  • Sink: Unbounded sprintf() at line 127, strcpy() at line 137, and strcat() at line 149, all writing into the fixed-size buffer sms_ctx.concat_rsp_buf
  • Missing control: No length validation of data->payload against the remaining capacity of sms_ctx.concat_rsp_buf before any of the three write operations
  • CWE: CWE-120 (Buffer Copy without Checking Size of Input) / CWE-121 (Stack-based Buffer Overflow)
  • Fix: Replaced sprintf() with snprintf() (explicit size), strcpy() with snprintf("%s", ...) (per-slot size), and strncat()/strcat() with strcat_s() (computed buf_size)

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

Buffer overflows caused by unsafe C string functions are among the oldest and most well-understood vulnerability classes in software security — yet they continue to appear in production codebases, particularly in embedded and firmware contexts where external data (like SMS payloads) flows into fixed-size buffers. The three-function combination of sprintf(), strcpy(), and strcat() in sms_concat_handle() created a compounding overflow risk that could have allowed an attacker to corrupt memory simply by sending a crafted SMS message.

The fix is straightforward: replace every unbounded string operation with a bounds-aware alternative, compute buffer sizes explicitly from known constants, and never trust that an external payload will fit within your buffer. These habits, applied consistently, eliminate an entire class of critical vulnerabilities.


References

Frequently Asked Questions

What is a buffer overflow in C SMS handling?

It occurs when string functions like sprintf() or strcpy() write more data into a fixed-size buffer than it can hold, overwriting adjacent memory — in this case triggered by an oversized SMS payload written into sms_ctx.concat_rsp_buf.

How do you prevent buffer overflows in C string handling?

Always use bounds-checked alternatives: snprintf() instead of sprintf(), strncpy() or snprintf("%s") instead of strcpy(), and strcat_s() or strncat() with explicit size limits instead of strcat().

What CWE is a buffer overflow from unsafe sprintf/strcpy?

CWE-120 (Buffer Copy without Checking Size of Input, "Classic Buffer Overflow") and CWE-121 (Stack-based Buffer Overflow) both apply here.

Is strncat() enough to prevent buffer overflow in concatenation loops?

Not always — strncat()'s third argument is the maximum number of bytes to append, not the total buffer size, which can still lead to overflow if the destination is already near-full. strcat_s() with the total buffer size is safer.

Can static analysis detect buffer overflows like this one?

Yes — static analysis tools (including Orbis AppSec's multi-agent AI scanner) can trace tainted data from input sources like SMS payloads through unsafe sink functions like sprintf() and strcpy() to flag these patterns before they reach production.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #294

Related Articles

critical

How out-of-bounds reads happen in C gettext .mo file parsers and how to fix it

A missing bounds check in the gettext `.mo` file parser inside `compose/asc-utils-l10n.c` allowed a malformed or truncated file to trigger out-of-bounds reads from heap memory. The vulnerability affected two distinct read sites — a `memcpy` of the full `AscLocaleGettextHeader` struct at line 131 and a 4-byte offset read at line 224 — neither of which validated that the source buffer was large enough. The fix adds explicit size checks before both reads, rejecting invalid files with a descriptive

critical

How integer overflow in regexJIT.c heap allocation happens in C and how to fix it

A critical integer overflow vulnerability in `regex_src/regexJIT.c` allowed crafted regex patterns to trigger a heap buffer overflow by causing an unchecked multiplication of `sizeof(struct stack_item) * dfa_size` to wrap around on 32-bit platforms, resulting in an undersized allocation. The fix adds a pre-allocation overflow guard that returns `REGEX_MEMORY_ERROR` before any dangerous write can occur. Left unpatched, this vulnerability could be exploited to corrupt heap memory, crash the proces

critical

How kernel stack buffer overflow happens in C vsprintf() and how to fix it

A critical stack buffer overflow vulnerability was discovered in `sys/kern/debug.c` where the kernel's `printf()` function called a custom `vsprintf()` implementation without any length constraint on the output buffer `db_msg`. By replacing the unbounded `vsprintf()` call with a size-aware `vsnprintf()` implementation, the fix prevents crafted format strings or oversized arguments from overwriting kernel stack memory, closing a path to arbitrary kernel code execution.

critical

How buffer overflow happens in C MCP protocol parsing and how to fix it

A critical buffer overflow vulnerability (CWE-120) was discovered in the `mcp_frame_process_input()` function in `src/mcp.c` at line 1384. The function used unsafe `strncpy()` calls to copy network-sourced MCP protocol messages into fixed-size buffers without proper bounds checking, allowing remote attackers to overflow the buffer and potentially execute arbitrary code. The fix replaced all `strncpy()` calls with `snprintf()` and added a buffer size validation check.

medium

How buffer overflow happens in C kernel PTY subsystem (tty_ptmx.c) and how to fix it

A stack buffer overflow vulnerability was discovered in `tty_ptmx.c`, the kernel-level pseudo-terminal multiplexer component, where an unchecked `sprintf()` call at line 293 could overflow the `device_name` buffer by combining `root_path` and `dev_rel_path` without bounds validation. Because this code executes in kernel context during PTY device creation, successful exploitation could lead to kernel memory corruption, privilege escalation, or system crashes. The fix replaces the unbounded `sprin

critical

How command injection happens in Python subprocess and how to fix it

A command injection vulnerability in `skills/skill-comply/scripts/runner.py` allowed attackers who could influence skill definition files to execute arbitrary binaries on the host system via `subprocess.run()`. The fix introduces an explicit allowlist of permitted executables (`ALLOWED_SETUP_EXECUTABLES`) that gates every command before it reaches the subprocess call at line 110. This closes a significant attack surface in the skill-comply pipeline without breaking legitimate setup workflows.