Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Assembly (x86-64, AArch64, RISC-V)  Module 15 – Security at the machine level (defensive)

Hardening in compiler output: canaries, PIE, NX

Read the guard word a hardened build adds on x86-64, AArch64 and RISC-V, watch a checked library call stop a copy, and check your own binary's flags.

  • Intermediate
  • 20 minutes
  • Examples run with Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 3 interview questions.

What you will learn

  • Read the guard-word sequence a hardened build adds, on x86-64, AArch64 and RISC-V
  • Explain what the stack protector and a checked library call each stop, and what they miss
  • Check your own binary's image type, stack permissions and relocation window
  • Distinguish a mitigation that changes how a bug fails from a fix that removes the bug

Before you start

What you’ll learn · Before you start

What you will learn

  • Read the guard-word sequence a hardened build adds, on x86-64, AArch64 and RISC-V
  • Explain what the stack protector and a checked library call each stop, and what they miss
  • Check your own binary's image type, stack permissions and relocation window
  • Distinguish a mitigation that changes how a bug fails from a fix that removes the bug

Before you start

On this page

The same source, built twice, behaved two different ways: one build copied 36 bytes into a 16-byte buffer and then died on the way out of the damaged function, the other stopped itself cleanly with status 134. Nothing in the C changed. One compiler option did.

Hardened builds are the reason a disassembly rarely matches the source instruction for instruction: the compiler has added a guard word, swapped a library call for a checked one, and asked the linker for an image the loader may place anywhere. This page reads those additions in generated code on three instruction sets, runs them to see what each one catches, and ends with the fields you can check on a binary you built yourself.

A guard word catches an overflow at the return

The copy below has the oldest bug in C: it copies until the terminating zero byte and never compares the length with the size of the buffer.

copy_loop.c, a copy with no length check C · copy_loop.c
/* A name is copied into a 16-byte buffer by hand, with no length check: the bug this page is about. */
#include <stdio.h>

__attribute__((noinline)) static void copy_loop(const char *src) {
    char name[16];
    int i = 0;
    while (src[i] != '\0') {      /* nothing compares i with sizeof name */
        name[i] = src[i];
        i++;
    }
    name[i] = '\0';
    printf("copied %d bytes: %s\n", i, name);
}

int main(void) {
    setvbuf(stdout, NULL, _IONBF, 0);   /* print each line as it happens, so the output survives an abort */
    copy_loop("Aarav");
    copy_loop("a name far longer than sixteen bytes");
    printf("returned normally\n");
    return 0;
}

Build it twice, with and without the stack protector, and run both:

The same overflow with the protector on and off Python · canary_session.py
import pathlib
import sys

sys.path.insert(0, str(pathlib.Path(__file__).parent / "sessions"))
import session

session.run("""
# Build the unchecked copy twice: once with the stack protector the compiler offers, once without it
clang -O1 -fstack-protector-strong -o copy_guarded copy_loop.c
clang -O1 -fno-stack-protector -o copy_bare copy_loop.c
# Without the protector, 36 bytes land in a 16-byte buffer and nothing checks them before the return
{ ./copy_bare; } 2>/dev/null; echo "exit=$?"
# With it, the same overflow is caught where the damaged function returns
{ ./copy_guarded; } 2>/dev/null; echo "exit=$?"
""")

session.run("""
# The guard word and its comparison are the only difference in the generated code
clang -O1 -fstack-protector-strong -S -o guarded.s copy_loop.c
clang -O1 -fno-stack-protector -S -o bare.s copy_loop.c
grep -c 'stack_chk' guarded.s bare.s
grep -n -A1 'stack_chk_guard@GOTPAGE' guarded.s | head -8
grep -n -B2 -A1 'stack_chk_fail' guarded.s | tail -8
""")

Output

