ASLR and Security Hardening

On this page

Runtime hardening causes certain errors to fail earlier or reduces the chance that errors are further exploited. It does not prove that a program is free from out-of-bounds access, use-after-free, or data races. Understanding what each mechanism protects, the toolchain support it requires, and what it does not cover is more useful than collecting a set of "all enabled" flags.

Different mechanisms limit different capabilities and cannot replace each other ASLR + PIE Reduces predictability of address locations NX / W^X Restricts data pages from being executed as code RELRO + NOW Protects relevant link-time data after relocation Stack protector Detects some stack corruption Shadow stack Validates control flow of return addresses CFI / IBT Constrains specific indirect control transfers

ASLR and PIE: Relocation Allowance Enables Randomization Space

ASLR randomizes parts of the address layout based on kernel policies. For Linux randomize_va_space, 0 means disabling randomization, 1 randomizes mmap, stack, vDSO, etc., and 2 further includes heap-related randomization. Default values and actual ranges depend on kernel configuration and architecture; not all objects are independently randomized with each allocation. Kernel randomize_va_space

PIE allows the main executable image to be positioned by load offset, and when combined with ASLR, its load address varies; non-PIE main images typically use a link-time fixed layout, but their dynamic libraries, stack, etc., can still be randomized. Therefore, "if not PIE, the entire process has no ASLR" is also inaccurate.

Whether the compiler generates PIE by default is determined by the distribution build and toolchain settings. When explicit control is needed, use -fPIE for compilation and -pie for final linking; whether to use -fPIC when building static libraries is another requirement. You cannot simply add a link switch to a single source file while ignoring object compatibility across the entire project.

ASLR limits predictability but does not prevent addresses from leaking through other errors. fork often inherits the existing layout, while a separate exec re-establishes the image; you should not generalize fixed addresses or random ranges from a specific example to all platforms.

RELRO: Complete Necessary Writes First, Then Tighten Permissions

The linker marks areas suitable for conversion to read-only after relocation with PT_GNU_RELRO, and the dynamic loader adjusts permissions after completing the relevant work. -z relro requests this area, and -z now requests immediate binding; full RELRO usually requires both to work together, and standalone NOW does not automatically create all read-only protections.

Lazy binding requires retaining relevant modifiable locations, and the layout affects which GOT areas can be protected. You cannot guarantee that all bytes are read-only in the same way just by seeing .got or .dynamic names; you should combine program headers, dynamic tags, and actual mappings. GNU ld Options

Immediate resolution moves some work to the startup phase, with costs depending on the scale of symbols and dependencies. RELRO protects the corresponding mappings but cannot prevent function pointers in arbitrary heap objects from being incorrectly overwritten, nor does it prevent programs with permissions from actively changing certain memory policies.

RELRO is a permission change at load time, not an inherent attribute of section names Link-time description of scope PT_GNU_RELRO + NOW Load-time address filling Completes necessary relocations at this time Subsequent permission tightening Relevant pages become read-only Three pieces of evidence: product scope → binding policy → current mapping permissions Looking at only one output from readelf cannot replace verification of the entire loading phase Illustrates applicable dynamic ELF; page boundaries and actual ranges are jointly determined by link layout and loader implementation.

How to Correlate File Scope with Runtime Read-Only Areas

GNU_RELRO in readelf -lW gives the virtual address and length from the link perspective; for PIE, first obtain the main image load offset B, then convert to the runtime range. maps displays intervals by page and permissions, so you should not expect the start and end addresses of a single maps line to exactly match the original byte range of GNU_RELRO. You can use the address conversion method in conjunction with ELF File Format.

ObservationConclusion That Can Be DrawnInsufficient To Infer
Has GNU_RELROThe linker described the candidate protection scopeAll relevant pages are currently read-only
Has BIND_NOW / NOWRequests early resolution of applicable dynamic symbolsAny function pointer is protected
Mapping corresponding range is read-onlyCannot be modified with normal write permissions currentlyPermissions will never change during the process lifecycle
Another library in the same directory was not checkedEvidence for the main program is still validAll loaded objects use the same hardening configuration

When checking plugins and dependency libraries, use their respective objects as units. The main program having a certain flag does not automatically recompile other objects into the same form afterwards.

