NetBSD Stack Clash: When the Floor Becomes the Ceiling
Jan 7, 2026·6 min read
Executive Summary (TL;DR)
NetBSD mapped the dynamic linker directly below the stack. Attackers could allocate large buffers to 'jump' over the stack guard page and write directly into the linker's memory. This allows for reliable Local Privilege Escalation (LPE), effectively bypassing ASLR and stack protections.
A fundamental memory management flaw in NetBSD allowed the stack to collide with the dynamic linker (ld.so), bypassing guard pages and enabling arbitrary code execution. Part of the broader 'Stack Clash' research by Qualys.
The Hook: A Game of Memory Tetris
In the world of operating system design, memory layout is everything. Ideally, you want your stack (where local variables and function calls live) and your heap (dynamic memory) to stay far, far away from each other. They are the volatile teenagers of memory segments—let them touch, and you get chaos.
Enter CVE-2017-1000375, a star player in the 'Stack Clash' saga of 2017. While Linux and OpenBSD were busy patching their own variations of this nightmare, NetBSD was found holding a particularly smoking gun. The operating system had decided, in its infinite wisdom, to map ld.so—the runtime link-editor responsible for loading shared libraries—directly below the stack.
This is the architectural equivalent of building a munitions depot in the basement of a fireworks factory. ld.so is a high-value target. It contains function pointers, relocation tables, and logic that runs with the privileges of the executing process. If you can write to it, you own the process. And thanks to this vulnerability, writing to it was as easy as asking the stack to grow a little too fast.
The Flaw: The Guard Dog That Slept
To understand why this is a vulnerability, we have to talk about Guard Pages. In a sane operating system, the kernel places a small region of unmapped memory (usually 4KB, or one page) at the bottom of the stack. This is the electric fence. If the stack grows naturally and hits this fence, the CPU throws a page fault, the kernel sees it, and either expands the stack legally or kills the process for misbehaving.
However, the 'Stack Clash' attack relies on a simple physics problem: Quantum Tunneling. Okay, not really, but close. If a program allocates a stack buffer that is larger than the guard page (say, 64KB), the Stack Pointer (SP) subtracts that amount instantly. The SP 'hops' over the guard page entirely, landing in whatever memory lies beyond.
In NetBSD's case, the land beyond the fence wasn't an empty void; it was the ld.so data segment. Because ld.so is a valid, writable memory region, the CPU sees the write instructions, checks the permissions, says 'Looks good to me!', and allows the program to corrupt the linker's internal state. No page fault. No crash. Just silent, deadly corruption.
The Layout: Visualizing the Crash
Let's visualize the memory layout to see exactly how egregious this setup was. In a standard exploitation scenario, we are fighting against Address Space Layout Randomization (ASLR), which shuffles these regions around. But here, the relative distance between the stack and the linker was often deterministic or easily manipulatable.
The red arrow represents the exploit. By using a large alloca() or a recursive function with large stack frames, the attacker subtracts enough from the Stack Pointer to bypass the Guard Page entirely. The write operations that follow execute directly inside ld.so.
The Exploit: Smashing the Linker
Qualys provided a beautiful, minimalist Proof of Concept (PoC) that demonstrates the mechanics without the weaponized payload. The core logic exploits the lack of stack probing. If the compiler doesn't generate code to touch every page as the stack grows (using -fstack-check), the CPU never validates the intermediate pages.
Here is the logic derived from the PoC:
static void smash_no_jump(const size_t smash_size) {
char buf[1024];
// 1. Fill the buffer to ensure we are actually using the memory
memset(buf, 'A', sizeof(buf));
// 2. Recursively call to grow the stack closer to the target
if (smash_size > sizeof(buf))
smash_no_jump(smash_size - sizeof(buf));
}A real-world weaponized exploit follows this chain:
- Preparation: The attacker forks a process or uses
execveto set up a clean memory layout where the distance between the stack andld.sois known. - The Approach: Use standard recursion (like
qsortor the loop above) to bring the Stack Pointer right to the edge of the Guard Page. - The Leap: Call a function that allocates a massive buffer (e.g., 64KB). The instruction
sub rsp, 0x10000executes. The Stack Pointer is now pointing insideld.so. - The Overwrite: The function continues to execute, writing data to its local variables (which are now mapped over
ld.sostructures). The attacker overwrites a function pointer in the Global Offset Table (GOT) or a destructor entry. - Execution: When the program calls
exit()or resolves a dynamic symbol, the linker wakes up, reads the corrupted pointer, and jumps to the attacker's shellcode.
The Impact: Why This Matters
You might look at the CVSS score of 9.8 and think 'Remote Code Execution?' Not exactly. NVD lists it as AV:N, but this is primarily a Local Privilege Escalation (LPE) vector. However, the criticality is justified by the reliability and the ubiquity of the stack.
This vulnerability allowed any local user with shell access to become root effectively 100% of the time. Unlike complex heap feng-shui exploits that rely on precise race conditions or heap grooming, Stack Clash is deterministic. If you can do math, you can own the box.
Furthermore, this highlighted a massive failure in OS design principles. We spent years building ASLR, NX (No-Execute), and Canaries, only to leave the back door open because we assumed the stack would never jump over a 4KB fence. It was a wake-up call that mitigation assumes normal behavior, and hackers rarely behave normally.
The Fix: Building Higher Fences
The remediation for Stack Clash was a multi-layered approach, acknowledging that a single fix wasn't enough.
- Bigger Guard Pages: NetBSD and other kernels increased the default guard page size (often to 1MB or dynamic sizing) to make it much harder for a single allocation to jump over it.
- Stack Probing: The real fix comes from the compiler. Recompiling userland with
-fstack-check(on GCC) forces the program to touch every 4KB page when allocating large buffers. This ensures you literally cannot skip the guard page; you will hit it and crash before you reach the juicy data. - Layout Randomization: Kernels became more aggressive about placing random gaps between the stack and shared libraries. If you don't know where
ld.sois relative to the stack, you're jumping blind.
For developers today: valid stack growth is not guaranteed. If you are writing low-level code or OS kernels, never assume adjacent memory is safe just because you put a PROT_NONE page in between.
Official Patches
Technical Appendix
CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:HAffected Systems
Affected Versions Detail
| Product | Affected Versions | Fixed Version |
|---|---|---|
NetBSD NetBSD | <= 7.1 | Subject to patch availability (Post-2017 updates) |
| Attribute | Detail |
|---|---|
| Attack Vector | Local (Stack manipulation) |
| CVSS v3.0 | 9.8 (Critical) |
| Bug Class | Stack Clash / Memory Corruption |
| Target Component | ld.so (Dynamic Linker) |
| Exploit Reliability | High (Deterministic memory layout) |
| EPSS Score | 38.41% |
MITRE ATT&CK Mapping
Out-of-bounds Write
Known Exploits & Detection
Vulnerability Timeline
Subscribe to updates
Get the latest CVE analysis reports delivered to your inbox.