# Build the unchecked copy twice: once with the stack protector the compiler offers, once without it
$ clang -O1 -fstack-protector-strong -o copy_guarded copy_loop.c
$ clang -O1 -fno-stack-protector -o copy_bare copy_loop.c
# Without the protector, 36 bytes land in a 16-byte buffer and nothing checks them before the return
$ { ./copy_bare; } 2>/dev/null; echo "exit=$?"
copied 5 bytes: Aarav
copied 36 bytes: a name far longer than sixteen bytes
exit=138
# With it, the same overflow is caught where the damaged function returns
$ { ./copy_guarded; } 2>/dev/null; echo "exit=$?"
copied 5 bytes: Aarav
copied 36 bytes: a name far longer than sixteen bytes
exit=134
# The guard word and its comparison are the only difference in the generated code
$ clang -O1 -fstack-protector-strong -S -o guarded.s copy_loop.c
$ clang -O1 -fno-stack-protector -S -o bare.s copy_loop.c
$ grep -c 'stack_chk' guarded.s bare.s
guarded.s:5
bare.s:0
$ grep -n -A1 'stack_chk_guard@GOTPAGE' guarded.s | head -8
58:	adrp	x8, ___stack_chk_guard@GOTPAGE
59-Lloh10:
60:	ldr	x8, [x8, ___stack_chk_guard@GOTPAGEOFF]
61-Lloh11:
--
91:	adrp	x9, ___stack_chk_guard@GOTPAGE
92-Lloh15:
93:	ldr	x9, [x9, ___stack_chk_guard@GOTPAGEOFF]
$ grep -n -B2 -A1 'stack_chk_fail' guarded.s | tail -8
101-	ret
102-LBB1_7:
103:	bl	___stack_chk_fail
104-	.loh AdrpLdrGotLdr	Lloh9, Lloh10, Lloh11

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 canary_session.py

Both builds copied 36 bytes into name[16] and printed them, so both overflowed. What differs is the ending. The build with -fstack-protector-strong ended with status 134, which a shell reports as 128 + 6, a process ended by signal 6: the guarded function noticed the damage and called the failing routine. The build with -fno-stack-protector reached its own return instruction with a return address made of the overflowing name, jumped there and ended with status 138. That second ending is undefined behaviour, not a defence; it happened to fault here, and on another day, with other bytes, it could land somewhere that runs.

The guarded listing has five stack_chk lines: an adrp and ldr pair on entry, the same pair before the return, and the call to the failing routine. Step through the frame they protect:

Interactive · step by step What the stack protector adds around a buffer Step through one guarded frame. The left column is what your code asked for, the right column what the compiler added.
What the stack protector adds around a buffer, step 4 of 4. Showing name[16], saved x29, x30, guard word, __stack_chk_guard, compare. In focus: guard word, compare.name[16]your 16-byte buffersaved x29, x30set by the prologueguard wordcopied in on entry__stack_chk_guardone per programcomparebefore the return

The drawing shows the whole scene; its 4 steps are listed below.

Every step as text
  1. The frame your code needs. The function asks for 16 bytes for the name. At higher addresses, the prologue saved the frame pointer and the return address, because the function calls printf.
  2. A guard word between them. With the protector on, the prologue also loads one program-wide value and stores a copy between name[16] and the saved registers. An overflow that runs up from name[16] towards them has to cross that copy first.
  3. The comparison before the return. Before the epilogue returns, the function reloads the program-wide value and compares it with the copy. Equal, and it returns as usual; different, and it calls the failing routine instead.
  4. What the copy catches. A 36-byte write into name[16] changed the copy, so the comparison failed and the program stopped. A write that skips over the copy, or one into a buffer the compiler did not protect, would not be caught here.

The stack canary: A value that a protected function copies into its stack frame on entry and compares with the original before it returns. If an overflow of a local buffer changed the copy, the program stops instead of returning through the damaged frame. More in “Reading compiler output: -O0 versus -O2” is the copy in the frame; the program-wide value it is compared with is named __stack_chk_guard in C, and a Mach-O listing puts one more underscore in front of every C name, which is why it reads ___stack_chk_guard. On AArch64 the listing reaches it through the table of addresses the loader fills in, with adrp and two ldr instructions; cmp and b.ne then route a mismatch to ___stack_chk_fail instead of the return. GCC’s documentation sets out which functions get this treatment: the plain form protects functions that call alloca and functions with buffers of eight bytes or more, and the strong form adds any function with a local array or with a reference to a local frame address. A function of pure arithmetic gets nothing, which is why counting the stack_chk lines is a quick way to see what a build chose.

Where the value lives differs by target, and the difference shows in one instruction:

The guard word on x86-64, AArch64 and RISC-V Python · isa_canary_session.py
import pathlib
import sys

sys.path.insert(0, str(pathlib.Path(__file__).parent / "sessions"))
import session

session.run("""
# Where the guard word comes from differs by target. x86-64 reads it from thread-local storage at %fs:40
clang --target=x86_64-linux-gnu -O2 -fstack-protector-strong -S -o - copy_name_portable.c | grep -E 'fs:40|stack_chk|cmpq|jne'
""")

