Back to Blog
critical SEVERITY8 min read

Stack Buffer Overflow via Unbounded sprintf() in HardInfo2 CPU Utility

A critical stack buffer overflow vulnerability was discovered and patched in HardInfo2's cpu_util.c, where six unbounded sprintf() calls wrote locale-translated CPU topology labels into fixed-size stack buffers without length constraints. An attacker supplying a crafted translation file could overflow the stack buffer, overwrite saved return addresses, and potentially achieve arbitrary code execution. The fix replaces these dangerous calls with length-bounded alternatives, eliminating the overfl

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published May 13, 2026•Reviewed June 3, 2026

Answer Summary

This is a critical stack buffer overflow vulnerability (CWE-121) in HardInfo2's C codebase, specifically in `cpu_util.c`. Six unbounded `sprintf()` calls wrote locale-translated CPU topology labels into fixed-size stack buffers, allowing an attacker with control over a translation file to overflow the stack, overwrite saved return addresses, and potentially execute arbitrary code. The fix replaces every vulnerable `sprintf()` call with `snprintf()`, passing the destination buffer's size as a bound to prevent any write beyond the allocated stack space.

Vulnerability at a Glance

cweCWE-121
fixReplace all six sprintf() calls with length-bounded snprintf() calls
riskArbitrary code execution via crafted translation file
languageC
root causeSix sprintf() calls writing unbounded locale-translated strings into fixed-size stack buffers
vulnerabilityStack Buffer Overflow via Unbounded sprintf()

Critical Stack Buffer Overflow Fixed in HardInfo2: When Translation Files Become Weapons

Introduction

Most developers are familiar with the classic buffer overflow — it's one of the oldest vulnerabilities in the book. Yet despite decades of awareness, unbounded string formatting functions continue to appear in production C code, quietly waiting for the right conditions to become exploitable. This week, a critical severity stack buffer overflow was patched in HardInfo2's cpu_util.c, serving as a timely reminder that even utility code handling "harmless" display labels can harbor serious security flaws.

This vulnerability is particularly interesting because the attack vector isn't network input or user-supplied data in the traditional sense — it's a crafted locale translation file. If you've never considered your gettext .po files as a security boundary, this post is for you.


The Vulnerability Explained

What Is a Stack Buffer Overflow?

Before diving in, a quick refresher: a stack buffer overflow occurs when a program writes more data into a fixed-size buffer allocated on the stack than the buffer can hold. The excess data spills over into adjacent stack memory, potentially overwriting critical control data like saved return addresses or frame pointers. When the function returns, instead of jumping back to legitimate code, the CPU can be redirected to attacker-controlled instructions.

This class of vulnerability is tracked under CWE-121: Stack-based Buffer Overflow and has been the root cause of countless real-world exploits.

The Vulnerable Code

In hardinfo2/cpu_util.c, around lines 266–278, there were six calls to sprintf() that looked something like this:

char sock_str[32];
char core_str[32];

// Dangerous: no length limit specified
sprintf(sock_str, "%s %d", _("Socket"), socket_id);
sprintf(sock_str, "%s %d", _("Core"),   core_id);
sprintf(core_str, "%s %d", _("Book"),   book_id);
sprintf(core_str, "%s %d", _("Drawer"), drawer_id);
// ... and two more similar calls

Two things make this dangerous:

  1. Fixed-size destination buffers: sock_str and core_str are allocated with a fixed size (e.g., 32 bytes) on the stack.
  2. The _() macro: This is the standard gettext internationalization macro. At runtime, _("Socket") doesn't return the literal string "Socket" — it returns whatever translation is loaded from the active locale file. The length of that string is determined entirely at runtime.

Why the _() Macro Is the Critical Factor

Here's where it gets subtle. If these calls used hardcoded string literals, the maximum output length would be predictable and a developer might (correctly) size the buffer accordingly. But because _() is involved, the actual string length is:

  • Determined at runtime by the loaded .mo/.po translation file
  • Potentially unbounded — nothing in the gettext API limits translation string length
  • Controllable by an attacker who can supply a crafted translation file