Why Must Relocation Write First, Then Protect?

Observing options and moments together: with only NOW, function addresses can be resolved early, but this does not automatically result in RELRO protection; with only RELRO, call slots that still need delayed modification may remain outside the protection scope. After protection is complete, the normal heap is still writable, indicating that hardening conclusions must point to specific areas.

Preparing the visual
Why Must Relocation Write First, Then Protect?

RELRO describes the scope to be tightened after loading, and NOW determines whether applicable function binding is completed in advance. Normal heap objects do not automatically become read-only due to these flags.

Stack Protection: Detecting Partial Stack Frame Corruption

Stack protector saves a protection value in selected functions and checks it on specified exit paths. When triggered, it usually enters a failure handler and terminates. The protection scope varies depending on -fstack-protector, -fstack-protector-strong, -fstack-protector-all, and target compiler rules; it cannot be described as placing a fixed 8-byte value in every function.

It mainly detects stack corruption passing through relevant layouts but cannot detect all local variable overwrites, heap overflows, or arbitrary data corruption. The initialization of the protection value depends on the runtime library and architecture; you cannot directly attribute the presence of AT_RANDOM to the randomize_va_space switch. GCC Stack Protection Options

NX, W^X, and Stack Conflict Protection

NX/XN and other page execution permission controls determine whether data pages can be fetched and executed. ELF program headers and runtime mappings determine actual permissions; PT_GNU_STACK can express the need for an executable stack; anonymous heaps and stacks are not all directly described by a single ELF PF_X field.

W^X is a policy of not giving execution capability to the same writable area at the same time. JIT and other runtimes may require explicit permission switching or dual-mapping designs; you cannot check the startup mapping once and assume it remains unchanged for the entire lifecycle.

NX does not prevent reusing existing executable code, nor does it prevent pure data errors. stack-clash-protection avoids crossing protection zones through stack growth detection, etc.; it is another type of mechanism and cannot replace stack canaries.

FORTIFY_SOURCE: Using Object Size Checks for Some Library Calls

FORTIFY provides compile-time or runtime checks for some libc interfaces based on object size information available to the compiler. On supported toolchains, level 3 can use dynamic object size information; the effect depends on optimization, call form, and version.

It does not instrument all C pointer reads and writes, and cannot prove that every memory access is legal. Safe calls may be proven at compile time, leaving no _chk symbol; therefore, "not seeing a certain check symbol" does not directly prove that the option is ineffective. glibc Source Fortification

CET: Hardware Support, Product Markers, and Runtime Enablement Should Be Viewed Separately

CET includes shadow stack and IBT. Shadow stack verifies return addresses against protected copies; IBT checks whether specific indirect call/jmp reach marked entry points. IBT does not check normal ret and cannot be described alone as a mechanism that prevents all ROP.

Support for each feature depends on the CPU; you cannot generalize it as "Intel 11th Gen and later." Linux kernel, runtime libraries, loaders, and binaries also need to jointly support and actually enable it.

The Linux kernel documentation checked in this article distinguishes between user-space shadow stack and kernel IBT support. The presence of SHSTK in ELF notes or ENDBR in assembly is insufficient to prove that the corresponding checks are enabled at runtime; user-space shadow stack can be verified by combining the relevant thread feature fields in /proc/PID/status. -fcf-protection=full is not a guarantee that all CET mechanisms are automatically enabled across any Linux environment. Linux CET Documentation

Purpose of CFI and Sanitizers

CFI constrains certain indirect calls and conversions according to type or control flow models, and specific strategies can be finer-grained than just identifying legal entry points. Clang's different CFI checks have different LTO, visibility, and cross-shared-library requirements; they must be configured according to the selected checks; they still only cover the edges and target sets modeled by them. Clang CFI

ASan, TSan, etc., are mainly used to detect execution-time errors and should not be treated as low-cost, compatibility-free default production sandboxes. For fixing and verifying actual memory errors, see Valgrind and Sanitizers. Kernel slab randomization and objects controlled by kptr_restrict, etc., are also different and cannot be treated as direct fixes for ordinary user-space heap UAF or arbitrary information leaks.

A Build and Check Example with Explicit Parameters