session.run("""
# AArch64 and RISC-V read a global, __stack_chk_guard, through the table of addresses the loader fills in
clang --target=aarch64-linux-gnu -O2 -fstack-protector-strong -S -o - copy_name_portable.c | grep -E 'stack_chk|cmp|b.ne'
clang-23 --target=riscv64-linux-gnu -march=rv64gc -O2 -fstack-protector-strong -S -o - copy_name_portable.c | grep -E 'stack_chk|bne|call'
""")

Output

# Where the guard word comes from differs by target. x86-64 reads it from thread-local storage at %fs:40
$ clang --target=x86_64-linux-gnu -O2 -fstack-protector-strong -S -o - copy_name_portable.c | grep -E 'fs:40|stack_chk|cmpq|jne'
	movq	%fs:40, %rax
	movq	%fs:40, %rax
	cmpq	24(%rsp), %rax
	jne	.LBB0_2
	callq	__stack_chk_fail@PLT
	.addrsig_sym __stack_chk_fail
# AArch64 and RISC-V read a global, __stack_chk_guard, through the table of addresses the loader fills in
$ clang --target=aarch64-linux-gnu -O2 -fstack-protector-strong -S -o - copy_name_portable.c | grep -E 'stack_chk|cmp|b.ne'
	adrp	x8, :got:__stack_chk_guard
	ldr	x8, [x8, :got_lo12:__stack_chk_guard]
	adrp	x8, :got:__stack_chk_guard
	ldr	x8, [x8, :got_lo12:__stack_chk_guard]
	cmp	x8, x9
	b.ne	.LBB0_2
	bl	__stack_chk_fail
$ clang-23 --target=riscv64-linux-gnu -march=rv64gc -O2 -fstack-protector-strong -S -o - copy_name_portable.c | grep -E 'stack_chk|bne|call'
	auipc	a0, %got_pcrel_hi(__stack_chk_guard)
	call	strcpy
	call	puts
	bne	a0, a1, .LBB0_2
	call	__stack_chk_fail

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 isa_canary_session.py

x86-64 keeps it in the running thread’s own storage block and reads it with movq %fs:40, %rax, a single load with no address computation. AArch64 and RISC-V read the global, one through adrp and :got:__stack_chk_guard, the other through auipc and %got_pcrel_hi(__stack_chk_guard). The shape is identical on all three: load on entry, compare before the return, branch to a failing routine.

Checkpoint

Check: The guard word and its comparison

  1. Question 1 of 2 In the recorded AArch64 listing, where does the protected function get the value it copies into its frame?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: From the program-wide variable __stack_chk_guard, read through the table of addresses the loader fills in

    The protected function loads one program-wide value and keeps a copy in its frame. x86-64 reads it from thread-local storage at %fs:40; AArch64 and RISC-V read the global __stack_chk_guard.

  2. Question 2 of 2 The guarded build printed both lines and then ended with status 134. What does 134 say happened?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The process was killed by signal 6: the comparison failed, so the function called the failing routine instead of returning

    The guard word is compared just before the epilogue returns. A damaged copy sends the function to the failing routine, which ends the process: the shell reports 128 + 6 = 134.

Checked library calls stop the program at the copy

The guard word acts late, at the return. A second addition acts at the moment of the copy, and it only appears when the compiler can see how much room the destination has.

copy_name.c, the same job with strcpy C · copy_name.c
/* One function with a 16-byte buffer and a library call, so the compiler shows both of its checks. */
#include <stdio.h>
#include <string.h>

void copy_name(const char *src) {
    char name[16];
    strcpy(name, src);
    printf("%s\n", name);
}
A strcpy that became a checked call Python · fortify_session.py
import pathlib
import sys

sys.path.insert(0, str(pathlib.Path(__file__).parent / "sessions"))
import session

session.run("""
# One function with a 16-byte buffer and a strcpy: the compiler adds two different checks
clang -O2 -fstack-protector-strong -S -o - copy_name.c | grep -E 'stack_chk|strcpy|cmp|b.ne|sub.sp'
""")

session.run("""
# A library call whose destination size the compiler knows becomes the checked form, and it stops the program at the copy
clang -O2 -fstack-protector-strong -o fortify_main fortify_main.c
{ ./fortify_main; } 2>/dev/null; echo "exit=$?"
# The version that compares the length itself accepts the short name and refuses the long one
clang -O1 -fstack-protector-strong -o copy_checked copy_checked.c
./copy_checked; echo "exit=$?"
""")

session.run("""
# No flag asked for the checked calls: the macro that switches them on already has a value in Apple's headers
echo '#include <string.h>' | clang -O2 -dM -E -x c - | grep 'define _FORTIFY_SOURCE'
""")

