Back to Blog
critical SEVERITY7 min read

Stack Buffer Overflow in nvme-print.c: How sprintf() Threatened NVMe Device Security

A critical stack-based buffer overflow vulnerability was discovered in `nvme-print.c`, where multiple `sprintf()` calls wrote formatted output into fixed-size stack buffers without any bounds checking. The vulnerability was most dangerous in `nvme_pel_event_to_string()` at line 224, where a malicious NVMe device could supply unexpected event type values to trigger a buffer overflow enabling arbitrary code execution. The fix replaces all unsafe `sprintf()` calls with `snprintf()`, enforcing stric

O
By Orbis AppSec
Published June 2, 2026Reviewed June 3, 2026

Answer Summary

This is a stack-based buffer overflow vulnerability (CWE-674) in C code where `sprintf()` writes unbounded data into fixed-size stack buffers in the NVMe printing utility. The vulnerability exists in `nvme_pel_event_to_string()` at line 224 where unexpected event type values from a malicious NVMe device could overflow the buffer. The fix replaces all unsafe `sprintf()` calls with `snprintf()`, which enforces maximum write limits and prevents the overflow from corrupting the stack and enabling code execution.

Vulnerability at a Glance

cweCWE-674 (Uncontrolled Recursion), CWE-120 (Buffer Copy without Checking Size of Input)
fixReplace sprintf() with snprintf() to enforce maximum buffer write limits
riskArbitrary code execution via stack corruption when processing malicious NVMe device events
languageC
root causesprintf() without bounds checking writing to fixed-size stack buffers
vulnerabilityStack-based buffer overflow via unsafe sprintf()

Stack Buffer Overflow in nvme-print.c: How sprintf() Threatened NVMe Device Security

Introduction

The nvme-print.c file is responsible for formatting and displaying diagnostic information from NVMe storage devices — a critical piece of infrastructure that processes data fed directly from hardware. But a subtle, dangerous flaw lurked in several of its formatting functions: every call to sprintf() wrote into a fixed-size stack buffer with zero bounds checking. This is the story of how a single missing function argument — the buffer size — created a critical, exploitable vulnerability, and how a targeted fix closed the door on potential arbitrary code execution.


The Vulnerability Explained

What Was Wrong

At the heart of the issue is a deceptively simple pattern. In C, sprintf() formats a string and writes it into a destination buffer — but it has no idea how large that buffer is. It will keep writing until it runs out of input, blissfully overwriting whatever sits adjacent in memory.

Here's the specific vulnerable code from nvme_pel_event_to_string() at line 224:

// VULNERABLE CODE — nvme-print.c:224
const char *nvme_pel_event_to_string(int type)
{
    static char str[STR_LEN];

    sprintf(str, "%s(%#x)", pel_event_to_string(type), type);

    return str;
}

The format string "%s(%#x)" combines two components:
1. The return value of pel_event_to_string(type) — a string whose length depends entirely on the type value passed in
2. The hex representation of type itself (e.g., 0xffffffff)

If pel_event_to_string(type) returns a string longer than STR_LEN minus the overhead of the hex suffix, the write overflows the stack buffer.

The same unsafe pattern appeared in nvme_degrees_string() at lines 907 and 909:

// VULNERABLE CODE — nvme-print.c:907-909
if (nvme_is_output_format_json())
    sprintf(str, "%ld %s", val, "Celsius");
else
    sprintf(str, "%ld °%s", val, "C");

Why This Is Exploitable

The threat model here is concrete: a malicious or malfunctioning NVMe device controls the input. NVMe devices communicate structured data back to the host system, including event type identifiers. An attacker with physical access, or one who has compromised firmware on a connected NVMe device, could craft device responses that return unexpected type values.

Here's what a 2-step exploit chain looks like:

  1. Step 1 — Craft the device response: The attacker programs an NVMe device (or emulates one) to return an event type value that, when passed to pel_event_to_string(), produces an abnormally long string — or causes the function to fall through to a default case that returns a very long unrecognized-type string.

  2. Step 2 — Trigger the overflow: When the host system calls nvme_pel_event_to_string() to display or log the event, sprintf() writes the oversized string into the fixed stack buffer str[STR_LEN], overflowing into adjacent stack frames. On systems without stack canaries or with bypassed mitigations, this enables control of the instruction pointer — arbitrary code execution.

This is classified as CWE-120: Buffer Copy Without Checking Size of Input, and the PR assessment confirms it as confirmed exploitable.


The Fix

Replacing sprintf() with snprintf()

The fix is surgical and precise: every sprintf() call in the affected functions was replaced with snprintf(), which takes an explicit maximum byte count as its second argument.

Before (vulnerable):

// nvme-print.c:224 — BEFORE
sprintf(str, "%s(%#x)", pel_event_to_string(type), type);

