Buffer Over-Read in sslsniff.c find_library_path(): When strlen() Goes Out of Bounds
Introduction
The bpf/sslsniff.c file is responsible for intercepting SSL/TLS traffic at the eBPF layer — a security-sensitive component that runs with elevated privileges on Linux systems. Inside its find_library_path() function, a single unsafe string operation quietly created the conditions for a high-severity memory safety bug. At line 515, a memmove() call was sized using an unbounded strlen() on a pointer derived from searching within a fixed stack buffer. The result: if the right conditions were met, the program could read memory beyond the buffer boundary — and potentially write it somewhere else.
This is the kind of bug that doesn't announce itself. No crash in normal operation, no obvious test failure. But under adversarial input, it becomes a reliable memory safety violation in a privileged, production eBPF tool.
The Vulnerability Explained
What the Code Was Doing
Inside find_library_path(), the code processes output lines from ldconfig, which typically look like:
libssl.so.1.1 -> /usr/lib/x86_64-linux-gnu/libssl.so.1.1
The function searches for the > character using strrchr(), checks that the next character is a space, and then uses memmove() to shift the path portion (after >) to the beginning of the path buffer:
// VULNERABLE CODE (before fix)
char *start = strrchr(path, '>');
if (start && *(start + 1) == ' ') {
memmove(path, start + 2, strlen(start + 2) + 1);
// ...
}
The Specific Problem: strlen(start + 2)
Here's where things go wrong. The path variable is a fixed-size stack buffer (declared with a compile-time size). The pointer start is computed by strrchr() — it points somewhere inside path. Then start + 2 advances two bytes further.
The critical issue: strlen(start + 2) has no knowledge of where the buffer ends. It will scan forward byte-by-byte looking for a null terminator, regardless of whether it has already passed the end of the allocated buffer. If start + 2 points near the tail of the buffer and the buffer is not properly null-terminated at that position, strlen() will read past the buffer boundary into adjacent stack memory.
Even worse, the result of that strlen() is then passed directly to memmove() as the byte count:
memmove(path, start + 2, strlen(start + 2) + 1);
// ^^^^^^^^^^^^^^^^^^^^
// Could be larger than remaining buffer space
If strlen() returns a value larger than the space between start + 2 and the end of path, the memmove() will copy bytes from beyond the buffer into path — a classic out-of-bounds read feeding into a write.
Attack Scenario
The PR notes that "an attacker who can influence /proc filesystem entries" could exploit this. More concretely:
find_library_path()parses output that may be influenced by the filesystem or environment (e.g., a craftedldconfigcache or symlink output).- An attacker crafts a library path entry where the
>character appears very close to the end of thepathbuffer, with no null terminator within the buffer afterstart + 2. strlen(start + 2)walks off the end of the stack buffer, reading adjacent stack frames.memmove()copies that out-of-bounds data intopath, potentially exposing stack contents or corrupting adjacent variables.
In the context of an eBPF-based SSL sniffer running as root or with CAP_BPF, memory corruption or information leakage from the kernel-adjacent stack is a serious concern.
CWE Reference
This vulnerability maps to:
- CWE-125: Out-of-bounds Read
- CWE-787: Out-of-bounds Write (via the subsequent memmove)
- CWE-120: Buffer Copy without Checking Size of Input
The Fix
Before and After
The fix is surgical: replace the unbounded strlen() with a bounds-aware strnlen() that is explicitly limited to the number of bytes remaining in the buffer between start + 2 and the end of path.
Before (vulnerable):
memmove(path, start + 2, strlen(start + 2) + 1);
After (fixed):
size_t avail = (size_t)(path + sizeof(path) - (start + 2));
memmove(path, start + 2, strnlen(start + 2, avail) + 1);
Why This Works
Let's break down each piece of the fix:
1. Computing avail:
size_t avail = (size_t)(path + sizeof(path) - (start + 2));
This calculates exactly how many bytes remain in the path buffer starting from start + 2. path + sizeof(path) is a pointer to one-past-the-end of the buffer — a well-defined sentinel in C. Subtracting start + 2 gives the maximum number of bytes we are allowed to examine.
2. Using strnlen() instead of strlen():
strnlen(start + 2, avail)
strnlen() will scan for a null terminator just like strlen(), but it stops after avail bytes even if no null terminator is found. This means it can never read past the end of the buffer. The returned length is guaranteed to be ≤ avail.
3. The memmove() is now safe:
Because strnlen() returns at most avail, and avail was computed as the remaining space in path, the memmove() cannot copy more bytes than the buffer can hold from the source position. The buffer boundary is respected.
The Regression Test
The PR also introduces tests/test_invariant_sslsniff.c, which validates four security invariants across 21 adversarial payloads:
- The function must never read beyond the input buffer boundary
- The function must never write beyond the output buffer boundary
- The resulting string must be null-terminated within bounds
- The resulting string length must be ≤ the original buffer size
The test uses canary values (0xAB and 0xCD byte patterns) placed adjacent to the buffer to detect any overflow, and validates boundary conditions at every offset position near the buffer edge. This is exactly the kind of property-based boundary testing that catches off-by-one errors that unit tests often miss.
Prevention & Best Practices
1. Treat Every Computed Pointer as Potentially Near the Buffer Edge
Whenever you compute a pointer by searching within a buffer (via strrchr, strstr, strchr, etc.) and then advance it by an offset, ask: what is the maximum distance from this pointer to the end of the buffer? Always compute avail before any subsequent string operation.
// Pattern to follow:
char *found = strrchr(buf, '>');
if (found) {
char *src = found + 2;
size_t avail = (size_t)(buf + sizeof(buf) - src);
size_t len = strnlen(src, avail);
// Now len is safe to use
}
2. Prefer strnlen Over strlen on Internal Buffer Pointers
strlen() is only safe when you have a guarantee that a null terminator exists within the buffer. For pointers computed at runtime within a buffer, that guarantee rarely holds without explicit enforcement. Use strnlen() with a computed maximum.
3. Use Compiler and Runtime Protections
Enable the following for C/C++ code in security-sensitive components:
- -fstack-protector-strong — detects stack buffer overflows at runtime
- -D_FORTIFY_SOURCE=2 — enables compile-time and runtime bounds checking for common string functions
- AddressSanitizer (-fsanitize=address) during testing — would have caught this exact bug
- Static analysis tools like cppcheck, clang-tidy, or Coverity with buffer overflow rules enabled
4. For eBPF and Privileged C Code, Apply Extra Scrutiny
Code that runs with elevated privileges or processes externally-influenced data (like filesystem paths, /proc entries, or network data) should be treated as a security boundary. Every string operation that touches external data deserves a manual review for bounds safety.
5. OWASP and Standards References
- OWASP: Buffer Overflow
- CERT C: STR31-C — Guarantee that storage for strings has sufficient space
- CWE-125: Out-of-bounds Read
- CWE-787: Out-of-bounds Write
Key Takeaways
-
strlen()on a sub-pointer is dangerous: Usingstrlen(start + 2)wherestartis derived fromstrrchr()within a fixed buffer is a classic pattern for out-of-bounds reads. Always usestrnlen()with an explicitly computed limit. -
The
availcalculation is the critical guard:size_t avail = (size_t)(path + sizeof(path) - (start + 2))is the correct idiom for computing remaining buffer space from a derived pointer — memorize it. -
memmove()is only as safe as its size argument: The function itself is bounds-safe, but it will faithfully copy whatever count you give it. A corrupted count from an unboundedstrlen()turns a safe function into an unsafe one. -
eBPF and BPF tools deserve memory-safety audits: Tools like
sslsniff.cparse data from the OS environment with elevated privileges. A memory safety bug here has higher impact than in a typical userspace utility. -
Regression tests with canary values catch what unit tests miss: The new
test_invariant_sslsniff.cuses0xAB/0xCDcanaries and boundary-edge payloads — this approach should be standard for any C function that processes variable-length string data.
Conclusion
A single strlen() call in find_library_path() was the difference between safe and unsafe memory access in a privileged eBPF tool. The fix — replacing strlen(start + 2) with strnlen(start + 2, avail) after computing the available buffer space — is minimal in size but significant in impact. It closes an out-of-bounds read/write path that could have been triggered by crafted filesystem or ldconfig output.
The broader lesson is one of defensive discipline: in C, every pointer arithmetic operation that feeds into a string or memory function must be accompanied by an explicit bounds calculation. When working in security-sensitive, privileged code like an SSL traffic interceptor, that discipline isn't optional — it's the baseline. The new regression test suite ensures this specific class of boundary violation cannot quietly re-enter the codebase.