Consider this: if a malicious or corrupted translation file maps "Socket" to a 200-character string, the sprintf() call will happily write all 200+ characters plus the formatted integer into a 32-byte buffer, smashing the stack.

The Attack Scenario

Normal execution:
  _("Socket") → "Socket" (6 bytes)
  sprintf result: "Socket 3" (8 bytes) ✅ fits in 32-byte buffer

Malicious translation file:
  _("Socket") → "AAAAAAAAAA...AAA" (200 bytes)
  sprintf result: 200+ bytes written into 32-byte buffer ❌ OVERFLOW
  Overwrites: local variables, saved frame pointer, saved return address
  Effect: Arbitrary code execution when function returns

Real-world impact includes:
- Privilege escalation if HardInfo2 runs with elevated privileges (e.g., to access /proc or hardware sensors)
- Code execution on systems where an attacker can influence locale settings or translation files
- Denial of service via reliable application crash

This is especially relevant in multi-user environments, containerized deployments with shared locale directories, or any scenario where locale files are sourced from untrusted locations.


The Fix

What Changed

The fix replaces all six unbounded sprintf() calls with snprintf(), the length-aware counterpart that guarantees it will never write more than N bytes to the destination buffer (including the null terminator).

Before (vulnerable):

char sock_str[64];
char core_str[64];

// No length bound — will overflow if _() returns a long string
sprintf(sock_str, "%s %d", _("Socket"), socket_id);
sprintf(core_str, "%s %d", _("Core"),   core_id);
sprintf(sock_str, "%s %d", _("Book"),   book_id);
sprintf(core_str, "%s %d", _("Drawer"), drawer_id);

After (safe):

char sock_str[64];
char core_str[64];

// Length-bounded — will never overflow the destination buffer
snprintf(sock_str, sizeof(sock_str), "%s %d", _("Socket"), socket_id);
snprintf(core_str, sizeof(core_str), "%s %d", _("Core"),   core_id);
snprintf(sock_str, sizeof(sock_str), "%s %d", _("Book"),   book_id);
snprintf(core_str, sizeof(core_str), "%s %d", _("Drawer"), drawer_id);

Why snprintf() Solves the Problem

snprintf(buf, size, fmt, ...) takes an explicit size argument. No matter how long the formatted output would be, it writes at most size - 1 characters and always null-terminates the buffer. The function also returns the number of characters that would have been written, allowing callers to detect truncation if needed.

Key properties of the fix:
- No stack overflow possible: The write is hard-capped at sizeof(buf)
- Using sizeof(buf) instead of a magic number: This ties the limit directly to the actual buffer size, so if the buffer is ever resized, the limit automatically adjusts
- Consistent pattern: All six calls are fixed with the same idiom, eliminating the vulnerability class entirely from this code section

A Note on Truncation

One might ask: "But what if the translation string is legitimately long and gets truncated?" This is a valid concern for functionality, but it's the correct security trade-off. A truncated label in a UI is a minor display issue. A stack buffer overflow is a critical security vulnerability. When in doubt, truncate safely rather than overflow dangerously.

If full translation strings must be supported, the proper solution is dynamic allocation:

// Even safer: dynamically sized buffer
int needed = snprintf(NULL, 0, "%s %d", _("Socket"), socket_id) + 1;
char *sock_str = malloc(needed);
if (sock_str) {
    snprintf(sock_str, needed, "%s %d", _("Socket"), socket_id);
    // ... use sock_str ...
    free(sock_str);
}

Conclusion

This vulnerability is a textbook example of how context transforms risk. A sprintf() call writing a short, hardcoded label string seems harmless. The same call writing a runtime-translated string is a critical security vulnerability. The difference is entirely in understanding where your data comes from and whether its length is bounded.

