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