How Buffer Overflow in handle_interlink_event() Happens in C Terminal Event Handling and How to Fix It
Summary
A critical buffer overflow vulnerability was found in src/terminal/event.c at line 250, inside the handle_interlink_event() function. A memcpy() call copied the current working directory string info->cwd into the fixed-size term->cwd buffer using MAX_CWD_LEN as the byte count — without ever checking whether the source string was actually that long or shorter. An attacker who could feed a crafted terminal event with an oversized cwd field could corrupt adjacent heap memory. The fix is a one-line change: replace memcpy() with safe_strncpy().
Introduction
The src/terminal/event.c file is responsible for processing interlink events that carry terminal state — things like the terminal name, environment variables, and the current working directory. Inside handle_interlink_event(), the code takes an incoming struct interlink_event, extracts its info payload, and copies fields into the live struct terminal object.
One of those field copies looked like this at line 250:
memcpy(term->cwd, info->cwd, MAX_CWD_LEN);
term->cwd[MAX_CWD_LEN - 1] = 0;
At first glance this looks defensive — it copies exactly MAX_CWD_LEN bytes and then null-terminates. But there is a subtle and dangerous assumption buried here: that info->cwd actually contains MAX_CWD_LEN or fewer bytes of meaningful data. If an attacker supplies a cwd string that is longer than MAX_CWD_LEN, memcpy() will happily read past the intended boundary of the source and write past the end of the destination buffer, corrupting whatever lives next to term->cwd on the heap.
The Vulnerability Explained
What memcpy() Does (and Doesn't Do)
memcpy(dest, src, n) copies exactly n bytes from src to dest. It does not care about null terminators. It does not check whether src is actually n bytes long. It does not check whether dest has room for n bytes beyond what was allocated.
The code allocates term->cwd with a buffer of size MAX_CWD_LEN. The intent is clearly to limit the copy to that size. But the third argument to memcpy() controls how many bytes are read from the source — not how many bytes are available in the destination. If info->cwd points to a string of 512 bytes and MAX_CWD_LEN is 256, memcpy() will write 256 bytes into term->cwd — filling it exactly — which sounds fine. But it will also have read 256 bytes from a source that may only have a 256-byte allocation itself, and any attacker-controlled data in adjacent source memory gets pulled in.
More critically: if info->cwd is an attacker-controlled buffer that is larger than MAX_CWD_LEN and the destination term->cwd was allocated at exactly MAX_CWD_LEN, then writing MAX_CWD_LEN bytes into it is safe for the destination — but the null-termination line term->cwd[MAX_CWD_LEN - 1] = 0 only protects the last byte of the destination. It does nothing to prevent the copy from reading attacker-controlled garbage from the source side.
The real danger is the inverse scenario: info->cwd is shorter than MAX_CWD_LEN but memcpy() copies MAX_CWD_LEN bytes anyway, reading garbage or attacker-crafted bytes from memory beyond the actual string. This can leak sensitive data or, if the attacker controls that adjacent memory, inject unexpected content into term->cwd.
The Vulnerable Code
// src/terminal/event.c, line 250 (before fix)
memcpy(term->cwd, info->cwd, MAX_CWD_LEN);
term->cwd[MAX_CWD_LEN - 1] = 0;
The problem: memcpy() copies a fixed number of bytes (MAX_CWD_LEN) regardless of the actual length of info->cwd. No validation of strlen(info->cwd) occurs before this call.
Exploitation Scenario
This is a local CLI tool, meaning exploitation requires the attacker to control command-line arguments or input files that ultimately populate terminal event data. In that threat model, a concrete attack looks like this:
- The attacker crafts an input file or command-line argument that causes
info->cwdto point to a string significantly longer thanMAX_CWD_LEN. handle_interlink_event()is called with this crafted event.memcpy(term->cwd, info->cwd, MAX_CWD_LEN)copiesMAX_CWD_LENbytes from the oversized source intoterm->cwd.- Depending on heap layout, the bytes immediately following
term->cwdon the heap — which could be another struct field, a function pointer, or heap metadata — are overwritten with attacker-controlled data. - This heap corruption can lead to a crash (denial of service) or, with heap grooming, arbitrary code execution.
The PR also flags src/terminal/event.c:531 as using the same pattern, meaning the attack surface exists in at least two places.
The Fix
What Changed
The fix is a single-line substitution in handle_interlink_event():
// BEFORE (vulnerable)
memcpy(term->cwd, info->cwd, MAX_CWD_LEN);
term->cwd[MAX_CWD_LEN - 1] = 0;
// AFTER (fixed)
safe_strncpy(term->cwd, info->cwd, MAX_CWD_LEN);
Two lines became one. The explicit null-termination line is gone because safe_strncpy() handles that internally.
Why This Fix Works
safe_strncpy() is a bounds-aware string copy function that:
- Copies at most
MAX_CWD_LEN - 1characters frominfo->cwdintoterm->cwd. - Always null-terminates
term->cwd[MAX_CWD_LEN - 1], regardless of source length. - Stops copying when it encounters a null terminator in the source — unlike
memcpy(), which is oblivious to string semantics.
This means that even if info->cwd is 10,000 characters long, safe_strncpy() will copy at most MAX_CWD_LEN - 1 characters and then write a null byte. The destination buffer is never overrun, and the result is always a properly terminated C string.
The Regression Test
A new test file test/test_invariant_event.c was added alongside the fix. It exercises three cases:
- A payload longer than
MAX_CWD_LEN(the exploit case) - A payload exactly
MAX_CWD_LEN - 1characters (the boundary case) - A normal short path like
/home/user(the valid input case)
For each case, the test fills the memory after term->cwd with a sentinel value (0xBB) and verifies it is unchanged after the copy. This is a canonical heap overflow detection technique and will catch any future regression that reintroduces an unbounded copy.
Prevention & Best Practices
Never Use memcpy() for String Copies Without Length Validation
memcpy() is for copying binary blobs of known, validated size. When copying C strings from user-influenced sources, always use a function that respects null terminators:
| Function | Safe? | Notes |
|---|---|---|
strcpy() |
❌ | No bounds checking at all |
strncpy() |
⚠️ | Does not guarantee null termination |
strlcpy() |
✅ | BSD; always null-terminates, returns required length |
safe_strncpy() |
✅ | Project-specific wrapper; always null-terminates |
snprintf(dest, n, "%s", src) |
✅ | Portable; always null-terminates |
Validate Input Length Before Any Copy
If you must use memcpy() for a string-like operation, validate first:
size_t src_len = strnlen(info->cwd, MAX_CWD_LEN);
memcpy(term->cwd, info->cwd, src_len);
term->cwd[src_len] = '\0';
This ensures you only copy as many bytes as actually exist in the source string.
Review All memcpy() Call Sites in Event Handlers
The PR explicitly notes that src/terminal/event.c:531 uses the same pattern. Any code that processes externally-influenced event data and uses memcpy() for string fields should be audited. A targeted Semgrep rule can find these patterns automatically:
rules:
- id: memcpy-string-no-length-check
pattern: memcpy($DEST, $SRC, $LEN);
message: Verify $SRC length does not exceed $LEN before memcpy
languages: [c]
severity: WARNING
Relevant Standards
- CWE-122: Heap-based Buffer Overflow
- CWE-120: Buffer Copy without Checking Size of Input ('Classic Buffer Overflow')
- OWASP: Input Validation — always validate the length and format of externally-supplied data before using it in memory operations
- SEI CERT C Coding Standard: STR31-C — Guarantee that storage for strings has sufficient space for character data and the null terminator
Key Takeaways
memcpy()with a fixed length is not a bounds check — it tellsmemcpy()how many bytes to copy, not how many bytes the source contains. Inhandle_interlink_event(), passingMAX_CWD_LENas the size was intended as a limit but acted as an unconditional copy length.- String fields in event structs are high-risk copy targets —
info->cwdcomes from external event data in a terminal interlink protocol, making it an attacker-reachable source. Anymemcpy()of such fields without a precedingstrlen()check is a potential overflow. safe_strncpy()eliminates the dual-write anti-pattern — the original code usedmemcpy()followed by a manual null-termination write. This two-step pattern is fragile;safe_strncpy()handles both atomically and correctly.- The same pattern exists at line 531 — fixing one instance is not enough. A full audit of
memcpy()calls on string fields throughoutevent.cis warranted. - Sentinel-based regression tests are the right tool here — filling memory after the destination buffer with a known value and asserting it is unchanged after the copy is a reliable, low-overhead way to catch buffer overflow regressions in C code.
How Orbis AppSec Detected This
- Source: The
info->cwdfield within thestruct interlink_eventpayload, populated from externally-supplied terminal event data - Sink:
memcpy(term->cwd, info->cwd, MAX_CWD_LEN)atsrc/terminal/event.c:250insidehandle_interlink_event() - Missing control: No validation of
strlen(info->cwd)orstrnlen(info->cwd, MAX_CWD_LEN)before the copy; the fixed copy lengthMAX_CWD_LENwas mistakenly treated as a safety bound rather than a copy count - CWE: CWE-122 — Heap-based Buffer Overflow
- Fix: Replaced
memcpy(term->cwd, info->cwd, MAX_CWD_LEN)and the manual null-termination with a single call tosafe_strncpy(term->cwd, info->cwd, MAX_CWD_LEN), which enforces the destination buffer limit and guarantees null termination
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
The buffer overflow in handle_interlink_event() is a textbook example of how a well-intentioned but subtly incorrect use of memcpy() creates a critical security vulnerability in C. The developer clearly intended to limit the copy to MAX_CWD_LEN bytes — the constant is right there in the call. But memcpy() does not interpret its third argument as a safety limit on the destination; it interprets it as an exact byte count to copy from the source. When the source is attacker-influenced event data, that distinction is the difference between safe code and a heap corruption primitive.
The fix is minimal and correct: one function call replaces two lines, and safe_strncpy() provides the string-aware, null-termination-guaranteed copy semantics that the original code was trying to achieve. Combined with the new regression test in test/test_invariant_event.c, this fix is both immediately effective and durably protected against future regressions.
If you work with C code that processes event-driven or protocol-driven data, audit every memcpy() call that touches string fields. The pattern of copying a "known maximum" number of bytes from an externally-influenced source is one of the most common and consequential mistakes in systems programming.