Output

# One function with a 16-byte buffer and a strcpy: the compiler adds two different checks
$ clang -O2 -fstack-protector-strong -S -o - copy_name.c | grep -E 'stack_chk|strcpy|cmp|b.ne|sub.sp'
	sub	sp, sp, #48
	adrp	x8, ___stack_chk_guard@GOTPAGE
	ldr	x8, [x8, ___stack_chk_guard@GOTPAGEOFF]
	bl	___strcpy_chk
	adrp	x9, ___stack_chk_guard@GOTPAGE
	ldr	x9, [x9, ___stack_chk_guard@GOTPAGEOFF]
	cmp	x9, x8
	b.ne	LBB0_2
	bl	___stack_chk_fail
# A library call whose destination size the compiler knows becomes the checked form, and it stops the program at the copy
$ clang -O2 -fstack-protector-strong -o fortify_main fortify_main.c
$ { ./fortify_main; } 2>/dev/null; echo "exit=$?"
Aarav
exit=133
# The version that compares the length itself accepts the short name and refuses the long one
$ clang -O1 -fstack-protector-strong -o copy_checked copy_checked.c
$ ./copy_checked; echo "exit=$?"
copied: Aarav
rejected: input longer than the buffer
returned normally
exit=0
# No flag asked for the checked calls: the macro that switches them on already has a value in Apple's headers
$ echo '#include <string.h>' | clang -O2 -dM -E -x c - | grep 'define _FORTIFY_SOURCE'
#define _FORTIFY_SOURCE 2

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 fortify_session.py

The listing shows both checks in one function: the guard word around the frame, and bl ___strcpy_chk where the source said strcpy. The checked form takes the destination size as a third argument, and the compiler can supply it because name is a 16-byte array in this frame. hardened library checks: Checked forms of library functions that a compiler substitutes when it can see the size of the destination, such as a copy routine that also takes that size and ends the process when the data does not fit. are what the substitution is called, and the macro _FORTIFY_SOURCE switches them on. The session’s last command shows that Apple’s headers already give it the value 2, which is why no flag asked for it; with the GNU C library you define it yourself, and the OpenSSF hardening guide recommends -D_FORTIFY_SOURCE=3 with optimisation at -O2 or above.

The run shows the order the two checks fire in. The short name printed. The long one did not: the process ended with status 133, which is 128 + 5, inside the checked copy, long before the guarded function would have reached its return. Apple’s published C library source shows what happened in there: its __strcpy_chk copies first, compares the length it copied with the 16 it was given, and ends the process on a mismatch. The bytes did land, but nothing after the copy ever ran. Step through the path from the source to that stop:

Interactive · stage by stage How a checked library call stops a copy Follow one strcpy from source to run time, through the checked form the compiler substituted for it.
How a checked library call stops a copy, step 5 of 5. Showing strcpy(name, src), size known?, call __strcpy_chk, length vs 16, stop the process. In focus: stop the process.strcpy(name, src)what you wrotesize known?name is char[16]call __strcpy_chkwith 16 as a third argumentlength vs 16measured at run timestop the processbefore the return

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. The call you wrote. The source says strcpy, which copies until the terminating zero byte and never looks at how much room the destination has.
  2. A destination of known size. The compiler can see that name is a 16-byte array in this frame, so it knows the size the copy must respect. For a destination it cannot size, a pointer from elsewhere, it leaves the plain call alone.
  3. The checked form instead. Knowing the size, it emits a call to the checked variant and passes 16 along with the two pointers. The recorded listing shows bl ___strcpy_chk where the source said strcpy.
  4. The comparison at run time. The checked routine compares the length of the copy with the 16 it was given; Apple's version copies first and compares after. A name that fits is copied exactly as before, so nothing changes for correct input.
  5. The stop happens first. A 36-byte name does not fit, so the routine ends the process inside the copy, and the function never reaches its return. The recorded run printed the short name, then stopped with status 133 at the long one.

Both additions are narrow on purpose, and the same session shows the fix that is not narrow. The length-checked version compares the length with sizeof name first, returns a failure for input that does not fit, and prints rejected: input longer than the buffer before finishing with status 0. No mitigation: A defence that changes what a remaining bug can do, rather than removing the bug: it may detect the damage, deny a step an attack needs, or make a result unpredictable to anyone outside the process. was involved in that ending: the overflow simply never happened.

Security

A hand-written copy loop never becomes a checked call, and a copy into a heap buffer gets no guard word either. Most parsers are full of both, which is why the length comparison belongs in the code, with the mitigations behind it rather than in front of it.

