2

I am trying to create an ELF64 file from scratch; the program I'm using to test it right now just calls the exit() syscall using assembly; there's no linking of libraries etc. Here's the output of "dumpelf":

#include <elf.h>

/*
 * ELF dump of 'elftest'
 *     4120 (0x1018) bytes
 */

Elf64_Dyn dumpedelf_dyn_0[];
struct {
        Elf64_Ehdr ehdr;
        Elf64_Phdr phdrs[3];
        Elf64_Shdr shdrs[3];
        Elf64_Dyn *dyns;
} dumpedelf_0 = {

.ehdr = {
        .e_ident = { /* (EI_NIDENT bytes) */
                /* [0] EI_MAG:        */ 0x7F,'E','L','F',
                /* [4] EI_CLASS:      */ 2 , /* (ELFCLASS64) */
                /* [5] EI_DATA:       */ 1 , /* (ELFDATA2LSB) */
                /* [6] EI_VERSION:    */ 1 , /* (EV_CURRENT) */
                /* [7] EI_OSABI:      */ 0 , /* (ELFOSABI_NONE) */
                /* [8] EI_ABIVERSION: */ 0 ,
                /* [9-15] EI_PAD:     */ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
        },
        .e_type      = 2          , /* (ET_EXEC) */
        .e_machine   = 62         , /* (EM_X86_64) */
        .e_version   = 1          , /* (EV_CURRENT) */
        .e_entry     = 0x400000   , /* (start address at runtime) */
        .e_phoff     = 64         , /* (bytes into file) */
        .e_shoff     = 232        , /* (bytes into file) */
        .e_flags     = 0x0        ,
        .e_ehsize    = 64         , /* (bytes) */
        .e_phentsize = 56         , /* (bytes) */
        .e_phnum     = 3          , /* (program headers) */
        .e_shentsize = 64         , /* (bytes) */
        .e_shnum     = 3          , /* (section headers) */
        .e_shstrndx  = 1
},

.phdrs = {
/* Program Header #0 0x40 */
{
        .p_type   = 6          , /* [PT_PHDR] */
        .p_offset = 64         , /* (bytes into file) */
        .p_vaddr  = 0x40       , /* (virtual addr at runtime) */
        .p_paddr  = 0x40       , /* (physical addr at runtime) */
        .p_filesz = 168        , /* (bytes in file) */
        .p_memsz  = 168        , /* (bytes in mem at runtime) */
        .p_flags  = 0x4        , /* PF_R */
        .p_align  = 8          , /* (min mem alignment in bytes) */
},
/* Program Header #1 0x78 */ <-- PT_NULL=OK, PT_LOAD=SEGFAULT
{
        .p_type   = 1          , /* [PT_LOAD] */ 
        .p_offset = 0          , /* (bytes into file) */
        .p_vaddr  = 0x0        , /* (virtual addr at runtime) */
        .p_paddr  = 0x0        , /* (physical addr at runtime) */
        .p_filesz = 441        , /* (bytes in file) */
        .p_memsz  = 441        , /* (bytes in mem at runtime) */
        .p_flags  = 0x4        , /* PF_R */
        .p_align  = 4096       , /* (min mem alignment in bytes) */
},
/* Program Header #2 0xB0 */ <-- WORKS AS EXPECTED
{
        .p_type   = 1          , /* [PT_LOAD] */
        .p_offset = 4096       , /* (bytes into file) */
        .p_vaddr  = 0x400000   , /* (virtual addr at runtime) */
        .p_paddr  = 0x400000   , /* (physical addr at runtime) */
        .p_filesz = 24         , /* (bytes in file) */
        .p_memsz  = 24         , /* (bytes in mem at runtime) */
        .p_flags  = 0x5        , /* PF_R | PF_X */
        .p_align  = 4096       , /* (min mem alignment in bytes) */
},
},

.shdrs = {
/* Section Header #0 '' 0xE8 */
{
        .sh_name      = 0          ,
        .sh_type      = 0          , /* [SHT_NULL] */
        .sh_flags     = 0          ,
        .sh_addr      = 0x0        ,
        .sh_offset    = 0          , /* (bytes) */
        .sh_size      = 0          , /* (bytes) */
        .sh_link      = 0          ,
        .sh_info      = 0          ,
        .sh_addralign = 0          ,
        .sh_entsize   = 0
},
/* Section Header #1 '.shstrtab' 0x128 */
{
        .sh_name      = 1          ,
        .sh_type      = 3          , /* [SHT_STRTAB] */
        .sh_flags     = 32         ,
        .sh_addr      = 0x1A8      ,
        .sh_offset    = 424        , /* (bytes) */
        .sh_size      = 17         , /* (bytes) */
        .sh_link      = 0          ,
        .sh_info      = 0          ,
        .sh_addralign = 1          ,
        .sh_entsize   = 1
},
/* Section Header #2 '.text' 0x168 */
{
        .sh_name      = 11         ,
        .sh_type      = 1          , /* [SHT_PROGBITS] */
        .sh_flags     = 6          ,
        .sh_addr      = 0x400000   ,
        .sh_offset    = 4096       , /* (bytes) */
        .sh_size      = 24         , /* (bytes) */
        .sh_link      = 0          ,
        .sh_info      = 0          ,
        .sh_addralign = 16         ,
        .sh_entsize   = 0
},
},

.dyns = dumpedelf_dyn_0,
};
Elf64_Dyn dumpedelf_dyn_0[] = {
 /* no dynamic tags ! */ };