After (fixed):

// nvme-print.c:224 — AFTER
snprintf(str, sizeof(str), "%s(%#x)", pel_event_to_string(type), type);

The critical addition is sizeof(str) as the second argument. This tells snprintf() the maximum number of bytes it is permitted to write, including the null terminator. No matter how long pel_event_to_string(type) returns, the output is hard-capped at the declared buffer size.

The same fix was applied to nvme_degrees_string():

Before (vulnerable):

// nvme-print.c:907-909 — BEFORE
if (nvme_is_output_format_json())
    sprintf(str, "%ld %s", val, "Celsius");
else
    sprintf(str, "%ld °%s", val, "C");

After (fixed):

// nvme-print.c:907-909 — AFTER
if (nvme_is_output_format_json())
    snprintf(str, sizeof(str), "%ld %s", val, "Celsius");
else
    snprintf(str, sizeof(str), "%ld °%s", val, "C");

Why sizeof(str) Is the Right Choice

Using sizeof(str) rather than a hardcoded constant like 256 is intentional and important. If STR_LEN is ever changed, sizeof(str) automatically tracks the new value — a hardcoded constant would silently become wrong. This idiom ties the size argument directly to the actual buffer declaration, making future maintenance safer.

Additional Lines Flagged for Review

The PR also notes that similar patterns appear at nvme-print.c:920, 922, 937, and at least 7 additional locations in the same file. These represent the same vulnerability class and should be reviewed and fixed using the same snprintf() pattern. Defense in depth requires addressing all instances, not just the highest-severity one.


Prevention & Best Practices

1. Ban sprintf() in New Code

In any C codebase that processes external input — especially from hardware devices — sprintf() should be treated as a legacy function and replaced with snprintf() everywhere. Many projects enforce this with compiler warnings or static analysis rules:

// In a compiler warning configuration or lint rule:
// -Wno-deprecated — won't help here, but tools like:
// cppcheck --enable=warning
// will flag sprintf() as potentially unsafe

2. Use sizeof() With the Buffer Variable, Not a Magic Number

// GOOD — tracks buffer size automatically
char buf[STR_LEN];
snprintf(buf, sizeof(buf), "%s(%#x)", event_str, type);

// RISKY — silently wrong if STR_LEN changes
snprintf(buf, 64, "%s(%#x)", event_str, type);

3. Validate Input Lengths Before Formatting

When possible, check the length of strings returned by functions like pel_event_to_string() before using them in format operations:

const char *event_str = pel_event_to_string(type);
if (strlen(event_str) + 12 > STR_LEN) {
    // Handle gracefully — truncate, log, or return an error
}

4. Enable Compiler and Linker Mitigations

While not a substitute for fixing the root cause, these mitigations raise the bar for exploitation:
- Stack canaries (-fstack-protector-strong): Detect stack corruption at runtime
- FORTIFY_SOURCE (-D_FORTIFY_SOURCE=2): Replaces some sprintf() calls with checked versions at compile time
- ASLR: Makes it harder to predict memory layout for exploitation

5. Static Analysis Tools

Tools that would catch this class of vulnerability:
- Coverity — detects unsafe format string operations
- cppcheck — flags sprintf() with external input
- CodeQL — can trace tainted data from device I/O to format functions
- clang-tidy with cert-err34-c and cppcoreguidelines-* checks

6. Reference Standards


Key Takeaways

  • sprintf() in nvme_pel_event_to_string() trusted hardware-controlled input: The type parameter ultimately came from NVMe device responses — external, attacker-influenced data. Never use unbounded string functions on hardware-derived input.

  • The fix is one word: snprintf(): Adding sizeof(str) as the second argument to every sprintf() call in this file is all it takes to eliminate the overflow risk. The change is minimal, but the security improvement is fundamental.

  • Static buffers on the stack are high-value overflow targets: str[STR_LEN] declared as a static char in a function is adjacent to return addresses and saved registers. Overflowing it is a classic path to code execution.

  • One vulnerable pattern, multiple locations: The PR identified the same sprintf() issue at lines 224, 907, 909, 920, 922, 937, and more. When you find one instance of an unsafe pattern, search the entire file — it's rarely isolated.

  • sizeof(buffer_variable) is safer than hardcoded sizes: Using sizeof(str) instead of a literal number ensures the size argument stays correct even if STR_LEN is refactored in the future.


Conclusion

The vulnerability in nvme-print.c is a textbook example of why sprintf() has no place in code that processes external input. The function nvme_pel_event_to_string() at line 224 — and its siblings throughout the file — trusted that the strings it formatted would always fit in their destination buffers. A malicious NVMe device could have violated that assumption, triggering a stack buffer overflow and potentially achieving arbitrary code execution on the host system.