The key takeaways from this patch:

  1. sprintf() is dangerous whenever the output length isn't provably bounded at compile time. Use snprintf() with sizeof(buffer).
  2. Translation files are external data. Strings returned by _() / gettext() can be arbitrarily long and should be treated with the same caution as user input.
  3. Compiler warnings and static analysis catch these issues before they become CVEs. Integrate them into your build pipeline.
  4. Defense in depth matters: Stack canaries and FORTIFY_SOURCE don't replace correct code, but they raise the bar for exploitation.

The fix here is small — swapping sprintf for snprintf and adding a size argument — but the security impact is significant. Six potential stack smashing vectors, eliminated. This is the essence of secure coding: small, disciplined habits that compound into robust, trustworthy software.

Stay safe, and always bound your buffers. 🔒


This vulnerability was identified and patched by OrbisAI Security. If you maintain C/C++ projects, consider running a security scan to catch similar issues before they reach production.

Prevention and further reading

Related Articles

critical

Aardvark.Cef.Process.Core Shared Memory Handler: Unvalidated memcpy

The shared memory handler in Aardvark.Cef.Process.Core failed to validate that the `length` parameter passed from JavaScript did not exceed the destination buffer size before calling `memcpy`. This allowed a compromised Chromium renderer process to corrupt heap memory by supplying an oversized `handle->length` value to the `openMapping` function. The fix adds explicit bounds checking using `GetArrayBufferByteLength()` before the copy operation.

high

XShmGetImage Heap Corruption: Unvalidated Image Height in streamproxy

The `XShmGetImage` function in the X11 shared memory image path copies pixel data row-by-row using `memcpy` without validating that the source image height matches the destination buffer height. An attacker or compromised server could provide an oversized source image, causing writes beyond the allocated heap buffer and triggering heap corruption or code execution.

critical

SGX Enclave ecall_store_data memcpy Buffer Overflow in 256-Byte

The Intel SGX enclave's trusted bridge functions `ecall_store_data` and `ecall_retrieve_data` used `memcpy()` to move data into and out of a fixed 256-byte `secure_storage` buffer without validating that `data_len` fit within destination boundaries. An attacker providing oversized `data_len` values could corrupt enclave memory, breaking SGX's confidentiality guarantees.

critical

How Buffer Overflow via strcpy() Happens in C++ XML Parsers and How to Fix It

A critical buffer overflow vulnerability was discovered in `buildroot-external/package/libxmlparser/xmlParser.cpp`, where the `toXMLString` function used `_tcscpy()` to write XML escape sequences into a destination buffer without any bounds checking. An attacker supplying a crafted XML document could overflow the buffer and potentially execute arbitrary code. The fix replaces all five unsafe `_tcscpy()` calls with `memcpy()` calls that copy only the exact number of bytes required for each escape

critical

How Heap Buffer Overflows Happen in C++ ZIP Extraction and How to Fix Them

A critical heap buffer overflow vulnerability was discovered in `TKLiveSync/unzip.cpp`, where ZIP archive entry names were copied into a `PATH_MAX`-sized heap buffer using `strcpy()` without any length validation. Since the ZIP specification allows entry names up to 65,535 bytes — far exceeding typical `PATH_MAX` values of 1,024 to 4,096 bytes — a crafted archive could overflow the buffer and corrupt heap memory. The fix replaces the unsafe `strcpy`/`dirname` pattern with `std::string` operation

medium

How Integer Overflow happens in C++ image processing and how to fix it

A signed integer overflow in OpenCV's `bilateralFilter.cpp` allowed the buffer size calculation `cal_width * cal_height * cn` to wrap around to a small or negative value, causing `padding.resize()` to allocate far less memory than needed. Subsequent `memcpy` operations would then write beyond the allocated buffer, creating a heap corruption primitive. The fix is a single targeted cast to `size_t` that promotes the multiplication to unsigned 64-bit arithmetic before any overflow can occur.