// Copyright 2015 The Go 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 buildid import ( "bytes" "debug/elf" "debug/macho" "encoding/binary" "fmt" "io" "io/fs" "os" ) func readAligned4(r io.Reader, sz int32) ([]byte, error) { full := (sz + 3) &^ 3 data := make([]byte, full) _, err := io.ReadFull(r, data) if err != nil { return nil, err } data = data[:sz] return data, nil } func ReadELFNote(filename, name string, typ int32) ([]byte, error) { f, err := elf.Open(filename) if err != nil { return nil, err } defer f.Close() for _, sect := range f.Sections { if sect.Type != elf.SHT_NOTE { continue } r := sect.Open() for { var namesize, descsize, noteType int32 err = binary.Read(r, f.ByteOrder, &namesize) if err != nil { if err == io.EOF { break } return nil, fmt.Errorf("read namesize failed: %v", err) } err = binary.Read(r, f.ByteOrder, &descsize) if err != nil { return nil, fmt.Errorf("read descsize failed: %v", err) } err = binary.Read(r, f.ByteOrder, ¬eType) if err != nil { return nil, fmt.Errorf("read type failed: %v", err) } noteName, err := readAligned4(r, namesize) if err != nil { return nil, fmt.Errorf("read name failed: %v", err) } desc, err := readAligned4(r, descsize) if err != nil { return nil, fmt.Errorf("read desc failed: %v", err) } if name == string(noteName) && typ == noteType { return desc, nil } } } return nil, nil } var elfGoNote = []byte("Go\x00\x00") var elfGNUNote = []byte("GNU\x00") // The Go build ID is stored in a note described by an ELF PT_NOTE prog // header. The caller has already opened filename, to get f, and read // at least 4 kB out, in data. func readELF(name string, f *os.File, data []byte) (buildid string, err error) { // Assume the note content is in the data, already read. // Rewrite the ELF header to set shoff and shnum to 0, so that we can pass // the data to elf.NewFile and it will decode the Prog list but not // try to read the section headers and the string table from disk. // That's a waste of I/O when all we care about is the Prog list // and the one ELF note. switch elf.Class(data[elf.EI_CLASS]) { case elf.ELFCLASS32: data[32], data[33], data[34], data[35] = 0, 0, 0, 0 data[48] = 0 data[49] = 0 case elf.ELFCLASS64: data[40], data[41], data[42], data[43] = 0, 0, 0, 0 data[44], data[45], data[46], data[47] = 0, 0, 0, 0 data[60] = 0 data[61] = 0 } const elfGoBuildIDTag = 4 const gnuBuildIDTag = 3 ef, err := elf.NewFile(bytes.NewReader(data)) if err != nil { return "", &fs.PathError{Path: name, Op: "parse", Err: err} } var gnu string for _, p := range ef.Progs { if p.Type != elf.PT_NOTE || p.Filesz < 16 { continue } var note []byte if p.Off+p.Filesz < uint64(len(data)) { note = data[p.Off : p.Off+p.Filesz] } else { // For some linkers, such as the Solaris linker, // the buildid may not be found in data (which // likely contains the first 16kB of the file) // or even the first few megabytes of the file // due to differences in note segment placement; // in that case, extract the note data manually. _, err = f.Seek(int64(p.Off), io.SeekStart) if err != nil { return "", err } note = make([]byte, p.Filesz) _, err = io.ReadFull(f, note) if err != nil { return "", err } } filesz := p.Filesz off := p.Off for filesz >= 16 { nameSize := ef.ByteOrder.Uint32(note) valSize := ef.ByteOrder.Uint32(note[4:]) tag := ef.ByteOrder.Uint32(note[8:]) nname := note[12:16] if nameSize == 4 && 16+valSize <= uint32(len(note)) && tag == elfGoBuildIDTag && bytes.Equal(nname, elfGoNote) { return string(note[16 : 16+valSize]), nil } if nameSize == 4 && 16+valSize <= uint32(len(note)) && tag == gnuBuildIDTag && bytes.Equal(nname, elfGNUNote) { gnu = string(note[16 : 16+valSize]) } nameSize = (nameSize + 3) &^ 3 valSize = (valSize + 3) &^ 3 notesz := uint64(12 + nameSize + valSize) if filesz <= notesz { break } off += notesz align := p.Align if align != 0 { alignedOff := (off + align - 1) &^ (align - 1) notesz += alignedOff - off off = alignedOff } filesz -= notesz note = note[notesz:] } } // If we didn't find a Go note, use a GNU note if available. // This is what gccgo uses. if gnu != "" { return gnu, nil } // No note. Treat as successful but build ID empty. return "", nil } // The Go build ID is stored at the beginning of the Mach-O __text segment. // The caller has already opened filename, to get f, and read a few kB out, in data. // Sadly, that's not guaranteed to hold the note, because there is an arbitrary amount // of other junk placed in the file ahead of the main text. func readMacho(name string, f *os.File, data []byte) (buildid string, err error) { // If the data we want has already been read, don't worry about Mach-O parsing. // This is both an optimization and a hedge against the Mach-O parsing failing // in the future due to, for example, the name of the __text section changing. if b, err := readRaw(name, data); b != "" && err == nil { return b, err } mf, err := macho.NewFile(f) if err != nil { return "", &fs.PathError{Path: name, Op: "parse", Err: err} } sect := mf.Section("__text") if sect == nil { // Every binary has a __text section. Something is wrong. return "", &fs.PathError{Path: name, Op: "parse", Err: fmt.Errorf("cannot find __text section")} } // It should be in the first few bytes, but read a lot just in case, // especially given our past problems on OS X with the build ID moving. // There shouldn't be much difference between reading 4kB and 32kB: // the hard part is getting to the data, not transferring it. n := sect.Size if n > uint64(readSize) { n = uint64(readSize) } buf := make([]byte, n) if _, err := f.ReadAt(buf, int64(sect.Offset)); err != nil { return "", err } return readRaw(name, buf) }