openlang

-fstack-protector-strong with no libc, segfault on a %fs load at 0x28

Freestanding hex dumper, x86-64 Linux, own _start, raw syscalls, linked with -nostdlib -static -no-pie. It builds clean with no undefined symbols, but ./hexd -v < /etc/hostname segfaults before the first note() output.

Under gdb the fault is at the very top of hexd_main, on a load through %fs that I didn't write, fault address 0x28. Nothing else has run. With -fno-stack-protector it's fine.

I defined the two symbols I thought the protector wanted:

unsigned long __stack_chk_guard = 0x595e9fbd94fda766UL;

__attribute__((noreturn))
void __stack_chk_fail(void) { /* write to fd 2, sys_exit(134) */ }
CFLAGS = -std=c11 -O2 -Wall -Wextra -ffreestanding -fno-pic -fno-asynchronous-unwind-tables -fstack-protector-strong

Why is the compiler reading %fs at all, when the guard is sitting right there as a plain global? Stack alignment in _start is right, and -mno-red-zone changed nothing. Do I have to set up the thread pointer myself, with prctl and ARCH_SET_FS, before calling anything?

The conversation continues 6

Follow the reasoning
  1. Permalink

    Beginner here (so grain of salt), but I thought the default guard location was the plain global and TLS was the opt-in? Does -mstack-protector-guard=global make your __stack_chk_guard get used instead of %fs:0x28?

  2. Permalink

    Kiln, you've got it the wrong way round for x86-64 Linux. The default is the TLS canary, read from %fs:0x28, and the plain global is the thing you have to ask for. That also explains my fault address exactly: _start never sets the FS base, so it's zero and the load lands on 0x28. My __stack_chk_guard was never going to be read, it's just a symbol sitting there.

    There are two ways out, and I'm going with the first.

    1. Tell the compiler to use the global, with -mstack-protector-guard=global, and on top of that -mstack-protector-guard-symbol=__stack_chk_guard so it names mine. Nothing at runtime needs setting up.
    2. Leave the default and do it by hand: reserve a zeroed block of 64 bytes or so, put the canary at offset 0x28, point FS at it with prctl(ARCH_SET_FS, ...) before the first protected function runs. _start itself has to be compiled without the protector, or it faults before it gets there.

    The second is more faithful to what glibc does, but for a hex dumper it's a lot of ceremony for one 8-byte value.

  3. Permalink

    Ok, so the FS base is zero, which makes sense now (cheers). Why 0x28 specifically, is that just where glibc's thread header keeps it?

  4. Permalink

    Pretty much, yeah. On x86-64 glibc, %fs points at the thread control block, and the start of that struct goes roughly like this:

    • 0x00: tcb pointer (points at itself)
    • 0x08: dtv
    • 0x10: self pointer
    • 0x18: multiple_threads (int), then gscope_flag (int) at 0x1c
    • 0x20: sysinfo
    • 0x28: stack_guard

    So 0x28 is just where the canary landed in that layout. GCC hardcodes %fs:0x28 because the x86-64 ABI docs pin it down, as I remember it, which is why every libc has to put it there. That's also why lathe's second option works: all you need is a block with 8 bytes at 0x28 and FS pointing at it, so nothing else in the header matters.

    Thanks for asking, by the way, I had to look at the struct again myself to be sure of the order. The other offsets only matter if something reads them, though.

  5. Permalink

    on a load through %fs that I didn't write, fault address 0x28

    Got this exact crash in my uni project too.

  6. Permalink

    Following up on my own fix, since there's a catch with option 1 that I skipped over.

    It stops the %fs load, but the canary is now whatever constant sits in the source, so it's identical on every run and in every binary. For a hex dumper that's harmless, but it isn't much of a protector. If you want it to be real, the kernel hands you 16 random bytes at AT_RANDOM in the auxiliary vector. In _start, step past argv and the envp NULL, walk the auxv pairs until you hit type 25, and copy 8 bytes from that pointer into __stack_chk_guard before calling hexd_main. _start has to stay unprotected, as with option 2, and the copy must happen before any protected function is entered, otherwise the prologue stores the old value and the epilogue compares against the new one, which fails.

    To check the compiler has really stopped touching FS:

    objdump -d hexd | grep '%fs'

    That should come back empty. If something still shows up, it's usually an object built without the guard flags, so check that every file in the link gets the same CFLAGS.

    On option 2, the syscall number you want for the FS base is 158 on x86-64.

Other sides of the same question

  1. STM32H7 UART TX DMA sends old bytes when D-cache is on

    STM32H7, arm-none-eabi-gcc. DMA TX of a 64 byte frame. Cache off: fine. With -DUSE_DCACHE, the analyzer shows wrong frames. T…

    Replies 8Latest activity c
  2. Callbacks from C lose the arena I set up

    A C library calls back into my Odin code, and the callback can't see the context I set up before registering it. A proc "c" g…

    Replies 5Latest activity odinc
  3. TMR3 ISR loses capture events although flags are w1c and cleared

    Reflow oven controller, Cortex-M0 clone, arm-none-eabi-gcc -O2, Fedora. TMR3 runs at 1 MHz. Update event is the 1 ms tick, CH…

    Replies 5Latest activity c