Heap Buffer Overflow in ShadowsocksR: How a Missing Bounds Check Could Let Attackers Crash Your Server
Introduction
Memory safety bugs remain some of the most dangerous and exploitable vulnerabilities in systems programming. Among them, heap buffer overflows occupy a particularly notorious position — they can be leveraged by remote attackers to crash services, corrupt memory, or in the worst case, achieve arbitrary code execution on a target system.
This post examines a critical heap buffer overflow discovered in shadowsocksr-libev, a widely used proxy tool for network traffic obfuscation. The vulnerability existed in src/server/server.c and could be triggered by any remote attacker capable of sending a crafted network packet — no authentication required.
Whether you're a C developer, a security engineer, or someone who runs ShadowsocksR in production, understanding this vulnerability and its fix will help you write safer, more resilient network code.
The Vulnerability Explained
What Is a Heap Buffer Overflow?
A heap buffer overflow occurs when a program writes more data into a heap-allocated memory region than that region can hold. Unlike stack overflows, heap overflows can be subtle and harder to detect — but they're equally dangerous. Depending on heap layout and allocator behavior, an overflow can corrupt adjacent heap metadata, overwrite function pointers, or enable sophisticated exploitation techniques like heap spraying.
Where Was the Bug?
The vulnerability had two distinct manifestations in server_recv_cb, the callback function that processes incoming network data.
Problem 1: Unchecked malloc Return Value
// VULNERABLE CODE (before fix)
char *back_buf = (char*)malloc(sizeof(buffer_t));
memcpy(back_buf, buf, sizeof(buffer_t));
When malloc fails (returns NULL), dereferencing the null pointer in the subsequent memcpy causes undefined behavior — typically a segmentation fault and crash. In a network server, this means a single malformed packet could bring down the entire process.
Problem 2: Unchecked brealloc Return Value Before memcpy
// VULNERABLE CODE (before fix)
size_t header_len = server->header_buf->len;
brealloc(server->header_buf, server->buf->len + header_len, BUF_SIZE);
memcpy(server->header_buf->array + header_len,
server->buf->array, server->buf->len);
server->header_buf->len = server->buf->len + header_len;
Here's where things get serious. The brealloc function resizes a buffer, but its return value is never checked. If reallocation fails:
server->header_bufmay point to freed or invalid memory- The subsequent
memcpywrites attacker-controlled data (server->buf->lenbytes) into that invalid region - Since
server->buf->lenis derived directly from the attacker's packet, an attacker can supply an arbitrarily large length value
The result: a classic attacker-controlled heap overflow.
How Could It Be Exploited?
An attacker targeting this vulnerability would:
- Connect to the ShadowsocksR server — no credentials needed, just a TCP connection
- Craft a malicious packet with an oversized
lenfield in the buffer structure - Send the packet during the handshake stage (
STAGE_HANDSHAKE) - Trigger the overflow when the server attempts to copy the oversized data into the under-allocated buffer
Potential impact:
- 🔴 Remote Denial of Service — crash the server process reliably
- 🔴 Heap Corruption — corrupt adjacent allocations, leading to unpredictable behavior
- 🔴 Remote Code Execution (RCE) — with sufficient heap manipulation, potentially hijack control flow
The CVSS score for this class of vulnerability in a network-facing daemon is typically 9.0+, firmly in critical territory.
The Bonus Bug: strncpy vs snprintf
The diff also reveals a subtler fix:
// BEFORE
strncpy(svaddr.sun_path, manager_address, sizeof(svaddr.sun_path) - 1);
// AFTER
snprintf(svaddr.sun_path, sizeof(svaddr.sun_path), "%s", manager_address);
While strncpy with size - 1 is a common pattern, it has a well-known pitfall: it does not guarantee null-termination if the source string exactly fills the buffer. snprintf always null-terminates within the specified size, making it the safer choice for this kind of bounded string copy.
The Fix
The patch introduces three targeted changes that eliminate the vulnerability without altering program logic.
Fix 1: Check malloc Return Value
// FIXED CODE
char *back_buf = (char*)malloc(sizeof(buffer_t));
if (back_buf == NULL) {
close_and_free_remote(EV_A_ remote);
close_and_free_server(EV_A_ server);
return;
}
memcpy(back_buf, buf, sizeof(buffer_t));
What changed: A null check is added immediately after malloc. If allocation fails, the connection is cleanly closed and the function returns — no crash, no undefined behavior.
Why it works: memcpy is never reached with a null destination pointer. The server gracefully handles memory pressure instead of crashing.
Fix 2: Check brealloc Return Value
// FIXED CODE
if (brealloc(server->header_buf, server->buf->len + header_len, BUF_SIZE) != 0) {
close_and_free_remote(EV_A_ remote);
close_and_free_server(EV_A_ server);
return;
}
memcpy(server->header_buf->array + header_len,
server->buf->array, server->buf->len);
server->header_buf->len = server->buf->len + header_len;
What changed: The return value of brealloc is now checked. A non-zero return indicates failure, and the code bails out cleanly before the memcpy.
Why it works: The memcpy only executes when we have a valid, correctly-sized destination buffer. An attacker can no longer trigger an overflow by sending a packet with an oversized length field — if the reallocation would fail or produce an insufficient buffer, the operation is aborted.
Fix 3: Safer String Copy
// FIXED CODE
snprintf(svaddr.sun_path, sizeof(svaddr.sun_path), "%s", manager_address);
What changed: strncpy replaced with snprintf, which guarantees null-termination.
Before vs. After: Side-by-Side
| Aspect | Before | After |
|---|---|---|
malloc failure |
Null pointer dereference | Graceful connection close |
brealloc failure |
Heap overflow via attacker-controlled len |
Graceful connection close |
| String copy | Potentially non-null-terminated | Always null-terminated |
| Attack surface | Remote, unauthenticated | Eliminated |
Prevention & Best Practices
This vulnerability is a textbook example of C memory management pitfalls. Here's how to avoid similar issues in your own code:
1. Always Check Allocation Return Values
// ✅ CORRECT
void *ptr = malloc(size);
if (ptr == NULL) {
// handle error
return ERROR_OOM;
}
// ❌ WRONG
void *ptr = malloc(size);
memcpy(ptr, src, size); // UB if ptr is NULL
This applies to malloc, calloc, realloc, and any custom allocator wrappers.
2. Validate Lengths Before Copying
Never trust length values derived from network input:
// ✅ CORRECT
if (attacker_supplied_len > destination_buffer_size) {
// reject or truncate
return ERROR_INVALID_LENGTH;
}
memcpy(dest, src, attacker_supplied_len);
// ❌ WRONG
memcpy(dest, src, attacker_supplied_len); // overflow if len > dest size
3. Prefer Safer Alternatives
| Unsafe | Safer Alternative |
|---|---|
strcpy |
snprintf, strlcpy |
strncpy |
snprintf |
sprintf |
snprintf |
gets |
fgets |
memcpy (unchecked) |
memcpy with bounds check |
4. Use Modern Analysis Tools
Several tools can catch these bugs automatically:
- AddressSanitizer (ASan) — detects heap overflows at runtime:
gcc -fsanitize=address - Valgrind — memory error detection:
valgrind --tool=memcheck ./server - Coverity / CodeQL — static analysis for null dereferences and buffer overflows
- libFuzzer / AFL++ — fuzz network inputs to discover overflow conditions
- clang-tidy — static linting with security-focused checks
5. Apply Defense in Depth
- ASLR + PIE: Randomize memory layout to make exploitation harder
- Stack canaries: Detect stack corruption (
-fstack-protector-strong) - RELRO: Protect GOT/PLT from overwrites (
-Wl,-z,relro,-z,now) - Seccomp: Restrict syscalls available to the server process
6. Relevant Security Standards
- CWE-122: Heap-based Buffer Overflow
- CWE-476: NULL Pointer Dereference
- CWE-131: Incorrect Calculation of Buffer Size
- OWASP: Memory Management Cheat Sheet
- SEI CERT C: Rule MEM35-C (allocate sufficient memory), Rule MEM32-C (detect allocation errors)
Conclusion
This vulnerability in ShadowsocksR-libev is a reminder that network-facing C code demands extreme care around memory operations. Two missing null checks and one unchecked reallocation were all it took to expose a critical attack surface to any remote client.
The fix is elegant in its simplicity: check return values, handle failures gracefully, and never perform a memory copy without verifying the destination is valid and large enough. These are fundamentals of safe C programming — but under deadline pressure or in complex codebases, they're easy to miss.
Key takeaways:
- ✅ Always check the return value of memory allocation functions
- ✅ Never use attacker-controlled length values in
memcpywithout validation - ✅ Prefer
snprintfoverstrncpyfor bounded string copies - ✅ Use ASan and fuzzing in your CI pipeline for network-facing code
- ✅ Treat all network input as potentially malicious — because it is
Security is not a feature you add at the end. It's a discipline you apply at every line.
This vulnerability was identified and fixed by automated security scanning. If you maintain C network code, consider integrating similar tooling into your development workflow.