June 19, 2017·6 min read·engineeringcsecuritymemorylibc

Stack Clash exposes the gap between stack movement and memory access

Qualys shows how stack growth can cross a guard page without touching it. The disclosure connects compiler-generated stack allocation with kernel mapping rules and libc behavior.

A guard page detects an access to its addresses. Moving a stack pointer past those addresses is a different operation, and it can leave the guard untouched. The June 19 Stack Clash advisory from Qualys demonstrates the security consequences of that distinction across several operating systems. Once a process uses a neighboring writable mapping as stack storage, writes through either interpretation can corrupt data belonging to the other.

This is a problem in the agreement between generated code and virtual memory management. A function can reserve stack space with a large change to the stack pointer, while the kernel learns about stack growth through faults caused by memory accesses. If the reservation crosses the protected region and the next access lands in another valid mapping, the event that was supposed to enforce separation never happens.

I regard this as a particularly instructive C systems problem. Source-level storage declarations depend on compiler and runtime mechanisms that are easy to leave implicit. The advisory shows why knowing that a platform has stack guards does not establish how every stack allocation interacts with them.

Stack expansion needs an observable access

Qualys describes a downward-growing stack on i386. When execution accesses an unmapped page below the existing stack, the page fault can cause the kernel to expand the stack mapping. If expansion cannot proceed, for example because of a stack limit, the process receives a segmentation fault. The fault supplies information that an arithmetic update to a register does not provide.

This distinction matters when another mapping lies nearby. Without an effective protected interval, the process can move its stack pointer into that mapping and access valid memory there. The kernel sees an access to an existing writable mapping. It has no reason to reinterpret that access as a request to extend the stack into a region already used for something else.

A guard page introduces an interval that faults when accessed. In the intended sequence, stack growth reaches it before reaching another region. The weakness is that allocation need not access every byte or page of the reserved stack interval. A large reservation can place the next used address beyond the guard, so the protected interval is skipped.

The advisory identifies an insufficiently large Linux guard as CVE-2017-1000364 and also describes implementation defects in BSD guard handling. Those are related but different failures. One concerns crossing an existing protected gap, while another can eliminate the intended gap through a mapping or growth rule. Their common consequence does not make the underlying fixes interchangeable.

Reserving a buffer is different from filling it

The disclosure explains why a large stack buffer that is only partly used can matter more than its visible writes suggest. Reserving its full extent changes where subsequent stack operations occur. A short write can then use an address in the neighboring region without touching the guard between that address and the original stack.

Writing the entire reserved interval would change the sequence of memory accesses. It would encounter the guard, producing the fault that the sparse use avoided. That does not make fully writing every temporary buffer a general repair. It explains which part of the mechanism depends on the distance between reserved storage and storage actually accessed.

The source of a reservation need not be a directly visible application array. Qualys identifies large frames, alloca and variable-length arrays among the relevant mechanisms. It also examines libc paths that reserve temporary stack buffers, so a call to a familiar library function can participate in the address movement.

The practical unit of analysis is therefore the executed call path, including library code. Looking only for a large array in the application's main function would miss temporary allocation performed below it. Conversely, finding a large frame does not by itself establish an exploit. The advisory's examples require a suitable relationship between mappings, subsequent calls and writable data.

libc contributes several distinct conditions

Qualys describes an LD_AUDIT memory leak in glibc's dynamic loader as one method of obtaining mappings that approach the stack. It also describes the loader's stack allocation for an expanded LD_LIBRARY_PATH. These play different roles. One affects the placement and persistence of memory, while another can provide the stack movement across the guard.

The advisory groups several glibc issues under CVE-2017-1000366. It also discusses locale processing through __dcigettext, where a temporary allocation depends on the LANGUAGE environment variable. The report explains that particular contents can leave much of that temporary storage unwritten. A variable used for a routine localization task therefore affects both allocation size and the pattern of accesses.

Formatted output and name resolution appear in the analysis too. Qualys describes temporary stack buffers in vfprintf and getaddrinfo as possible ways to cross a guard. It explicitly says that its exploits do not use those methods. Preserving that qualification matters, since an identified ingredient is weaker evidence than a completed exploit against a particular service.

The report's demonstrated local privilege escalations include dynamic-loader paths and privileged programs. Its remote discussion includes obstacles and failed attempts. The disclosure establishes a broad memory-management problem, but it does not justify describing every application using libc as remotely exploitable. Reachability and the required memory layout remain specific to the affected path.

Probing makes reserved growth visible

Qualys recommends increasing the guard region as a short-term measure. A larger interval increases the distance a single reservation must cross, but the advisory points out that a sufficiently large stack buffer can still cross it. Size changes affect the required conditions without changing the distinction between pointer movement and access.

The compiler-oriented recommendation is to access successive pages while extending the stack. Those probes ensure that a large reservation cannot silently omit all accesses to the protected interval. The advisory discusses GCC's -fstack-check in that context and recommends rebuilding user-space code, including the loader and libraries, rather than applying an option only to the final application.

The GCC 7.1 instrumentation manual is useful for understanding what that option promises. It says the generated code makes stack extension visible to the operating system or language runtime. The option does not independently provide the entire checking mechanism. The runtime still has to enforce an appropriate boundary when those accesses occur.

GCC distinguishes generic checking from target-specific checking. Its documentation lists costs and restrictions of the generic mechanism, including a changed allocation strategy for large objects and a fixed limit on static frame size. If target-specific support is unavailable, the generic mechanism is the fallback. A build flag therefore needs to be understood in the context of the compiler target, not just copied as a universal binary property.

Stack limits also need interpretation in this setting. Qualys discusses argument and environment storage as a way to consume stack space, including the distinction between the strings and the arrays of pointers referring to them. Its Linux analysis identifies separate treatment of those quantities. A limit that accounts for one representation does not automatically account for the other. That matters because initial process construction can help establish the layout before the application executes its own allocation logic, extending the relevant review beyond explicit calls to alloca.

Canaries address a different event

The same GCC manual describes stack-protector options as instrumentation against stack smashing. Qualys's mechanism begins earlier, with where a stack frame is placed and whether it overlaps another mapping. Detecting corruption of a protected frame and ensuring that stack growth encounters a guard are separate operations.

This distinction explains why the presence of one defense does not prove the other. A canary does not cause each newly reserved page to be accessed. A guard does not identify every overwrite within already mapped stack storage. The generated accesses and the runtime mapping policy need to cooperate for the guard to enforce the separation that Stack Clash attacks exploit.

I find the disclosure useful because it makes that cooperation concrete. The kernel can act on a fault only if code actually touches the protected interval. Compiler probing can provide those touches, while the kernel must keep the interval protected as mappings and the stack change. The advisory's examples show failures in both parts of that arrangement and identify libc paths that can supply the necessary allocations. Fixing an individual path removes a demonstrated route, while enforcing the stack-growth boundary addresses the underlying opportunity for one mapping to be used as another.