Checkpoint

Check: Checked library calls

  1. Question 1 of 2 The listing of the strcpy version shows bl ___strcpy_chk. What made the compiler change the call?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It could see the destination is a 16-byte array, so it passed that size to the checked form of the copy

    With a destination whose size it can see, the compiler emits the checked variant and passes the size. A destination it cannot size keeps the plain call, so this check is not a general defence.

  2. Question 2 of 2 The strcpy build printed the short name, then stopped with status 133 at the long one. Why not status 134, like the guard-word build?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The checked call stopped the process at the copy, before any return, and it ends the process with a different signal

    The two checks act at different times: the checked library call refuses the copy as it happens, while the guard word is only compared when the damaged function returns.

PIE, a non-executable stack and RELRO are facts about the file

The next three defences are not instructions at all. They are recorded in the executable’s headers, decided at link time, and applied by the loader, so you can read them from a file without running it.

Four links of one object, and what each header says Python · report_session.py
import pathlib
import sys

sys.path.insert(0, str(pathlib.Path(__file__).parent / "sessions"))
import session

session.run("""
# On a Mach-O file the loader reads the flags in the header: PIE says the image may be placed anywhere
clang -O1 -fstack-protector-strong -o copy_checked copy_checked.c
otool -hv copy_checked | tail -2
""")

session.run("""
# The same questions on an ELF file are answered by the program headers, so link one object four ways
clang --target=aarch64-linux-gnu -ffreestanding -fPIE -c -o report_target.o report_target.c
ld.lld -pie -e _start -o report_pie report_target.o
ld.lld -pie -z relro -z now -e _start -o report_relro report_target.o
ld.lld -pie -z execstack -e _start -o report_execstack report_target.o
ld.lld -e _start -o report_fixed report_target.o
# DYN means the image may be placed anywhere; EXEC means it insists on its link-time addresses
llvm-readelf -h report_pie report_fixed | grep -E 'File:|Type:'
""")

session.run("""
# The stack's permissions and the read-only relocation window are program headers, one line each
llvm-readelf -l report_pie report_execstack | grep -E 'File:|GNU_STACK|GNU_RELRO'
llvm-readelf -l report_relro | grep -E 'GNU_STACK|GNU_RELRO'
llvm-readelf -d report_relro | grep -E 'FLAGS'
""")

Output

# On a Mach-O file the loader reads the flags in the header: PIE says the image may be placed anywhere
$ clang -O1 -fstack-protector-strong -o copy_checked copy_checked.c
$ otool -hv copy_checked | tail -2
      magic  cputype cpusubtype  caps    filetype ncmds sizeofcmds      flags
MH_MAGIC_64    ARM64        ALL  0x00     EXECUTE    17       1056   NOUNDEFS DYLDLINK TWOLEVEL PIE
# The same questions on an ELF file are answered by the program headers, so link one object four ways
$ clang --target=aarch64-linux-gnu -ffreestanding -fPIE -c -o report_target.o report_target.c
$ ld.lld -pie -e _start -o report_pie report_target.o
$ ld.lld -pie -z relro -z now -e _start -o report_relro report_target.o
$ ld.lld -pie -z execstack -e _start -o report_execstack report_target.o
$ ld.lld -e _start -o report_fixed report_target.o
# DYN means the image may be placed anywhere; EXEC means it insists on its link-time addresses
$ llvm-readelf -h report_pie report_fixed | grep -E 'File:|Type:'
File: report_pie
  Type:                              DYN (Shared object file)
File: report_fixed
  Type:                              EXEC (Executable file)
# The stack's permissions and the read-only relocation window are program headers, one line each
$ llvm-readelf -l report_pie report_execstack | grep -E 'File:|GNU_STACK|GNU_RELRO'
File: report_pie
  GNU_RELRO      0x000270 0x0000000000020270 0x0000000000020270 0x000090 0x000d90 R   0x1
  GNU_STACK      0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW  0x0
File: report_execstack
  GNU_RELRO      0x000270 0x0000000000020270 0x0000000000020270 0x000090 0x000d90 R   0x1
  GNU_STACK      0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RWE 0x0
$ llvm-readelf -l report_relro | grep -E 'GNU_STACK|GNU_RELRO'
  GNU_RELRO      0x000270 0x0000000000020270 0x0000000000020270 0x0000a0 0x000d90 R   0x1
  GNU_STACK      0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW  0x0
$ llvm-readelf -d report_relro | grep -E 'FLAGS'
  0x000000000000001e (FLAGS)    BIND_NOW
  0x000000006ffffffb (FLAGS_1)  NOW PIE

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 report_session.py