The following is for modern Linux GCC/glibc environments supporting the corresponding options. Existing projects should be placed in the correct targets and compilation/linking stages to avoid mutual overwriting of duplicate definitions. Older toolchains need to choose the supported FORTIFY level based on their capabilities.

First, save the following demo.c. It accepts up to 63 bytes and checks the length before copying to a local array; long inputs are rejected before copying. This normal program is used to check build artifacts while retaining clear boundaries, without needing to cause out-of-bounds access to observe hardening options.

#include <stdio.h>
#include <string.h>
__attribute__((noinline))
static unsigned checksum(const char *input, size_t length) {
    char buffer[64];
    memcpy(buffer, input, length);
    buffer[length] = '\0';
    unsigned value = 0;
    for (size_t i = 0; i < length; ++i)
        value = value * 33u + (unsigned char)buffer[i];
    return value;
}
int main(int argc, char **argv) {
    if (argc != 2) return 2;
    size_t length = strlen(argv[1]);
    if (length >= 64) { fputs("input too long\n", stderr); return 2; }
    printf("%u\n", checksum(argv[1], length));
    return 0;
}

The unsigned accumulation in checksum allows modulo wraparound; it is a demonstration operation, not a cryptographic digest. This function is an internal helper, and the length being less than 64 is a precondition for the call; if changed to a public interface, the capacity check should be incorporated into the interface itself.

gcc -O2 -fPIE -pie -fstack-protector-strong -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3 \
  -Wl,-z,relro,-z,now,-z,noexecstack demo.c -o demo
readelf -hW demo
readelf -lW demo
readelf -dW demo
readelf -nW demo

Then run ./demo abc, expecting output 108966; an empty string should output 0, and 64-byte input should return 2 and report "input too long". Use objdump -d --disassemble=checksum demo to check the actual function; optimization may adjust function names or create specialized variants, in which case combine nm and full disassembly for positioning.

To distinguish between source code verification and hardening verification: long inputs being rejected by the application itself is evidence of length checking; it does not trigger canary or FORTIFY. Whether the compiler retained the local array and protection checks should be judged by the instructions and symbol evidence of the corresponding function. noinline does not prohibit all cross-function analysis: the compiler may still derive the length range from the unique call site, inline memcpy without generating __memcpy_chk. If a certain version generates that symbol, it cannot be inferred that all memcpy calls undergo runtime checks.

CheckEvidence Provided
ELF type, interpreter, and tool identificationWhether the main program forms PIE as expected; ET_DYN alone may also be a shared library
GNU_RELRO program header and NOW dynamic tagRELRO and immediate binding requests
GNU_STACK permissionsWhether the product requests an executable stack
Disassembly, symbols, and compilation recordsProtection and check code for specific functions
Runtime maps, thread featuresCurrent mapping permissions and whether applicable hardware mechanisms are actually enabled

Checking build artifacts and runtime confirmation should be performed in conjunction. Hardening options may also affect startup, code size, plugin interfaces, and debugging; verify actual functionality and representative performance before comparing solutions, avoiding references to fixed percentage overheads.

Writing Verifiable Conclusions for Hardening Checks

"Everything enabled" hides the scope of evidence. A more useful record is: which build/link options produced this specific binary, which program headers or dynamic tags support the conclusion, which functions actually have checks, and which runtime states have been observed.

Conclusion WritingWhy It Is More Accurate
Main program has GNU_RELRO and NOWClearly indicates product request, not the runtime state of all dependencies
GNU_STACK does not request execution permissionsSpecifies that the check object is stack requirements, not generalizing all anonymous mappings
Stack protection check exists in checksumSpecifies the function, avoiding claims that all functions are protected
Input boundary cases passIndicates application functionality verification, not equivalent to triggering hardening mechanisms
CET runtime enablement not yet checkedIndicates evidence gap, cannot use ENDBR instruction as a substitute

When encountering compatibility issues, first locate where the failure occurs—compilation, linking, loading, or specific calls—then narrow down to relevant options. The fact that the program runs after canceling all options only indicates that this set of settings affects behavior, and does not point out which mechanism or which source code contract is problematic.

Keywords: ASLR, PIE, RELRO, BIND_NOW, stack protector, NX, W^X, FORTIFY_SOURCE, CET, IBT, shadow stack, CFI