If I try to execute the program in x86_64 Linux, the program segfaults. Attempting to debug it in gdb is literally a non-starter, GDB tells me the program segfaults "during startup" so no help there.

After several days of beating my head against this problem, I can not for the life of me figure out why this is causing a segfault; I have looked at numerous other executable files including system binaries and files compiled with various different programs, they all load the ELF headers at vaddr=0x0 without problems so why does it fail in this case?

If I change Program Header #1 0x78 from PT_LOAD to PT_NULL to not load the ELF64 headers into memory, the program runs as expected and exits without error so I know the executable part is OK. This narrows the problem down to that header but it isn't a workable solution.

Edit: Here's the same file as viewed with readelf -a:

  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x400000
  Start of program headers:          64 (bytes into file)
  Start of section headers:          232 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         3
  Size of section headers:           64 (bytes)
  Number of section headers:         3
  Section header string table index: 1

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .shstrtab         STRTAB           00000000000001a8  000001a8
       0000000000000011  0000000000000001   S       0     0     1
  [ 2] .text             PROGBITS         0000000000400000  00001000
       0000000000000018  0000000000000000  AX       0     0     16
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  D (mbind), l (large), p (processor specific)

There are no section groups in this file.

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000000040 0x0000000000000040
                 0x00000000000000a8 0x00000000000000a8  R      0x8
  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x00000000000001b9 0x00000000000001b9  R      0x1000
  LOAD           0x0000000000001000 0x0000000000400000 0x0000000000400000
                 0x0000000000000018 0x0000000000000018  R E    0x1000

 Section to Segment mapping:
  Segment Sections...
   00
   01
   02     .text

There is no dynamic section in this file.

There are no relocations in this file.
No processor specific unwind information to decode

No version information found in this file.

Edit #2: Added output of strace as both ordinary user, and as root which surprisingly works:

$ strace ./filename
execve("./filename", ["./filename"], 0x7ffd660b1910 /* 28 vars */) = -1 EPERM (Operation not permitted)
+++ killed by SIGSEGV +++
Segmentation fault

(as root)
# strace ./filename
execve("./filename", ["./filename"], 0x7ffd06138b90 /* 27 vars */) = 0
exit(0)                                 = ?
+++ exited with 0 +++

$ stat filename
  File: filename
  Size: 4120            Blocks: 16         IO Block: 4096   regular file