The Mach-O header’s flags line ends with PIE: this image is a position-independent executable: An executable whose code reaches other code and data through PC-relative addressing instead of fixed addresses, so it runs correctly at any load address. This is what lets the loader randomise its placement under ASLR. More in “Memory and a process's address space”, so the loader may place it at a different address on every run. The same question on an ELF file is answered by the file type, and the recorded links show both answers from one object: DYN for the position-independent link, EXEC for the link that insists on its own addresses.

Two program headers carry the other two fields. GNU_STACK holds the permissions the kernel gives the stack mapping: RW in the default link and the RELRO link, and RWE in the one linked with -z execstack. A non-executable stack: A stack mapping without execute permission, so instructions cannot be fetched from bytes stored there. On ELF the GNU_STACK program header carries the permissions. is the RW case, and what it stops is narrow and exact: instructions cannot be fetched from bytes stored on the stack. It does not stop a single write. GNU_RELRO names a window of memory the loader write-protects once it has applied the relocations in it. ld.lld writes one unless it is told -z norelro, so all three position-independent links show it; only the link with -z relro -z now also reported BIND_NOW and NOW PIE in its dynamic flags: resolve everything up front, then make that window read-only. read-only relocations: A link setting that has the loader apply the relocations in one window of memory and then write-protect that window, so the pointer tables in it cannot be changed later. Full protection also needs the relocations resolved before the program starts. are the name for the window, and the OpenSSF guide calls the pair with -z now full RELRO: without it, the table that lazy binding fills in later has to stay writable, so it cannot be inside the window.

Key idea

The guard word and the checked call are code the compiler generated; PIE, the stack permissions and the read-only window are fields the linker wrote. The first pair you find by reading instructions, the second by reading headers.

Checkpoint

Check: PIE, a non-executable stack and RELRO

  1. Question 1 of 2 Two links of the same object gave Type: DYN and Type: EXEC. Which one may the loader place at a random address?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: DYN: the image is position-independent, so the loader may place it anywhere

    Randomised placement needs a position-independent image. On ELF that shows as Type: DYN; on Mach-O it is the PIE flag in the header, which the recorded otool line ends with.

  2. Question 2 of 2 Two links differed only in one option, and their GNU_STACK lines read RW and RWE. What does the RWE link allow?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It asks the kernel to map the stack executable, so bytes written there could be run as instructions

    The GNU_STACK program header carries the permissions the kernel gives the stack mapping. RW is the hardened default; RWE is what a program needs only if it generates code on its stack.

Reading a hardening report of your own binary

Put those fields together and you have a report you can run on any binary you built. Four questions, four places to look, and one honest limit on each answer:

Field Where to read it What it means What it does not say
Image type Type: in an ELF header, the flags line of otool -hv DYN or PIE: the loader may place the image anywhere Nothing about the code’s own bugs
Stack permissions the GNU_STACK program header RW refuses to run bytes stored on the stack Writes to the stack are unaffected
Relocation window GNU_RELRO plus BIND_NOW in the dynamic flags the loader write-protects the window after resolving it Only that window; other data stays writable
Guard-word code stack_chk in a listing, or the names in the symbol table the protector ran on at least some functions Which functions, once the names are stripped

The last column is the part a report usually leaves out. A binary can pass every field above and still overflow a buffer on every run. The guarded build of the first section is a small proof: its guard-word code was in place, and it still wrote 36 bytes into a 16-byte buffer each time. The protector changed how the bug ended, from an unpredictable jump to an immediate stop, and that is all it changed. A report is a list of defences that are switched on, never a statement that the program is memory-safe.

Two practical cautions follow. First, a field read from a header survives stripping, while a name may not: in a static link __stack_chk_fail is one more name in the symbol table and goes with it, so a stripped binary without that name has to be checked in its disassembly before it is called unprotected. Second, these are build settings on code you own: when a field comes back missing, the answer is a rebuild with the flag, which the compilers’ documentation and the OpenSSF guide list by name.

Checkpoint

Check: Reading a hardening report

  1. Question 1 of 2 Which of these can be answered from a binary alone, with no source and no rebuild?

    Choose every answer that is right.

    Show the answer: Question 1 of 2

    Answer:

    • Whether the stack mapping is executable
    • Whether the image is position-independent
    • Whether the relocation window is made read-only after loading

    A report reads what the file records: image type, stack permissions, the read-only relocation window, and the presence of guard-word code. Whether the program handles its input safely is not in the file.

  2. Question 2 of 2 A report on your binary shows PIE, a non-executable stack, full RELRO and guard-word code. What have you learnt about its memory safety?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Nothing about memory safety: the mitigations make a bug harder to turn into control of the program, but the bug is still there

    The fields say which defences are switched on. Fixing the copy, as the length-checked version does, is what removes the bug; the defences only decide how loudly a remaining bug fails.

