Home | History | Annotate | Download | only in buildid
      1 // Copyright 2015 The Go Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style
      3 // license that can be found in the LICENSE file.
      4 
      5 package buildid
      6 
      7 import (
      8 	"bytes"
      9 	"debug/elf"
     10 	"debug/macho"
     11 	"encoding/binary"
     12 	"fmt"
     13 	"io"
     14 	"os"
     15 )
     16 
     17 func readAligned4(r io.Reader, sz int32) ([]byte, error) {
     18 	full := (sz + 3) &^ 3
     19 	data := make([]byte, full)
     20 	_, err := io.ReadFull(r, data)
     21 	if err != nil {
     22 		return nil, err
     23 	}
     24 	data = data[:sz]
     25 	return data, nil
     26 }
     27 
     28 func ReadELFNote(filename, name string, typ int32) ([]byte, error) {
     29 	f, err := elf.Open(filename)
     30 	if err != nil {
     31 		return nil, err
     32 	}
     33 	for _, sect := range f.Sections {
     34 		if sect.Type != elf.SHT_NOTE {
     35 			continue
     36 		}
     37 		r := sect.Open()
     38 		for {
     39 			var namesize, descsize, noteType int32
     40 			err = binary.Read(r, f.ByteOrder, &namesize)
     41 			if err != nil {
     42 				if err == io.EOF {
     43 					break
     44 				}
     45 				return nil, fmt.Errorf("read namesize failed: %v", err)
     46 			}
     47 			err = binary.Read(r, f.ByteOrder, &descsize)
     48 			if err != nil {
     49 				return nil, fmt.Errorf("read descsize failed: %v", err)
     50 			}
     51 			err = binary.Read(r, f.ByteOrder, &noteType)
     52 			if err != nil {
     53 				return nil, fmt.Errorf("read type failed: %v", err)
     54 			}
     55 			noteName, err := readAligned4(r, namesize)
     56 			if err != nil {
     57 				return nil, fmt.Errorf("read name failed: %v", err)
     58 			}
     59 			desc, err := readAligned4(r, descsize)
     60 			if err != nil {
     61 				return nil, fmt.Errorf("read desc failed: %v", err)
     62 			}
     63 			if name == string(noteName) && typ == noteType {
     64 				return desc, nil
     65 			}
     66 		}
     67 	}
     68 	return nil, nil
     69 }
     70 
     71 var elfGoNote = []byte("Go\x00\x00")
     72 var elfGNUNote = []byte("GNU\x00")
     73 
     74 // The Go build ID is stored in a note described by an ELF PT_NOTE prog
     75 // header. The caller has already opened filename, to get f, and read
     76 // at least 4 kB out, in data.
     77 func readELF(name string, f *os.File, data []byte) (buildid string, err error) {
     78 	// Assume the note content is in the data, already read.
     79 	// Rewrite the ELF header to set shnum to 0, so that we can pass
     80 	// the data to elf.NewFile and it will decode the Prog list but not
     81 	// try to read the section headers and the string table from disk.
     82 	// That's a waste of I/O when all we care about is the Prog list
     83 	// and the one ELF note.
     84 	switch elf.Class(data[elf.EI_CLASS]) {
     85 	case elf.ELFCLASS32:
     86 		data[48] = 0
     87 		data[49] = 0
     88 	case elf.ELFCLASS64:
     89 		data[60] = 0
     90 		data[61] = 0
     91 	}
     92 
     93 	const elfGoBuildIDTag = 4
     94 	const gnuBuildIDTag = 3
     95 
     96 	ef, err := elf.NewFile(bytes.NewReader(data))
     97 	if err != nil {
     98 		return "", &os.PathError{Path: name, Op: "parse", Err: err}
     99 	}
    100 	var gnu string
    101 	for _, p := range ef.Progs {
    102 		if p.Type != elf.PT_NOTE || p.Filesz < 16 {
    103 			continue
    104 		}
    105 
    106 		var note []byte
    107 		if p.Off+p.Filesz < uint64(len(data)) {
    108 			note = data[p.Off : p.Off+p.Filesz]
    109 		} else {
    110 			// For some linkers, such as the Solaris linker,
    111 			// the buildid may not be found in data (which
    112 			// likely contains the first 16kB of the file)
    113 			// or even the first few megabytes of the file
    114 			// due to differences in note segment placement;
    115 			// in that case, extract the note data manually.
    116 			_, err = f.Seek(int64(p.Off), io.SeekStart)
    117 			if err != nil {
    118 				return "", err
    119 			}
    120 
    121 			note = make([]byte, p.Filesz)
    122 			_, err = io.ReadFull(f, note)
    123 			if err != nil {
    124 				return "", err
    125 			}
    126 		}
    127 
    128 		filesz := p.Filesz
    129 		off := p.Off
    130 		for filesz >= 16 {
    131 			nameSize := ef.ByteOrder.Uint32(note)
    132 			valSize := ef.ByteOrder.Uint32(note[4:])
    133 			tag := ef.ByteOrder.Uint32(note[8:])
    134 			nname := note[12:16]
    135 			if nameSize == 4 && 16+valSize <= uint32(len(note)) && tag == elfGoBuildIDTag && bytes.Equal(nname, elfGoNote) {
    136 				return string(note[16 : 16+valSize]), nil
    137 			}
    138 
    139 			if nameSize == 4 && 16+valSize <= uint32(len(note)) && tag == gnuBuildIDTag && bytes.Equal(nname, elfGNUNote) {
    140 				gnu = string(note[16 : 16+valSize])
    141 			}
    142 
    143 			nameSize = (nameSize + 3) &^ 3
    144 			valSize = (valSize + 3) &^ 3
    145 			notesz := uint64(12 + nameSize + valSize)
    146 			if filesz <= notesz {
    147 				break
    148 			}
    149 			off += notesz
    150 			align := uint64(p.Align)
    151 			alignedOff := (off + align - 1) &^ (align - 1)
    152 			notesz += alignedOff - off
    153 			off = alignedOff
    154 			filesz -= notesz
    155 			note = note[notesz:]
    156 		}
    157 	}
    158 
    159 	// If we didn't find a Go note, use a GNU note if available.
    160 	// This is what gccgo uses.
    161 	if gnu != "" {
    162 		return gnu, nil
    163 	}
    164 
    165 	// No note. Treat as successful but build ID empty.
    166 	return "", nil
    167 }
    168 
    169 // The Go build ID is stored at the beginning of the Mach-O __text segment.
    170 // The caller has already opened filename, to get f, and read a few kB out, in data.
    171 // Sadly, that's not guaranteed to hold the note, because there is an arbitrary amount
    172 // of other junk placed in the file ahead of the main text.
    173 func readMacho(name string, f *os.File, data []byte) (buildid string, err error) {
    174 	// If the data we want has already been read, don't worry about Mach-O parsing.
    175 	// This is both an optimization and a hedge against the Mach-O parsing failing
    176 	// in the future due to, for example, the name of the __text section changing.
    177 	if b, err := readRaw(name, data); b != "" && err == nil {
    178 		return b, err
    179 	}
    180 
    181 	mf, err := macho.NewFile(f)
    182 	if err != nil {
    183 		return "", &os.PathError{Path: name, Op: "parse", Err: err}
    184 	}
    185 
    186 	sect := mf.Section("__text")
    187 	if sect == nil {
    188 		// Every binary has a __text section. Something is wrong.
    189 		return "", &os.PathError{Path: name, Op: "parse", Err: fmt.Errorf("cannot find __text section")}
    190 	}
    191 
    192 	// It should be in the first few bytes, but read a lot just in case,
    193 	// especially given our past problems on OS X with the build ID moving.
    194 	// There shouldn't be much difference between reading 4kB and 32kB:
    195 	// the hard part is getting to the data, not transferring it.
    196 	n := sect.Size
    197 	if n > uint64(readSize) {
    198 		n = uint64(readSize)
    199 	}
    200 	buf := make([]byte, n)
    201 	if _, err := f.ReadAt(buf, int64(sect.Offset)); err != nil {
    202 		return "", err
    203 	}
    204 
    205 	return readRaw(name, buf)
    206 }
    207