1// Copyright 2022 Google Inc. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package elf
16
17import (
18	"debug/elf"
19	"encoding/binary"
20	"encoding/hex"
21	"errors"
22	"fmt"
23	"io"
24	"os"
25)
26
27const gnuBuildID = "GNU\x00"
28
29// Identifier extracts the elf build ID from an elf file.  If allowMissing is true it returns
30// an empty identifier if the file exists but the build ID note does not.
31func Identifier(filename string, allowMissing bool) (string, error) {
32	f, err := os.Open(filename)
33	if err != nil {
34		return "", fmt.Errorf("failed to open %s: %w", filename, err)
35	}
36	defer f.Close()
37
38	return elfIdentifierFromReaderAt(f, filename, allowMissing)
39}
40
41// elfIdentifierFromReaderAt extracts the elf build ID from a ReaderAt.  If allowMissing is true it
42// returns an empty identifier if the file exists but the build ID note does not.
43func elfIdentifierFromReaderAt(r io.ReaderAt, filename string, allowMissing bool) (string, error) {
44	f, err := elf.NewFile(r)
45	if err != nil {
46		if allowMissing {
47			if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
48				return "", nil
49			}
50			if _, ok := err.(*elf.FormatError); ok {
51				// The file was not an elf file.
52				return "", nil
53			}
54		}
55		return "", fmt.Errorf("failed to parse elf file %s: %w", filename, err)
56	}
57	defer f.Close()
58
59	buildIDNote := f.Section(".note.gnu.build-id")
60	if buildIDNote == nil {
61		if allowMissing {
62			return "", nil
63		}
64		return "", fmt.Errorf("failed to find .note.gnu.build-id in  %s", filename)
65	}
66
67	buildIDs, err := readNote(buildIDNote.Open(), f.ByteOrder)
68	if err != nil {
69		return "", fmt.Errorf("failed to read .note.gnu.build-id: %w", err)
70	}
71
72	for name, desc := range buildIDs {
73		if name == gnuBuildID {
74			return hex.EncodeToString(desc), nil
75		}
76	}
77
78	return "", nil
79}
80
81// readNote reads the contents of a note section, returning it as a map from name to descriptor.
82func readNote(note io.Reader, byteOrder binary.ByteOrder) (map[string][]byte, error) {
83	var noteHeader struct {
84		Namesz uint32
85		Descsz uint32
86		Type   uint32
87	}
88
89	notes := make(map[string][]byte)
90	for {
91		err := binary.Read(note, byteOrder, &noteHeader)
92		if err != nil {
93			if err == io.EOF {
94				return notes, nil
95			}
96			return nil, fmt.Errorf("failed to read note header: %w", err)
97		}
98
99		nameBuf := make([]byte, align4(noteHeader.Namesz))
100		err = binary.Read(note, byteOrder, &nameBuf)
101		if err != nil {
102			return nil, fmt.Errorf("failed to read note name: %w", err)
103		}
104		name := string(nameBuf[:noteHeader.Namesz])
105
106		descBuf := make([]byte, align4(noteHeader.Descsz))
107		err = binary.Read(note, byteOrder, &descBuf)
108		if err != nil {
109			return nil, fmt.Errorf("failed to read note desc: %w", err)
110		}
111		notes[name] = descBuf[:noteHeader.Descsz]
112	}
113}
114
115// align4 rounds the input up to the next multiple of 4.
116func align4(i uint32) uint32 {
117	return (i + 3) &^ 3
118}
119