Practise

These run on an Apple silicon Mac; on a Linux machine, follow setting up the toolchain first. Work on copies of this page’s own files.

  1. Build copy_loop.c with -fstack-protector instead of -fstack-protector-strong and count the stack_chk lines in the listing. The buffer is 16 bytes, so both forms protect this function; now shrink name to 4 bytes and count again, and the plain form drops it while the strong form keeps it.
  2. Add a second function to copy_loop.c that only multiplies two integers, build with the strong form and look for stack_chk inside it. There is none: no array, no address of a local, nothing to guard.
  3. Run the report session’s four links again with -z noexecstack added to the execstack link and compare the GNU_STACK line. Then link report_target.o without -pie and check which of the four report fields change.
  4. Change copy_checked.c to use a buffer of 64 bytes and run it with the same long name: the rejection line disappears, because the 36 bytes and the terminating zero now fit. The fix depends on the size comparison, not on the buffer being small.

Practice that adapts to you

Practise this lesson

9 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 9 The two builds of the same source differed by one option and by five lines mentioning stack_chk. Which option adds them?

    Choose one answer.

    Show the answer: Question 1 of 9

    Answer: -fstack-protector-strong

    The stack protector is a compiler option. Counting the stack_chk lines in the two listings is the quickest way to see whether a build has it.

  2. Question 2 of 9 Why does the strong form of the protector not add the guard word to every function in a program?

    Choose one answer.

    Show the answer: Question 2 of 9

    Answer: It protects functions that have something worth protecting, such as a local array or an address taken of a local, and leaves the rest alone to save time and space

    Each guard word costs a load, a store and a comparison, so the strong form spends them where a frame holds an array or a local whose address escapes.

  3. Question 3 of 9 The x86-64 listing reads the guard value from %fs:40 rather than from a global. What is at %fs:40?

    Choose one answer.

    Show the answer: Question 3 of 9

    Answer: A slot in the running thread's own storage block, which the segment register fs points at

    The x86-64 ABI keeps per-thread values in a block that fs points at, so the guard value is one load away with no address computation.

  4. Question 4 of 9 Which of these copies can the compiler turn into a checked library call?

    Choose one answer.

    Show the answer: Question 4 of 9

    Answer: A strcpy into a char array of 16 bytes declared in the same function

    The substitution needs a library call and a destination whose size the compiler knows. The hand-written loop of the first section has neither, which is why only the guard word caught it.

  5. Question 5 of 9 A team adds the checked library calls to a build and asks whether their parsing code is now safe. What do you answer?

    Choose one answer.

    Show the answer: Question 5 of 9

    Answer: Only the library calls whose destination size the compiler can see are checked: their own loops, and any copy into a pointer of unknown size, are unchanged

    Both mitigations are narrow by design. The lesson''s hand-written loop is exactly the case neither of them prevents, which is why the length-checked copy is the real fix.

  6. Question 6 of 9 The link with -z relro -z now reported FLAGS BIND_NOW and a GNU_RELRO header. What does that combination do at load time?

    Choose one answer.

    Show the answer: Question 6 of 9

    Answer: The loader resolves the relocations straight away and then makes that part of memory read-only, so it cannot be rewritten later

    Resolving early allows the write-protection, and the write-protection is the point: a pointer table that is never written again cannot be altered by a later bug.

  7. Question 7 of 9 What does a stack mapping without execute permission stop?

    Choose one answer.

    Show the answer: Question 7 of 9

    Answer: Instructions being run from bytes stored on the stack

    A non-executable stack narrows one route: data placed on the stack cannot be executed. It says nothing about what is written there.

  8. Question 8 of 9 Your report finds no stack_chk name in a stripped, statically linked release build of a program you own. What can you conclude?

    Choose one answer.

    Show the answer: Question 8 of 9

    Answer: Little on its own: in a static link the name went with the symbol table, so the guard-word code has to be looked for in the disassembly instead

    Header fields survive stripping; a name in the symbol table may not. A report should say which of its answers came from a header and which from a name, so a stripped binary is not reported as unprotected by mistake.

  9. Question 9 of 9 A report on a program you own comes back with a non-executable stack, no PIE and no RELRO. What do you do about it?

    Choose one answer.

    Show the answer: Question 9 of 9

    Answer: Rebuild it with -pie and -z relro -z now in the same link, then run the report again to confirm Type: DYN, GNU_RELRO and BIND_NOW

    All three fields are build settings, so the fix is a rebuild with the flags, followed by the same report on the new file. A report is useful only when it ends in that rebuild and the second reading.

