internal/gocore: support PIE ELF binaries
Go binaries built with `-buildmode=pie` are loaded at random addresses
(ASLR) in memory by Linux.
Before this change, the mappings read from the `NT_NOTE` section of the
ELF binary matches the exe mappings:
# From NT_NOTE:
0x400000-0x491000 ---- 593920 /tmp/TestVersionsgoroot#003207751013/001/test.exe+0x0
0x491000-0x54b000 ---- 761856 /tmp/TestVersionsgoroot#003207751013/001/test.exe+0x91000
0x54b000-0x556000 ---- 45056 /tmp/TestVersionsgoroot#003207751013/001/test.exe+0x14b000
# From executable:
0x400000-0x491000 r-xp 593920 /tmp/TestVersionsgoroot#003207751013/001/test.exe+0x0
0x491000-0x54b000 r--p 761856 /tmp/TestVersionsgoroot#003207751013/001/test.exe+0x91000
0x54b000-0x555000 rw-p 40960 /tmp/TestVersionsgoroot#003207751013/001/test.exe+0x14b000
0x555000-0x579000 rw-p 147456 anon+0x-440
# From core
0xc000000000-0xc000400000 rw-p 4194304 /tmp/TestVersionsgoroot#003207751013/001/core+0x32000
0x7f8b17c00000-0x7f8b19c00000 rw-p 33554432 /tmp/TestVersionsgoroot#003207751013/001/core+0x432000
0x7f8b29d80000-0x7f8b29d81000 rw-p 4096 /tmp/TestVersionsgoroot#003207751013/001/core+0x2432000
0x7f8b49d80000-0x7f8b49d81000 rw-p 4096 /tmp/TestVersionsgoroot#003207751013/001/core+0x2433000
0x7f8b5bc30000-0x7f8b5bc31000 rw-p 4096 /tmp/TestVersionsgoroot#003207751013/001/core+0x2434000
0x7f8b5e006000-0x7f8b5e007000 rw-p 4096 /tmp/TestVersionsgoroot#003207751013/001/core+0x2435000
0x7f8b5e466000-0x7f8b5e4c6000 rw-p 393216 /tmp/TestVersionsgoroot#003207751013/001/core+0x2436000
0x7f8b5e4c6000-0x7f8b5e5c6000 rw-p 1048576 /tmp/TestVersionsgoroot#003207751013/001/core+0x2496000
0x7f8b5e5c6000-0x7f8b5e5d7000 rw-p 69632 /tmp/TestVersionsgoroot#003207751013/001/core+0x2596000
0x7f8b5e657000-0x7f8b5e658000 rw-p 4096 /tmp/TestVersionsgoroot#003207751013/001/core+0x25a7000
0x7f8b5e6d7000-0x7f8b5e737000 rw-p 393216 /tmp/TestVersionsgoroot#003207751013/001/core+0x25a8000
0x7f8b5e737000-0x7f8b5e73b000 r--p 16384 /tmp/TestVersionsgoroot#003207751013/001/core+0x2608000
0x7f8b5e73b000-0x7f8b5e73d000 r-xp 8192 /tmp/TestVersionsgoroot#003207751013/001/core+0x260c000
0x7ffd88d3c000-0x7ffd88d5e000 rw-p 139264 /tmp/TestVersionsgoroot#003207751013/001/core+0x260e000
And things worked fine. But, with PIE binaries, we see the following raw
addresses when loading (e.g.):
# From NT_NOTE:
0x563f53d35000-0x563f53dc7000 ---- 598016 /tmp/TestVersionsgoroot-buildmode=pie3327190927/001/test.exe+0x0
0x563f53dc7000-0x563f53e10000 ---- 299008 /tmp/TestVersionsgoroot-buildmode=pie3327190927/001/test.exe+0x92000
0x563f53e10000-0x563f53ea3000 ---- 602112 /tmp/TestVersionsgoroot-buildmode=pie3327190927/001/test.exe+0xdb000
0x563f53ea3000-0x563f53eaf000 ---- 49152 /tmp/TestVersionsgoroot-buildmode=pie3327190927/001/test.exe+0x16e000
0x7ff5d0deb000-0x7ff5d0dec000 ---- 4096 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2+0x0
0x7ff5d0dec000-0x7ff5d0e11000 ---- 151552 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2+0x1000
0x7ff5d0e11000-0x7ff5d0e1b000 ---- 40960 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2+0x26000
0x7ff5d0e1b000-0x7ff5d0e1f000 ---- 16384 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2+0x30000
# From executable:
0x400000-0x492000 r-xp 598016 /tmp/TestVersionsgoroot-buildmode=pie3327190927/001/test.exe+0x0
0x492000-0x4db000 r--p 299008 /tmp/TestVersionsgoroot-buildmode=pie3327190927/001/test.exe+0x92000
0x4db000-0x56f000 rw-p 606208 /tmp/TestVersionsgoroot-buildmode=pie3327190927/001/test.exe+0xdb000
0x56f000-0x579000 rw-p 40960 /tmp/TestVersionsgoroot-buildmode=pie3327190927/001/test.exe+0x16f000
0x579000-0x59d000 rw-p 147456 anon+0x-460
# From core
0xc000000000-0xc000400000 rw-p 4194304 /tmp/TestVersionsgoroot-buildmode=pie3327190927/001/core+0x3000
0x563f53d35000-0x563f53d36000 r-xp 4096 /tmp/TestVersionsgoroot-buildmode=pie3327190927/001/core+0x403000
0x563f53d36000-0x563f53dc7000 r-xp 593920 anon+0x0
0x563f53dc7000-0x563f53e10000 r--p 299008 anon+0x0
0x563f53e10000-0x563f53ea3000 r--p 602112 /tmp/TestVersionsgoroot-buildmode=pie3327190927/001/core+0x404000
0x563f53ea3000-0x563f53eaf000 rw-p 49152 /tmp/TestVersionsgoroot-buildmode=pie3327190927/001/core+0x497000
0x563f53eaf000-0x563f53ed2000 rw-p 143360 /tmp/TestVersionsgoroot-buildmode=pie3327190927/001/core+0x4a3000
...
0x7ff5d0deb000-0x7ff5d0dec000 r--p 4096 /tmp/TestVersionsgoroot-buildmode=pie3327190927/001/core+0x26a4000
0x7ff5d0dec000-0x7ff5d0e11000 r-xp 151552 anon+0x0
0x7ff5d0e11000-0x7ff5d0e1b000 r--p 40960 anon+0x0
0x7ff5d0e1b000-0x7ff5d0e1f000 rw-p 16384 /tmp/TestVersionsgoroot-buildmode=pie3327190927/001/core+0x26a5000
0x7fff34a26000-0x7fff34a48000 rw-p 139264 /tmp/TestVersionsgoroot-buildmode=pie3327190927/001/core+0x26a9000
This caused two issues:
1. The mappings from the executable and the core were not unified
properly done in `addProgMappings -> splicedMemory.Add` due to the
address ranges being different. Both existeded in the
`splicedMemory` set. Normally, the core mappings would override the
executable mappings when they overlap. To correct for this, add the
load offset (`staticBase`) to the executable addresses.
2. DWARF data (contained in the executable) references the base
addresses (for example, `allgs` is at 0x4003c4). Due to #1, a lookup
*would not crash*, but it would read the location from the
executable. In a PIE binary, such a lookup into the `.data` section
would be a relocation, and be present in the binary as all zeroes
(0x0). If #1 were fixed to offset by `staticBase` without fixing #2
would result in a read from unmapped memory.
Inspiration was taken from the Delve commit that added PIE support for
multiple platforms/architectures in
https://github.com/go-delve/delve/commit/025d47c6e96e8ab5a2d2c142ae554e760cddabf8.
Also add tests that this works. I verified that if the changes from
non-test files are omitted, the test fails.
Change-Id: Ifa08f71e7ed22320b78bc6015554e997b8ae521d
Reviewed-on: https://go-review.googlesource.com/c/debug/+/618977
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Nicolas Hillegeer <aktau@google.com>
Reviewed-by: Michael Knyszek <mknyszek@google.com>
diff --git a/internal/core/process.go b/internal/core/process.go
index 7890be5..74400e4 100644
--- a/internal/core/process.go
+++ b/internal/core/process.go
@@ -40,6 +40,7 @@
meta metadata // basic metadata about the core
entryPoint Address
+ staticBase uint64 // Offset at which the executable was loaded in memory. 0 when binary is not-PIE.
args string // first part of args retrieved from NT_PRPSINFO
threads []*Thread // os threads (TODO: map from pid?)
@@ -183,6 +184,12 @@
return p.syms, p.symErr
}
+// StaticBase returns the offset at which the main executable was loaded in
+// memory. For example, it should be used when dereferencing DWARF locations.
+func (p *Process) StaticBase() uint64 {
+ return p.staticBase
+}
+
var mapFile = func(fd int, offset int64, length int) (data []byte, err error) {
return nil, fmt.Errorf("file mapping is not implemented yet")
}
@@ -241,11 +248,16 @@
return nil, fmt.Errorf("failed to parse executable: %v", err)
}
+ staticBase := uint64(entryPoint) - exeElf.Entry // If not PIE, this is 0.
+ if exeElf.Entry > uint64(entryPoint) {
+ return nil, fmt.Errorf("malformed binary or core, core entry point (%d) - executable entry point (%d) is < 0", entryPoint, exeElf.Entry)
+ }
+
// The base memory layout is defined by the binary itself. Additional
// mappings from the core layer on top. This ordering is important to
// ensure that dirty data/bss pages from the core take priority over
// the initial state from the binary.
- mem := readExecMappings(exeFile, exeElf)
+ mem := readExecMappings(exeFile, exeElf, staticBase)
addCoreMappings(&mem, coreFile, coreElf)
// Add os.File references to mappings of files.
warnings := updateMappingFiles(&mem, fileMappings, base, exeFile, origExePath)
@@ -342,6 +354,7 @@
p := &Process{
meta: meta,
entryPoint: entryPoint,
+ staticBase: staticBase,
args: args,
threads: threads,
memory: mem,
@@ -357,13 +370,14 @@
}
// readExecMappings returns the memory mappings defined by the executable
-// itself.
-func readExecMappings(exeFile *os.File, exeElf *elf.File) splicedMemory {
+// itself. staticBase should be the offset at which the executable was loaded in
+// memory.
+func readExecMappings(exeFile *os.File, exeElf *elf.File, staticBase uint64) splicedMemory {
// Load virtual memory mappings.
var mem splicedMemory
for _, prog := range exeElf.Progs {
if prog.Type == elf.PT_LOAD {
- addProgMappings(&mem, prog, exeFile)
+ addProgMappings(&mem, prog, exeFile, staticBase)
}
}
return mem
@@ -373,14 +387,18 @@
func addCoreMappings(mem *splicedMemory, coreFile *os.File, coreElf *elf.File) {
for _, prog := range coreElf.Progs {
if prog.Type == elf.PT_LOAD {
- addProgMappings(mem, prog, coreFile)
+ addProgMappings(mem, prog, coreFile, 0)
}
}
}
// addProgMappings adds memory mappings for prog (from file f) to mem.
-func addProgMappings(mem *splicedMemory, prog *elf.Prog, f *os.File) {
+// staticBase is added to the p_vaddr [1].
+//
+// [1]: https://man7.org/linux/man-pages/man5/elf.5.html
+func addProgMappings(mem *splicedMemory, prog *elf.Prog, f *os.File, staticBase uint64) {
min := Address(prog.Vaddr)
+ min = min.Add(int64(staticBase))
max := min.Add(int64(prog.Memsz))
var perm Perm
if prog.Flags&elf.PF_R != 0 {
diff --git a/internal/gocore/dwarf.go b/internal/gocore/dwarf.go
index 967531a..e8f9497 100644
--- a/internal/gocore/dwarf.go
+++ b/internal/gocore/dwarf.go
@@ -455,6 +455,7 @@
} else {
a = core.Address(p.proc.ByteOrder().Uint32(loc[1:]))
}
+ a = a.Add(int64(p.proc.StaticBase()))
if !p.proc.Writeable(a) {
// Read-only globals can't have heap pointers.
// TODO: keep roots around anyway?
diff --git a/internal/gocore/gocore_test.go b/internal/gocore/gocore_test.go
index 91f69c1..8c8c58e 100644
--- a/internal/gocore/gocore_test.go
+++ b/internal/gocore/gocore_test.go
@@ -85,7 +85,7 @@
// loadExampleGenerated generates a core from a binary built with
// runtime.GOROOT().
-func loadExampleGenerated(t *testing.T) *Process {
+func loadExampleGenerated(t *testing.T, buildFlags ...string) *Process {
t.Helper()
testenv.MustHaveGoBuild(t)
switch runtime.GOOS {
@@ -100,7 +100,7 @@
defer cleanup()
dir := t.TempDir()
- file, output, err := generateCore(dir)
+ file, output, err := generateCore(dir, buildFlags...)
t.Logf("crasher output: %s", output)
if err != nil {
t.Fatalf("generateCore() got err %v want nil", err)
@@ -157,7 +157,7 @@
}
}
-func generateCore(dir string) (string, []byte, error) {
+func generateCore(dir string, buildFlags ...string) (string, []byte, error) {
goTool, err := testenv.GoTool()
if err != nil {
return "", nil, fmt.Errorf("cannot find go tool: %w", err)
@@ -170,7 +170,10 @@
}
srcPath := filepath.Join(cwd, source)
- cmd := exec.Command(goTool, "build", "-o", "test.exe", srcPath)
+ argv := []string{"build"}
+ argv = append(argv, buildFlags...)
+ argv = append(argv, "-o", "test.exe", srcPath)
+ cmd := exec.Command(goTool, argv...)
cmd.Dir = dir
b, err := cmd.CombinedOutput()
@@ -443,15 +446,22 @@
}
t.Run("goroot", func(t *testing.T) {
- p := loadExampleGenerated(t)
- checkProcess(t, p)
+ for _, buildFlags := range [][]string{
+ nil,
+ {"-buildmode=pie"},
+ } {
+ t.Run(strings.Join(buildFlags, ","), func(t *testing.T) {
+ p := loadExampleGenerated(t, buildFlags...)
+ checkProcess(t, p)
- // TODO(aktau): Move sanityCheckDominator into sanityCheckProcess once this
- // passes for loadExampleGenerated.
- t.Skip(`skipping dominator check due to "panic: can't find type runtime.itab"`)
- lt := runLT(p)
- if !checkDominator(t, lt) {
- t.Errorf("sanityCheckDominator(...) = false, want true")
+ // TODO(aktau): Move checkDominator into checkProcess once this passes
+ // for loadExampleGenerated.
+ t.Skip(`skipping dominator check due to "panic: can't find type runtime.itab"`)
+ lt := runLT(p)
+ if !checkDominator(t, lt) {
+ t.Errorf("checkDominator(...) = false, want true")
+ }
+ })
}
})
}