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:
- Fixed-size destination buffers:
sock_strandcore_strare allocated with a fixed size (e.g., 32 bytes) on the stack. - 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/.potranslation 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);
}
Prevention & Best Practices
1. Treat sprintf() as Deprecated in Security-Sensitive Code
In modern C development, sprintf() and strcpy() should be considered legacy functions that require explicit justification to use. Most static analysis tools will flag them by default. Prefer:
| Dangerous | Safe Alternative |
|---|---|
sprintf() |
snprintf() |
strcpy() |
strncpy(), strlcpy() |
strcat() |
strncat(), strlcat() |
gets() |
fgets() |
2. Treat Locale/Translation Data as Untrusted Input
This is the less obvious lesson from this vulnerability. Translation strings sourced via gettext are external data — they come from files on disk that could be modified or replaced. Apply the same skepticism you'd apply to network input:
- Never assume a translated string fits in a fixed buffer
- Always use length-bounded formatting when incorporating translated strings
- Consider validating translation file integrity (e.g., checksums) in security-sensitive applications
3. Use sizeof(buffer) Not Magic Numbers
// Bad: magic number can drift out of sync with actual buffer size
char buf[64];
snprintf(buf, 32, ...); // ❌ Wrong limit, still dangerous
// Good: limit is always in sync with buffer declaration
char buf[64];
snprintf(buf, sizeof(buf), ...); // ✅ Always correct
4. Enable Compiler Protections
Modern compilers and linkers offer several mitigations that can limit the exploitability of buffer overflows:
# GCC/Clang: Stack canaries, ASLR-friendly PIE, FORTIFY_SOURCE
gcc -fstack-protector-strong \
-D_FORTIFY_SOURCE=2 \
-fpie -Wformat -Wformat-security \
-o myprogram myprogram.c
-fstack-protector-strong: Inserts stack canary values that detect overflows before function return-D_FORTIFY_SOURCE=2: Enables compile-time and runtime checks for unsafe string functions-Wformat-security: Warns about format string vulnerabilities
Note: These are mitigations, not fixes. The correct fix is always to eliminate the overflow in the code itself.
5. Static Analysis Integration
Several tools can catch unbounded sprintf() calls automatically:
- Clang Static Analyzer: Detects buffer overflows and unsafe API usage
- Coverity: Free for open source, excellent C/C++ analysis
- CodeQL: GitHub's semantic code analysis engine (free for open source)
- Flawfinder: Lightweight tool specifically targeting dangerous C/C++ functions
- cppcheck: Open-source static analysis for C/C++
Adding one of these to your CI/CD pipeline would have caught this vulnerability before it ever reached production.
6. Relevant Security Standards
- CWE-121: Stack-based Buffer Overflow
- CWE-134: Use of Externally-Controlled Format String
- OWASP: Buffer Overflow: Overview and prevention guidance
- SEI CERT C Coding Standard - STR07-C: Use bounds-checking interfaces for string manipulation
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:
sprintf()is dangerous whenever the output length isn't provably bounded at compile time. Usesnprintf()withsizeof(buffer).- Translation files are external data. Strings returned by
_()/gettext()can be arbitrarily long and should be treated with the same caution as user input. - Compiler warnings and static analysis catch these issues before they become CVEs. Integrate them into your build pipeline.
- 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.