Interview questions

A warm-up for freshers: a build has the stack protector on, so is the program safe from buffer overflows?

No. The protector does not prevent an overflow; it detects one particular consequence of one afterwards. A guard word is copied into the frame on entry and compared before the return, so an overflow that runs from a local buffer towards the saved return address changes the copy and the comparison fails, ending the process. The overflow still happened: every byte was written. An overflow that goes the other way, one into a heap buffer, one that stops short of the guard word, or one in a function the protector did not cover, is not noticed at all. In this lesson’s recorded run, the guarded build wrote 36 bytes into a 16-byte buffer and printed them before the check fired. The fix is the length comparison in the code; the protector decides only how a remaining bug fails.

Three deeper questions follow from this page: choosing flags for a build you own, reading a report on a stripped binary, and explaining to a team what a mitigation does not do.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    You own a C service that parses network messages. Its build uses -O2 and nothing else. Which hardening options would you add, in what order, and how would you show each one took effect?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to staff

    A report on a stripped binary your team ships says "no stack protector" because it found no __stack_chk_fail symbol. A colleague wants to add the flag to the build. What do you check first, and why?

    Model answer, Pro. First step and follow-ups

  3. Senior to staff

    A team closes a reported overflow in their own code by turning on the stack protector and the checked library calls, and leaves the copy as it is. Explain to them, with evidence, what they have and have not changed.

    Model answer, Pro. First step and follow-ups

Flashcards · 6 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. How does a stack protector notice an overflow, and when?

    The function copies a guard word into its frame on entry, from __stack_chk_guard (or %fs:40 on x86-64), and compares it just before returning. An overflow that ran over the copy changes it, and the failed compare ends the process at the return.

  2. The stack protector is on. Is the program safe from buffer overflows?

    No. It detects one consequence afterwards: every byte was still written, and the recorded guarded build wrote 36 bytes into a 16-byte buffer before the check fired. Overflows away from the guard word go unnoticed; only the length comparison in the code removes the bug.

  3. When does a library copy become a checked call, and where does it stop the program?

    When the compiler can see the size of the destination, as for a local array, it substitutes the checked form, which ends the process at the copy rather than at the return. A hand-written copy loop never becomes one.

  4. Which macro switches the checked library calls on, and what does each platform do by default?

    _FORTIFY_SOURCE. Apple's headers already give it the value 2; with the GNU C library you define it yourself, and the OpenSSF guide recommends -D_FORTIFY_SOURCE=3 with -O2 or higher.

  5. Which defences are fields in the file rather than instructions?

    PIE, the stack's permissions (non-executable on ELF through the GNU_STACK header) and the read-only relocation window. The linker writes them, so you read them from the headers of a file you never run.

  6. A hardening report shows every defence switched on. What does it prove?

    Only that the defences are on. None of the fields says the program has no memory bugs; each mitigation changes what a remaining bug can do, it does not remove it.

Key takeaways

  • A hardened build adds a guard word to functions with arrays or escaping locals, loaded from __stack_chk_guard (or from %fs:40 on x86-64), and compares it just before the return.
  • A library call whose destination size the compiler can see becomes a checked call, which ends the process at the copy instead of at the return; a hand-written loop never becomes one.
  • PIE, the stack’s permissions and the read-only relocation window are header fields the linker wrote, readable from a file you never run.
  • Every field in a hardening report says a defence is switched on, and none of them says the program has no memory bugs: the length comparison in the source is the only thing that removes the bug.

Terms in this lesson

hardened library checks
Checked forms of library functions that a compiler substitutes when it can see the size of the destination, such as a copy routine that also takes that size and ends the process when the data does not fit.
mitigation
A defence that changes what a remaining bug can do, rather than removing the bug: it may detect the damage, deny a step an attack needs, or make a result unpredictable to anyone outside the process.
non-executable stack
A stack mapping without execute permission, so instructions cannot be fetched from bytes stored there. On ELF the GNU_STACK program header carries the permissions.
read-only relocations
A link setting that has the loader apply the relocations in one window of memory and then write-protect that window, so the pointer tables in it cannot be changed later. Full protection also needs the relocations resolved before the program starts.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.