1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
|
runtime: 0.422
instruction: 0.385
syscall: 0.192
elf-linux-user: segfault caused by invalid loaddr extracted by the ELF loader
Description of problem:
Emulating ELF binaries as emitted by Zig may lead to segfault in QEMU, which typically looks like this
```
$ qemu-x86_64 simple
fish: Job 1, 'qemu-x86_64 simple' terminated by signal SIGSEGV (Address boundary error)
```
Steps to reproduce:
1. Obtain latest Zig nightly
2. Compile simple static C program using Zig's ELF linker:
```
$ echo "int main() { return 0 };" > simple.c
$ zig build-exe simple.c -lc -target x86_64-linux-musl -fno-lld --image-base 0x1000000
$ qemu-x86_64 simple
fish: Job 1, 'qemu-x86_64 simple' terminated by signal SIGSEGV (Address boundary error)
```
Additional information:
Note that running `simple` directly it's correctly mmaped and executed by the kernel:
```
$ ./simple
$ echo $status
0
```
The reason this happens is because of an assumption QEMU's ELF loader makes on the virtual addresses and offsets of `PT_LOAD` segments, namely:
```
vaddr2 - vaddr1 >= off2 - off1
```
Typically, to the best of my knowledge, this is conformed to by the linkers in the large, but it is not required at all. Here's a one-line tweak to QEMU's loader that fixes the segfault:
```diff
diff --git a/linux-user/elfload.c b/linux-user/elfload.c
index f21e2e0c3d..eabb4fed03 100644
--- a/linux-user/elfload.c
+++ b/linux-user/elfload.c
@@ -3211,7 +3211,7 @@ static void load_elf_image(const char *image_name, int image_fd,
for (i = 0; i < ehdr->e_phnum; ++i) {
struct elf_phdr *eppnt = phdr + i;
if (eppnt->p_type == PT_LOAD) {
- abi_ulong a = eppnt->p_vaddr - eppnt->p_offset;
+ abi_ulong a = eppnt->p_vaddr & ~(eppnt->p_align - 1);
if (a < loaddr) {
loaddr = a;
}
```
The reason why this breaks for ELF binaries emitted by Zig is that while virtual addresses are allocated sequentially or pre-allocated, file offsets are allocated on a best-effort basis wherever there is enough space in the file to fit a given section/segment so that we can move the contents in file while preserving the allocated virtual addresses on a whim. To provide a more concrete example, here's the load segment layout for `simple` as emitted by Zig:
```
$ readelf -l simple
Elf file type is EXEC (Executable file)
Entry point 0x1002000
There are 7 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000001000040 0x0000000001000040
0x0000000000000188 0x0000000000000188 R 0x8
LOAD 0x0000000000000000 0x0000000001000000 0x0000000001000000
0x00000000000001c8 0x00000000000001c8 R 0x1000
LOAD 0x0000000000021000 0x0000000001001000 0x0000000001001000
0x0000000000000078 0x0000000000000078 R 0x1000
LOAD 0x0000000000022000 0x0000000001002000 0x0000000001002000
0x000000000000065a 0x000000000000065a R E 0x1000
LOAD 0x0000000000023000 0x0000000001003000 0x0000000001003000
0x0000000000000060 0x0000000000000278 RW 0x1000
GNU_EH_FRAME 0x0000000000021064 0x0000000001001064 0x0000000001001064
0x0000000000000014 0x0000000000000014 R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x1
Section to Segment mapping:
Segment Sections...
00
01
02 .rodata.str1.1 .rodata .eh_frame .eh_frame_hdr
03 .text .init .fini
04 .data .got .bss
05 .eh_frame_hdr
06
```
As you can see, initially `loaddr := 0x1000000 - 0x0 = 0x1000000`. However, upon iterating over the second load segment, we already get
```
a := 0x1001000 - 0x21000 = 0xfe000
```
and since `a < loaddr`, we incorrectly set `loaddr := 0xfe000`.
|