Feb 25, 2026·7 min read·5 visits
NetBSD mapped the `ld.so` library immediately below the stack. Attackers can use large allocations to 'jump' over the stack guard page, land in `ld.so` memory, and overwrite function pointers to gain code execution.
In the golden age of memory corruption, 2017 was the year the 'Stack Clash' returned with a vengeance. CVE-2017-1000375 is a specific, devastating instance of this class affecting NetBSD. By mapping the dynamic linker (`ld.so`) directly adjacent to the stack region, the OS unwittingly created a playground for attackers. Despite protections like ASLR and stack guard pages, a clever manipulation of stack allocation allows an attacker to 'jump' the safety rails and overwrite critical linker structures, leading to total system compromise.
In the world of exploit development, we often obsess over bad code—missing null checks, integer overflows, or use-after-free bugs. But sometimes, the vulnerability isn't in the code logic itself; it's in the geometry of the process memory. CVE-2017-1000375 is a masterclass in why memory layout matters just as much as code quality.
NetBSD, like a well-meaning architect, designed its process memory space with efficiency in mind. However, it made a fatal error in urban planning: it placed the user-space stack right next to the dynamic linker (ld.so). In a standard process, the stack grows downward (from high addresses to low addresses). If it grows too much, it hits a 'guard page'—a demilitarized zone meant to kill the process if breached.
But here's the kicker: NetBSD placed ld.so directly below that guard page. If an attacker could figure out how to step over the guard page rather than walking into it, they wouldn't just crash the app—they would land silently in the memory space of the most powerful utility in the process: the runtime linker. It’s like locking your front door (the guard page) but leaving the window to the master bedroom (ld.so) wide open.
To understand this flaw, you have to understand the 'Stack Guard Page'. It is essentially a tripwire. The kernel maps a page of memory at the bottom of the stack with no read/write permissions. If the stack grows incrementally (pushing values one by one), the Stack Pointer (SP) eventually hits this page, triggers a segmentation fault, and the kernel kills the process. Safe, right?
Not quite. The flaw in NetBSD (and other systems vulnerable to Stack Clash) is the assumption that the stack always grows continuously. It doesn't. Functions like alloca() or variable-length arrays allow a program to subtract a massive value from the Stack Pointer in a single instruction.
> [!NOTE]
> The Mechanics of the Jump
> If the stack pointer is at address 0x1000 and the guard page is at 0x0000 to 0x0FFF, a subtraction of 0x2000 moves the pointer to 0xFFFFF000. The CPU never touches the guard page addresses. It simply teleports the pointer past them.
In NetBSD's case, because ld.so was mapped immediately below the stack, this 'teleportation' lands the Stack Pointer squarely inside the writable memory segments of the dynamic linker. The kernel doesn't notice because the destination address is valid, writable memory. The tripwire is intact, but the intruder is already inside.
The vulnerability isn't a single line of C code you can point to and laugh at; it's a systemic failure in the memory allocator's policy. However, we can look at the Proof-of-Concept (PoC) provided by Qualys to see exactly how this geometry is weaponized. The code is deceptively simple.
/* NetBSD_CVE-2017-1000375.c - trimmed for clarity */
static void smash_no_jump(const size_t smash_size) {
char buf[1024];
memset(buf, 'A', sizeof(buf));
// Recursion to grow the stack close to the edge
if (smash_size > sizeof(buf))
smash_no_jump(smash_size - sizeof(buf));
}
int main(const int argc, const char * const argv[]) {
// ... setup code ...
const size_t smash_size = strtoul(argv[1], NULL, 0);
smash_no_jump(smash_size);
}This PoC demonstrates the 'Clash' aspect. By recursively calling smash_no_jump, the attacker forces the stack to grow until it is right up against the ld.so mapping. In a real weaponized exploit, the attacker wouldn't just fill the stack; they would use a specific allocation pattern:
ld.so structures.The fix implemented in NetBSD 7.1.1 didn't change the alloca logic; instead, it changed the map layout. The kernel was patched to enforce a much larger gap (essentially a 'Death Valley') between the stack and any other mapping, making the jump impossible without hitting unmapped memory.
Let's break down the attack chain. Qualys dubbed this the "Clash, Run, Jump, Smash" method. It sounds like a dance move, but it ends with a root shell.
The attacker identifies a target SUID binary or a remote service. They need to control the stack depth. They might do this by passing deep recursive arguments or large environment variables. The goal is to bring the Stack Pointer (RSP) as close to the guard page as possible without touching it.
This is the critical moment. The attacker triggers a code path that performs a large stack allocation that is larger than the guard page size (typically 4KB at the time).
Now that RSP points into ld.so, the application continues. It eventually decides to write data to this 'buffer'. This write operation corrupts the linker's internal state. A classic target is the Global Offset Table (GOT) or function pointers used by the linker during symbol resolution.
When the application later calls exit() or resolves a dynamic symbol, ld.so wakes up, reads the corrupted pointer, and unwittingly jumps to the attacker's shellcode (which was likely sprayed on the stack earlier). Game over.
Why is this CVSS 9.8? Usually, stack clashes are treated as Local Privilege Escalation (LPE) bugs. You run it on a shell you already have to get root. However, the vector here is listed as Network (AV:N). This implies that if you can trigger this allocation behavior in a network daemon (like an HTTP server or mail handler running on NetBSD), you can achieve Remote Code Execution (RCE).
If the vulnerability is triggered in a critical system process or a setuid binary, the attacker instantly gains root privileges. The proximity of ld.so is particularly damning because ld.so is loaded into every dynamically linked executable. This isn't a vulnerability in one program; it's a vulnerability in the environment that hosts all programs.
Successful exploitation allows:
Fixing Stack Clash required a multi-layered approach. You can't just tell developers "don't allocate large buffers," because they won't listen.
Kernel-Level Layout Randomization: NetBSD 7.1.1 introduced a much larger guard region. Instead of a single 4KB page, the kernel now reserves 1MB (or more) of unmapped space below the stack. Jumping 1MB with a single alloca is significantly harder and rarer than jumping 4KB.
Stack Probing (-fstack-check): This is the compiler-side fix. When this flag is enabled, the compiler inserts code for every allocation larger than a page size. This code 'probes' (touches) every page between the old SP and the new SP.
// Conceptual logic of -fstack-check
sub rsp, 4096
test [rsp], rax // Touch the page
sub rsp, 4096
test [rsp], rax // Touch the next pageThis ensures that you cannot step over the guard page. You are forced to walk through it, triggering the trap and crashing the program safely before you can do any damage.
CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
NetBSD NetBSD | <= 7.1 | 7.1.1 |
| Attribute | Detail |
|---|---|
| Attack Vector | Network / Local |
| CVSS v3.0 | 9.8 (Critical) |
| EPSS Score | 38.41% |
| Exploit Status | PoC Available |
| Weakness | Stack Clash |
| Target Component | ld.so / Kernel Memory Manager |