Device: 820h/2080d      Inode: 1920        Links: 1
Access: (0755/-rwxr-xr-x)  Uid: ( 1000/ username)   Gid: ( 1000/ username)
Access: 2023-06-15 13:24:24.007538545 +0200
Modify: 2023-06-15 13:24:16.787538566 +0200
Change: 2023-06-15 13:24:16.787538566 +0200
 Birth: 2023-06-15 13:24:16.787538566 +0200
  • 1
    FYI, a bad executable can segfault inside the `execve` system call if enough of the old process has already been replaced that the kernel can't just return to it with an error code when it discovers a problem in the ELF metadata. GDB can only debug user-space using the `ptrace` system call, so it can fork / PTRACE_TRACEME, but then yeah, all it will see is a segfault inside `execve` for that child process. So that's the kind of problem you're looking for, e.g. that can be caused by `vaddr - file_offset % 4096 != 0` for a segment, so the kernel couldn't mmap the file pages into memory. – Peter Cordes Jun 15 '23 at 07:46
  • Does `readelf -a` show the metadata you expected? – Peter Cordes Jun 15 '23 at 07:48
  • Yes, `readelf -a` shows the same as `dumpelf`, I can't show them all because of the size limit but the relevant program header shows the correct offset/vaddr/paddr=0 and file/mem size=0x1b9. – Andreas Lund Jun 15 '23 at 07:59
  • 1
    I was facing similar problem: the dynamic loader crashed on loading my handcrafted DSO. Try `strace ./executable` and watch if all `mmap` and `mprotext` ended before the segfault occured. – vitsoft Jun 15 '23 at 10:38
  • This is interesting, strace immediately goes `execve("./filename", ["./filename"], 0x7ffd660b1910 /* 28 vars */) = -1 EPERM (Operation not permitted)` (and then it segfaults). So I tried running as root and that works. I still don't understand why, the file is plain mode=0755, but this is progress. – Andreas Lund Jun 15 '23 at 11:45
  • 1
    `EPERM` is returned by `execve` if the file format is not valid. BTW, where does the number 441 for the size of the second program header come from? 3*56 = 168 – Margaret Bloom Jun 15 '23 at 16:59
  • The second program header (which is the one causing all the problems) is trying to load the ELF headers, including the strings table) into memory. Now, this may just be me confusing myself, but looking at other binaries this seems to be a pretty common thing to do and I was assuming it was a necessary step somewhere down the road. – Andreas Lund Jun 15 '23 at 17:42

1 Answers1

4

After several days of beating my head against this problem, I can not for the life of me figure out why this is causing a segfault

It's not actually causing a SIGSEGV. Rather, the kernel looks at your binary and says "no can't do", and sends it a "kill with fire" signal.

The reason is the same as this answer: you have an ET_EXEC file (which must be loaded at the linked-at address, which you have as 0). But the kernel will not load anything below 0x10000 (there is some kernel constant which defines the lowest allowed address, I haven't tracked it down).

You can change the binary to ET_DYN (making it a PIE binary; which can be loaded at arbitrary location), or you can link that ET_EXEC at address 0x10000 or above, and it would start working.

Employed Russian
  • 199,314
  • 34
  • 295
  • 362
  • This explanation does make a lot of sense, but when I point `readelf` at common binaries like `/bin/ls` they all seem to load stuff at 0x0 don't they? This is why I assumed that it was not only allowed but somehow needed for dynamic linking etc to work. Mystery half solved I guess. – Andreas Lund Jun 15 '23 at 17:52
  • 1
    @AndreasLund The binaries which are linked at `0x0` are all `ET_DYN`s, not `ET_EXEC`s. – Employed Russian Jun 15 '23 at 19:02
  • 1
    `0x10000` is `vm.mmap_min_addr` (https://wiki.debian.org/mmap_min_addr) so the kernel constant would be an initializer for that sysctl setting. If you change that setting to `0`, you can probably run your original ET_EXEC binary that wants to map itself at address `0`, @AndreasLund – Peter Cordes Jun 15 '23 at 20:44