How Buffer Overflow Happens in C String Operations with strcpy/strncpy and How to Fix It
The Incident
In the pomoc.c file—a local CLI tool that communicates with a daemon via Unix sockets—a critical buffer overflow vulnerability was discovered at line 25. The code used strncpy() to copy a socket path into a fixed-size buffer without ensuring proper null-termination. This created a pathway for attackers to exploit the tool through oversized input arguments, potentially leading to code execution or denial of service.
// VULNERABLE CODE (line 25)
strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path) - 1);
The vulnerability was flagged as HIGH severity by Semgrep's static analysis, marked as "likely exploitable," and required immediate remediation in production code.
The Vulnerability Explained
Understanding the Problem
The vulnerability centers on how C handles string copying. Unlike higher-level languages where strings are length-prefixed or automatically managed, C relies on null-terminated character arrays. This design creates two critical failure modes with strcpy() and strncpy():
strcpy()has NO size checking — it blindly copies until it finds a null byte, regardless of buffer sizestrncpy()doesn't guarantee null-termination — if the source string is longer than the specified size, the destination buffer is left without a null terminator
The Specific Vulnerability in pomoc.c
In the vulnerable code:
struct sockaddr_un addr;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path) - 1);
The addr.sun_path field in struct sockaddr_un is typically 108 bytes. The code attempts to be safe by using strncpy() with sizeof(addr.sun_path) - 1 as the size limit. However, strncpy() will NOT automatically null-terminate the string if it reaches the size limit. This means:
- If
SOCKET_PATHis exactly 108 characters or longer,strncpy()copies 107 bytes and leavesaddr.sun_path[107]as garbage - The subsequent socket operations may read past the intended string boundary, causing undefined behavior
- An attacker controlling
SOCKET_PATH(via environment variables, configuration files, or other input vectors) could craft an oversized string to corrupt adjacent memory
Attack Scenario
Consider this exploit scenario:
- Attacker sets an environment variable or modifies a config file to inject a malicious socket path
- The attacker crafts a path longer than 107 characters, e.g.:
/tmp/socket_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA strncpy()copies 107 bytes but doesn't null-terminate- When the code later uses
addr.sun_path(e.g., inconnect()calls), the kernel or subsequent code reads beyond the intended boundary - This could leak sensitive data from adjacent memory, crash the program, or potentially execute arbitrary code if the attacker controls the overflowed data
Real-World Impact
For this CLI tool:
- Denial of Service: Crafted input crashes the program, breaking automation or user workflows
- Information Disclosure: Buffer overflow can leak sensitive data from the stack or heap
- Local Privilege Escalation: If the tool runs with elevated privileges, an attacker could exploit this to gain code execution
The Fix
What Changed
The fix replaces the dangerous strncpy() call with snprintf(), which provides automatic bounds checking and guaranteed null-termination:
- strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path) - 1);
+ snprintf(addr.sun_path, sizeof(addr.sun_path), "%s", SOCKET_PATH);
Why This Works
snprintf() is fundamentally safer for this use case:
| Aspect | strncpy() |
snprintf() |
|---|---|---|
| Bounds Checking | Manual (error-prone) | Automatic |
| Null Termination | NOT guaranteed | Guaranteed |
| Truncation Handling | Silent, leaves buffer dirty | Silent, but buffer is valid |
| Return Value | Number of bytes copied | Number of bytes that would be written |
With snprintf(addr.sun_path, sizeof(addr.sun_path), "%s", SOCKET_PATH):
- The second argument (sizeof(addr.sun_path)) is the total buffer size, not size minus one
- snprintf() automatically writes a null terminator within that size
- If SOCKET_PATH exceeds 107 bytes, it's truncated safely to 107 bytes + null terminator
- The buffer is always valid and properly null-terminated
Verification
The PR included a regression test to ensure the fix holds:
START_TEST(test_buffer_reads_never_exceed_declared_length)
{
const char *payloads[] = {
"normal", // Valid input
"A", // Boundary: single char
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" // 100 chars
};
for (int i = 0; i < num_payloads; i++) {
char dest[16] = {0};
const char *src = payloads[i];
snprintf(dest, sizeof(dest), "%s", src); // Now using snprintf
// Verify no buffer overflow occurred
ck_assert_msg(strlen(dest) < sizeof(dest),
"Buffer overflow detected for payload: %s", src);
ck_assert_msg(dest[sizeof(dest) - 1] == '\0',
"Buffer not properly terminated for payload: %s", src);
}
}
END_TEST
This test confirms that even with 100-character payloads (far exceeding the buffer), the buffer remains properly bounded and null-terminated.
Prevention & Best Practices
1. Never Use strcpy() in Production Code
strcpy() has NO size checking whatsoever. It should be considered deprecated:
// ❌ NEVER DO THIS
strcpy(dest, src); // No size limit - guaranteed overflow risk
2. Use snprintf() or strlcpy() Instead of strncpy()
Both are safer alternatives:
// ✅ GOOD - snprintf (portable, POSIX)
snprintf(dest, sizeof(dest), "%s", src);
// ✅ GOOD - strlcpy (BSD extension, widely available)
strlcpy(dest, src, sizeof(dest));
Preference: snprintf() is more portable across platforms (POSIX standard), while strlcpy() is cleaner but may not be available on all systems.
3. Always Pass the Full Buffer Size
When using snprintf(), pass sizeof(dest), not sizeof(dest) - 1:
// ✅ CORRECT - snprintf handles the null terminator
snprintf(dest, sizeof(dest), "%s", src);
// ❌ WRONG - unnecessary and confusing
snprintf(dest, sizeof(dest) - 1, "%s", src);
4. Enable Compiler Warnings
Use GCC/Clang flags to catch dangerous functions:
gcc -Wall -Wextra -Wformat -Wformat-security -Wstrict-overflow src/pomoc.c
Many compilers will warn about strcpy() and unsafe string operations.
5. Use Static Analysis Tools
Semgrep, Clang Static Analyzer, and Coverity automatically detect these patterns:
semgrep --config=p/security-audit src/pomoc.c
6. Validate Input Length Before Copying
For defense-in-depth, validate the source string length:
if (strlen(SOCKET_PATH) >= sizeof(addr.sun_path)) {
fprintf(stderr, "Socket path too long\n");
return -1;
}
snprintf(addr.sun_path, sizeof(addr.sun_path), "%s", SOCKET_PATH);
References to Standards
- CWE-120: Buffer Copy without Checking Size of Input
- CWE-119: Improper Restriction of Operations within the Bounds of a Memory Buffer
- OWASP: Buffer Overflow
Key Takeaways
strncpy()is a footgun: Even when limiting the size, it doesn't guarantee null-termination. Never use it without manually null-terminating the buffer.snprintf()is the safer choice for socket paths and other fixed-size buffers: It enforces both bounds checking and null-termination in a single call.- This vulnerability affected production code handling user input: The
pomoc.cCLI tool could be exploited through environment variables or configuration files—a realistic attack vector. - Static analysis caught what code review might miss: Semgrep automatically flagged this pattern before it caused a security incident.
- Truncation is better than overflow: When input is too long,
snprintf()silently truncates it safely, whereasstrncpy()leaves the buffer in an undefined state.
How Orbis AppSec Detected This
Source: Socket path constant (SOCKET_PATH) and potential environment-controlled input passed to the socket connection function.
Sink: strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path) - 1) at line 25 in src/pomoc.c—a dangerous string copy into a fixed-size buffer.
Missing Control: The code lacked guaranteed null-termination after the strncpy() call. While the size was limited, the absence of an explicit null-byte assignment left the buffer in an undefined state.
CWE: CWE-120 (Buffer Copy without Checking Size of Input)
Fix: Replaced strncpy() with snprintf(), which automatically enforces bounds checking and null-termination in a single, safer function call.
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 in C string operations remain one of the most dangerous vulnerability classes, yet they're entirely preventable with the right tools and practices. The fix in pomoc.c demonstrates a simple but powerful principle: use language features and library functions that enforce safety constraints automatically.
By replacing strncpy() with snprintf(), the development team eliminated an entire class of potential exploits in a single line change. This is a reminder that security isn't about complex solutions—it's about choosing the right primitives from the start.
For developers working with C, especially on system utilities, CLI tools, and daemon code that handles user input: audit your string operations today. Replace strcpy() with snprintf() or strlcpy(), enable compiler warnings, and integrate static analysis into your CI/CD pipeline. The pomoc.c fix is a template for how to do it right.
References
- CWE-120: Buffer Copy without Checking Size of Input
- CWE-119: Improper Restriction of Operations within the Bounds of a Memory Buffer
- OWASP: Buffer Overflow
- OWASP: C Cheat Sheet - String Functions
- Semgrep Rule: insecure-use-string-copy-fn
- C11 Standard: snprintf() documentation
- BSD strlcpy() manual
- fix: use bounded strlcpy/snprintf in pomoc.c...