The Stack Clash advisory by Qualys provided detailed insight as to what happens when the heap and the stack meet. The stack grows down and the heap grows up. The stack grows on an as-needed basis. When the stack pointer is decremented beyond an existing page boundary, a page fault happens and the kernel will allocate more space for the stack (assuming the application hasn't hit the stack limit.) If there is an existing memory mapping with PROT_READ and PROT_WRITE set right below the stack, then no page fault occurs and the application will use the mapping as if it were for the stack. Ideally, this should never happen. In order to prevent this from happening, most operating systems (FreeBSD included) implement a "stack guard," which is a guard of one or more pages reserved below the stack, preventing other mappings. A properly implemented stack guard will effectively prevent the heap or other memory mappings from reaching the stack.
FreeBSD provides a stack guard implementation, but has it disabled by default. As discussed in the Qualys report, when enabled, FreeBSD's stack guard implementation had a logic flaw that prevented it from being effective. A proof-of-concept exploit written by HardenedBSD's own Shawn Webb demonstrated Qualys' claims. HardenedBSD had the stack guard enabled by default.
To mitigate Stack Clash, we in HardenedBSD performed the following in 12-CURRENT over the week of 19 Jun 2017 to 24 Jun 2017:
- Fixed the flaw in the stack guard implementation that prevented it from being effective.
- Increased the size of the stack guard from one 4KB page to 2MB.
- Prevented mappings from occurring between the bottom-most limit of the stack and the top of the stack.
- (Soon) Modified the per-thread stack guard in libthr to be of random size, minimum 1MB, maximum 5MB.
- This also randomizes the top-most address of each per-thread stack.
The commits for these changes have been backported to HardenedBSD 11-STABLE. Item #1 has been backported to HardenedBSD 10-STABLE.
On 24 Jun 2017, FreeBSD committed their Stack Clash mitigation. It introduces the concept of MAP_GUARD, which is a special PROT_NONE mapping. It's placed immediately below the bottom-most limit of the stack. It's a really innovative implementation that allows general use of guard pages. Indeed, in a follow-up commit, the RTLD now uses MAP_GUARD for guard pages between shared objects. FreeBSD's stack guard is still a single 4KB page in size, even with Qualys' recommendation to use a minimum of 1MB. On 25 Jun 2017, FreeBSD followed up with a commit to fix a regression that effectively disabled the stack guard in certain edge cases with the new implementation. Overall, FreeBSD's solution to the Stack Clash problem is innovative and even useful outside the context of Stack Clash.
We in HardenedBSD now use a hybrid of both approaches. We've hardened the security.bsd.stack_guard_page
sysctl node to a 2MB stack guard. We've made that sysctl node a read-only tunable, configurable only at boot-time. The changes to libthr still stand and the per-thread stack guard size is a random size between 1MB and 5MB. We may look to integrate MAP_GUARD with libthr instead of its reliance on mprotect(PROT_NONE)
.
Update 25 Jun 2017: The randomization of the per-thread stack guard has been found to be too aggressive. We are investigating this feature and will revisit it soon.