The fix is elegantly simple: snprintf(str, sizeof(str), ...) instead of sprintf(str, ...). That single additional argument is the difference between a function that blindly trusts its inputs and one that enforces a hard boundary. For developers working in C, especially on code that interfaces with hardware or external devices, this fix is a reminder that buffer safety is not optional — it must be explicitly enforced at every formatting call.

Automated security scanning caught this issue before it could be exploited. The regression test included in the PR ensures it stays fixed.


This vulnerability was identified and fixed by OrbisAI Security. For automated security scanning and remediation for your codebase, visit orbisappsec.com.

Frequently Asked Questions

What is a stack-based buffer overflow?

A stack buffer overflow occurs when a program writes more data to a stack-allocated buffer than it can hold, corrupting the stack and potentially allowing attackers to execute arbitrary code by overwriting return addresses or other critical data.

How do you prevent buffer overflow in C?

Use bounded string functions like `snprintf()`, `strncpy()`, or `strlcpy()` instead of unbounded functions like `sprintf()`, `strcpy()`, or `gets()`. Always validate input size and check function return values to ensure no truncation occurred.

What CWE is this buffer overflow?

This vulnerability is primarily CWE-120 (Buffer Copy without Checking Size of Input) and CWE-674 (Uncontrolled Recursion), with elements of CWE-119 (Improper Restriction of Operations within the Bounds of a Memory Buffer).

Is input validation enough to prevent this buffer overflow?

No. While input validation helps, the real protection comes from using bounded functions like `snprintf()`. Even with validation, unexpected values from hardware can bypass checks—bounded functions provide a safety net regardless of input.

Can static analysis detect this buffer overflow?

Yes. Modern static analysis tools like Clang's Static Analyzer, Coverity, and Semgrep can detect unsafe `sprintf()` calls and flag them as potential buffer overflow risks, especially when writing to fixed-size buffers.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #3407

Related Articles

high

How heap buffer overflow happens in C JMA archive extraction and how to fix it

A heap buffer overflow vulnerability in `jma/jma.cpp` allowed a crafted JMA ROM archive to trigger out-of-bounds memory writes during file extraction. The flaw existed at line 446, where `memcpy` was called with `first_chunk_offset` and `copy_amount` values derived directly from archive header metadata without any validation that those values stayed within the bounds of either the source or destination buffer. The fix adds a pre-copy bounds check that rejects malformed archives before the danger

critical

How unsafe buffer copying happens in C credential storage and how to fix it

A critical vulnerability in `lib/server.c` allowed attackers to trigger out-of-bounds memory reads when copying credentials via unsafe `memcpy()` calls. By replacing `memcpy()` with bounds-safe `strlcpy()`, the fix ensures credentials are safely stored without buffer overruns or null-termination issues.

critical

How buffer overflow happens in C Bluetooth device handling and how to fix it

A critical buffer overflow vulnerability in `src/wiiuse.c` allowed attackers within Bluetooth range to trigger heap corruption by sending specially crafted HID packets with oversized length values. The fix adds strict bounds checking to validate that data lengths don't exceed buffer capacity before performing memory operations, preventing exploitation by malicious or intercepted Bluetooth devices.

critical

How buffer overflow happens in C patches.c sprintf macros and how to fix it

A critical buffer overflow vulnerability was discovered in `src/patches.c` where the `_EPRINT_I`, `_EPRINT_F`, and `_EPRINT_COEF` macros used `sprintf()` to write formatted AMY event data into a fixed-size buffer without any bounds checking. By replacing every `sprintf()` call with `snprintf()` and tracking remaining buffer space using a `s_entry` base pointer, the fix ensures that formatting 22 event fields — even at maximum values — can never write beyond the buffer boundary.

critical

How buffer overflow happens in C dcraw_lz.c nikon_3700() and how to fix it

A critical buffer overflow vulnerability was discovered in `lightcrafts/coprocesses/dcraw/dcraw_lz.c` at line 1334, where the `nikon_3700()` function used `strcpy()` to copy camera make and model strings into fixed 64-byte buffers without any bounds checking. A crafted RAW image file with oversized make/model metadata could trigger a heap or stack corruption, potentially enabling arbitrary code execution. The fix replaces both `strcpy()` calls with `strncpy()` and explicit null-termination, enfo

critical

How buffer overflow in modxo_queue.c memcpy happens in C embedded systems and how to fix it

A critical buffer overflow vulnerability was discovered in `modxo/modxo_queue.c`, where two `memcpy` operations in the `modxo_queue_insert` and `modxo_queue_remove` functions used `queue->item_size` as the copy length without validating it against the destination buffer's bounds. If `item_size` was corrupted or maliciously set to an oversized value, both the enqueue (line 49) and dequeue (line 61) operations could overflow adjacent heap or stack memory on the embedded target. The fix adds bounds