Compiler and linker hardening

The compiler and linker can insert or enforce a set of mitigations that raise the cost of exploiting memory corruption vulnerabilities. Most are off by default. In hosted environments they are routine; in embedded and OT targets the picture is more complicated, because some mitigations carry runtime overhead that conflicts with real-time constraints, and some require OS or hardware support that bare-metal targets lack entirely.

The practical approach is to enable everything the target supports, know why each flag exists, and document explicitly which mitigations are absent and why.

Stack canaries

-fstack-protector-strong inserts a random value (the canary) between local variables and the saved return address. On function return the canary is checked; a mismatch triggers an abort before the corrupted return address is used.

-fstack-protector-strong instruments functions that have stack-allocated buffers, use alloca, or take the address of a local variable. It is narrower than -fstack-protector-all (which instruments every function) and wider than -fstack-protector (which only instruments functions with character arrays larger than eight bytes).

On RTOS targets the canary abort handler needs to be implemented: the default __stack_chk_fail provided by the C library either calls abort() or is a stub. In safety-critical OT code, a well-configured abort handler logs the fault, transitions the controlled process to a safe state, and halts or restarts rather than silently continuing.

Stack canaries do not stop a heap overflow or a write-what-where primitive that targets the canary directly, but they stop naive stack smashing reliably.

Position-independent code

-fPIC and -fPIE generate position-independent code and executables, which is the prerequisite for ASLR at the OS level. On a general-purpose OS, ASLR randomises load addresses at runtime, making it harder to predict where shellcode or return-oriented programming gadgets land.

On bare-metal targets without an OS there is no ASLR, so -fPIE provides no runtime randomisation benefit. It is still worth using on engineering workstations, historian servers, and HMI applications running on Windows or Linux, where the OS can apply ASLR.

On microcontroller targets with fixed memory maps, the flag is typically omitted or unsupported by the toolchain.

RELRO and GOT protection

On Linux-based OT systems (historians, HMIs, soft PLCs), the dynamic linker’s Global Offset Table is a common target for write-primitive exploits: overwriting a GOT entry redirects the next call to a library function.

Full RELRO (-Wl,-z,relro,-z,now) resolves all dynamic symbols at load time and marks the GOT read-only before execution begins. The cost is slightly longer startup time as all symbols are resolved eagerly rather than on first call. For OT processes that run continuously, the startup cost is paid once.

LDFLAGS += -Wl,-z,relro,-z,now

Partial RELRO (-z,relro without -z,now) marks some sections read-only but leaves the GOT writeable for lazy binding. It is weaker.

Format string hardening

-Wformat -Wformat-security -Werror=format-security produces an error when a format string argument to printf-family functions is not a string literal. A non-literal format string that contains user-controlled data is a format string vulnerability.

/* unsafe: user_input as format string */
printf(user_input);

/* safe: explicit format specifier */
printf("%s", user_input);

This is a warning-as-error at compile time with no runtime cost.

Integer overflow detection

-fsanitize=signed-integer-overflow (and -fsanitize=unsigned-integer-overflow with Clang’s UBSan) instruments integer arithmetic to trap on overflow at runtime. This is a debugging and testing tool rather than a production mitigation: the instrumentation adds overhead and the trap handler needs to be defined for embedded targets.

Running with sanitisers in a hardware-in-the-loop test environment catches overflow bugs before they reach production firmware, where the sanitiser overhead would be unacceptable.

A note on -Wconversion

-Wconversion warns on implicit type conversions that may silently truncate or change sign. In C code that processes protocol frame fields, a uint16_t field silently narrowed to uint8_t is a class of bug that produces incorrect behaviour under specific frame values rather than a crash. The warning is noisy on legacy codebases but worth enabling for new code.

Practical flag set

For a cross-compiled embedded target (GCC, ARM Cortex-M, FreeRTOS):

CFLAGS  += -Wall -Wextra -Wformat -Wformat-security -Werror=format-security
CFLAGS  += -Wconversion -Wshadow
CFLAGS  += -fstack-protector-strong
CFLAGS  += -fno-common
LDFLAGS += -Wl,--warn-common

ASLR, RELRO, and full PIE are omitted because the target has no OS and a fixed memory map.

For a Linux-based soft PLC, historian, or HMI application:

CFLAGS  += -Wall -Wextra -Wformat -Wformat-security -Werror=format-security
CFLAGS  += -Wconversion -Wshadow
CFLAGS  += -fstack-protector-strong
CFLAGS  += -fPIE
LDFLAGS += -pie
LDFLAGS += -Wl,-z,relro,-z,now

The flag set is a floor, not a ceiling. Each mitigation addresses a different failure mode; omitting one because another is present does not close the gap.