| // Copyright 2018 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package elflib |
| |
| import ( |
| "bufio" |
| "bytes" |
| "debug/elf" |
| "encoding/binary" |
| "encoding/hex" |
| "fmt" |
| "io" |
| "os" |
| "path/filepath" |
| "strings" |
| ) |
| |
| const NT_GNU_BUILD_ID uint32 = 3 |
| |
| // The file suffix used by unstripped debug binaries. |
| const DebugFileSuffix = ".debug" |
| |
| // BinaryFileRef represents a reference to an ELF file. The build id |
| // and filepath are stored here. BinaryFileRefs can verify that their |
| // build id matches their contents. |
| type BinaryFileRef struct { |
| BuildID string |
| Filepath string |
| } |
| |
| func NewBinaryFileRef(filepath, build string) BinaryFileRef { |
| return BinaryFileRef{BuildID: build, Filepath: filepath} |
| } |
| |
| type buildIDError struct { |
| err error |
| filename string |
| } |
| |
| func newBuildIDError(err error, filename string) *buildIDError { |
| return &buildIDError{err: err, filename: filename} |
| } |
| |
| func (b buildIDError) Error() string { |
| return fmt.Sprintf("error reading %s: %v", b.filename, b.err) |
| } |
| |
| // Verify verifies that the build id of b matches the build id found in the file. |
| func (b BinaryFileRef) Verify() error { |
| file, err := os.Open(b.Filepath) |
| if err != nil { |
| return newBuildIDError(err, b.Filepath) |
| } |
| defer file.Close() |
| buildIDs, err := GetBuildIDs(b.Filepath, file) |
| if err != nil { |
| return newBuildIDError(err, b.Filepath) |
| } |
| binBuild, err := hex.DecodeString(b.BuildID) |
| if err != nil { |
| return newBuildIDError(fmt.Errorf("build ID `%s` is not a hex string: %v", b.BuildID, err), b.Filepath) |
| } |
| for _, buildID := range buildIDs { |
| if bytes.Equal(buildID, binBuild) { |
| return nil |
| } |
| } |
| return newBuildIDError(fmt.Errorf("build ID `%s` could not be found", b.BuildID), b.Filepath) |
| } |
| |
| // rounds 'x' up to the next 'to' aligned value |
| func alignTo(x, to uint32) uint32 { |
| return (x + to - 1) & -to |
| } |
| |
| type noteEntry struct { |
| noteType uint32 |
| name string |
| desc []byte |
| } |
| |
| func forEachNote(note []byte, endian binary.ByteOrder, entryFn func(noteEntry)) error { |
| for { |
| var out noteEntry |
| // If there isn't enough to parse set n to nil. |
| if len(note) < 12 { |
| return nil |
| } |
| namesz := endian.Uint32(note[0:4]) |
| descsz := endian.Uint32(note[4:8]) |
| out.noteType = endian.Uint32(note[8:12]) |
| if namesz+12 > uint32(len(note)) { |
| return fmt.Errorf("invalid name length in note entry") |
| } |
| out.name = string(note[12 : 12+namesz]) |
| // We need to account for padding at the end. |
| descoff := alignTo(12+namesz, 4) |
| if descoff+descsz > uint32(len(note)) { |
| return fmt.Errorf("invalid desc length in note entry") |
| } |
| out.desc = note[descoff : descoff+descsz] |
| next := alignTo(descoff+descsz, 4) |
| // If the final padding isn't in the entry, don't throw error. |
| if next >= uint32(len(note)) { |
| note = note[len(note):] |
| } else { |
| note = note[next:] |
| } |
| entryFn(out) |
| } |
| } |
| |
| func getBuildIDs(filename string, endian binary.ByteOrder, data io.ReaderAt, size uint64) ([][]byte, error) { |
| noteBytes := make([]byte, size) |
| _, err := data.ReadAt(noteBytes, 0) |
| if err != nil { |
| return nil, fmt.Errorf("error parsing section header in %s: %v", filename, err) |
| } |
| out := [][]byte{} |
| err = forEachNote(noteBytes, endian, func(entry noteEntry) { |
| if entry.noteType != NT_GNU_BUILD_ID || entry.name != "GNU\000" { |
| return |
| } |
| out = append(out, entry.desc) |
| }) |
| return out, err |
| } |
| |
| func GetBuildIDs(filename string, file io.ReaderAt) ([][]byte, error) { |
| elfFile, err := elf.NewFile(file) |
| if err != nil { |
| return nil, fmt.Errorf("could not parse ELF file %s: %v", filename, err) |
| } |
| if len(elfFile.Progs) == 0 && len(elfFile.Sections) == 0 { |
| return nil, fmt.Errorf("no program headers or sections in %s", filename) |
| } |
| var endian binary.ByteOrder |
| if elfFile.Data == elf.ELFDATA2LSB { |
| endian = binary.LittleEndian |
| } else { |
| endian = binary.BigEndian |
| } |
| out := [][]byte{} |
| // Check every SHT_NOTE section. |
| for _, section := range elfFile.Sections { |
| if section == nil || section.Type != elf.SHT_NOTE { |
| continue |
| } |
| buildIDs, err := getBuildIDs(filename, endian, section, section.Size) |
| if err != nil { |
| return out, err |
| } |
| out = append(out, buildIDs...) |
| } |
| // If we found what we were looking for with sections, don't reparse the program |
| // headers. |
| if len(out) > 0 { |
| return out, nil |
| } |
| // Check every PT_NOTE segment. |
| for _, prog := range elfFile.Progs { |
| if prog == nil || prog.Type != elf.PT_NOTE { |
| continue |
| } |
| buildIDs, err := getBuildIDs(filename, endian, prog, prog.Filesz) |
| if err != nil { |
| return out, err |
| } |
| out = append(out, buildIDs...) |
| } |
| return out, nil |
| } |
| |
| type DynamicTable struct { |
| SoName string |
| } |
| |
| func GetSoName(filename string, file io.ReaderAt) (string, error) { |
| elfFile, err := elf.NewFile(file) |
| if err != nil { |
| return "", fmt.Errorf("could not parse ELF file %s: %v", filename, err) |
| } |
| strings, err := elfFile.DynString(elf.DT_SONAME) |
| if err != nil { |
| return "", fmt.Errorf("when parsing .dynamic from %s: %v", filename, err) |
| } |
| if len(strings) > 1 { |
| return "", fmt.Errorf("expected at most one DT_SONAME entry in %s", filename) |
| } |
| if len(strings) == 0 { |
| return "", nil |
| } |
| return strings[0], nil |
| } |
| |
| func ReadIDsFile(file io.Reader) ([]BinaryFileRef, error) { |
| scanner := bufio.NewScanner(file) |
| out := []BinaryFileRef{} |
| for line := 1; scanner.Scan(); line++ { |
| text := scanner.Text() |
| parts := strings.SplitN(text, " ", 2) |
| if len(parts) != 2 { |
| return nil, fmt.Errorf("error parsing on line %d: found `%s`", line, text) |
| } |
| build := strings.TrimSpace(parts[0]) |
| filename := strings.TrimSpace(parts[1]) |
| out = append(out, BinaryFileRef{Filepath: filename, BuildID: build}) |
| } |
| return out, nil |
| } |
| |
| // WalkBuildIDDir walks the directory containing symbol files at dirpath and creates a |
| // BinaryFileRef for each symbol file it finds. Files without a ".debug" suffix are |
| // skipped. Each output BinaryFileRef's BuildID is formed by concatenating the file's |
| // basename with its parent directory's basename. For example: |
| // |
| // Input directory: |
| // |
| // de/ |
| // eadbeef.debug |
| // eadmeat.debug |
| // |
| // Output BuildIDs: |
| // |
| // deadbeef.debug |
| // deadmeat.debug |
| func WalkBuildIDDir(dirpath string) ([]BinaryFileRef, error) { |
| info, err := os.Stat(dirpath) |
| if err != nil { |
| return nil, fmt.Errorf("failed to stat %q: %v", dirpath, err) |
| } |
| if !info.IsDir() { |
| return nil, fmt.Errorf("%q is not a directory", dirpath) |
| } |
| var refs []BinaryFileRef |
| if err := filepath.Walk(dirpath, func(path string, info os.FileInfo, err error) error { |
| if err != nil { |
| return fmt.Errorf("%q: %v", path, err) |
| } |
| if info.IsDir() { |
| return nil |
| } |
| if !strings.HasSuffix(path, DebugFileSuffix) { |
| return nil |
| } |
| dir, basename := filepath.Split(path) |
| buildID := filepath.Base(dir) + strings.TrimSuffix(basename, DebugFileSuffix) |
| refs = append(refs, BinaryFileRef{ |
| Filepath: path, |
| BuildID: buildID, |
| }) |
| return nil |
| }); err != nil { |
| return nil, err |
| } |
| return refs, nil |
| } |