Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions ebpftracer/elf.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"debug/elf"
"fmt"
"io"
"os"

"github.com/cilium/ebpf"
"github.com/cilium/ebpf/link"
Expand Down Expand Up @@ -116,7 +117,10 @@ func (s *Symbol) AttachUretprobes(exe *link.Executable, prog *ebpf.Program, pid
}

type ELFFile struct {
path string
path string
// file is the open binary. Everything is read through it: reopening path,
// a /proc/<pid>/exe link, fails once that process has exited.
file *os.File
elf *elf.File
symbols []elf.Symbol
textSection *elf.Section
Expand All @@ -126,11 +130,16 @@ type ELFFile struct {
}

func OpenELFFile(path string) (*ELFFile, error) {
file, err := elf.Open(path)
file, err := os.Open(path)
if err != nil {
return nil, err
}
return &ELFFile{path: path, elf: file}, nil
ef, err := elf.NewFile(file)
if err != nil {
file.Close()
return nil, err
}
return &ELFFile{path: path, file: file, elf: ef}, nil
}

func (f *ELFFile) readSymbols() error {
Expand Down Expand Up @@ -176,7 +185,7 @@ func (f *ELFFile) GetSymbol(name string) (*Symbol, error) {
// cannot be read.
func (f *ELFFile) goFuncTable() *goFuncTable {
if f.goFuncs == nil && f.goFuncsErr == nil {
f.goFuncs, f.goFuncsErr = openGoFuncTable(f.path, f.elf)
f.goFuncs, f.goFuncsErr = openGoFuncTable(f.file, f.elf)
}
return f.goFuncs
}
Expand All @@ -196,7 +205,8 @@ func (f *ELFFile) Close() error {
if f.goFuncs != nil {
f.goFuncs.close()
}
return f.elf.Close()
// elf.File.Close does nothing for a file made with elf.NewFile.
return f.file.Close()
}

// stackCheckWindow bounds the prologue scanned for the stack check: at most
Expand Down
8 changes: 2 additions & 6 deletions ebpftracer/gopclntab.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ type goFuncTable struct {
textEnd uint64 // end of .text; a function must lie in [textStart, textEnd)
}

func openGoFuncTable(path string, ef *elf.File) (*goFuncTable, error) {
func openGoFuncTable(file *os.File, ef *elf.File) (*goFuncTable, error) {
sec := ef.Section(".gopclntab")
if sec == nil {
// PIE binaries place it in the relocated read-only data.
Expand All @@ -49,11 +49,7 @@ func openGoFuncTable(path string, ef *elf.File) (*goFuncTable, error) {
return nil, errNoGoFuncTable
}

file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
// The mapping outlives file: closing the descriptor does not unmap it.
info, err := file.Stat()
if err != nil {
return nil, err
Expand Down
44 changes: 44 additions & 0 deletions ebpftracer/gopclntab_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,3 +241,47 @@ func TestReturnOffsets_RejectsSymbolOutsideText(t *testing.T) {
}
}
}

// A stripped Go binary's symbols come only from .gopclntab. It used to be read
// by reopening the path, a /proc/<pid>/exe link for a process: when the
// process exited between the ELF open and that reopen, its TLS functions were
// "not found", the binary was cached as having none, and every later process
// of it went unprobed. The table is now read through the file already open.
func TestGetSymbol_StrippedGoBinaryAfterPathIsGone(t *testing.T) {
if testing.Short() {
t.Skip("builds Go binaries")
}
goBin, err := exec.LookPath("go")
if err != nil {
t.Skip("go toolchain not found")
}
src := t.TempDir()
if err := os.WriteFile(filepath.Join(src, "go.mod"), []byte("module tlsprobe\n\ngo 1.21\n"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(src, "main.go"), []byte(gopclntabTestProgram), 0o644); err != nil {
t.Fatal(err)
}
bin := filepath.Join(t.TempDir(), "stripped")
cmd := exec.Command(goBin, "build", "-ldflags=-s -w", "-o", bin, ".")
cmd.Dir = src
// An ELF binary whatever the host builds natively.
cmd.Env = append(os.Environ(), "CGO_ENABLED=0", "GOOS=linux")
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("go build: %v\n%s", err, out)
}

ef, err := OpenELFFile(bin)
if err != nil {
t.Fatal(err)
}
defer ef.Close()
if err := os.Remove(bin); err != nil { // the process exits
t.Fatal(err)
}
for _, name := range tlsFuncs {
if _, err := ef.GetSymbol(name); err != nil {
t.Errorf("%s: %v", name, err)
}
}
}
7 changes: 7 additions & 0 deletions ebpftracer/symbol_cache.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ebpftracer

import (
"errors"
"fmt"
"os"
"sync"
Expand Down Expand Up @@ -150,6 +151,12 @@ func readProbeTargets(path string, names []string) (map[string]ProbeTarget, erro
for _, name := range names {
s, err := ef.GetSymbol(name)
if err != nil {
// "Not found" is cached for the binary, so it must mean the
// binary lacks the symbol, not that its function table (the only
// symbol source of a stripped Go binary) could not be read.
if ef.goFuncsErr != nil && !errors.Is(ef.goFuncsErr, errNoGoFuncTable) {
return nil, ef.goFuncsErr
}
targets[name] = ProbeTarget{}
continue
}
Expand Down
Loading