• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2021 The Pigweed Authors
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may not
4# use this file except in compliance with the License. You may obtain a copy of
5# the License at
6#
7#     https://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, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations under
13# the License.
14"""Library that parses an ELF file for a GNU build-id."""
15
16import argparse
17import logging
18from pathlib import Path
19import sys
20from typing import BinaryIO, Optional
21import elftools  # type: ignore
22from elftools.elf import elffile, notes, sections  # type: ignore
23
24_LOG = logging.getLogger('build_id_parser')
25_PW_BUILD_ID_SYM_NAME = 'gnu_build_id_begin'
26
27
28class GnuBuildIdError(Exception):
29    """An exception raised when a GNU build ID is malformed."""
30
31
32def read_build_id_from_section(elf_file: BinaryIO) -> Optional[bytes]:
33    """Reads a build ID from a .note.gnu.build-id section."""
34    parsed_elf_file = elffile.ELFFile(elf_file)
35    build_id_section = parsed_elf_file.get_section_by_name(
36        '.note.gnu.build-id')
37
38    if build_id_section is None:
39        return None
40
41    section_notes = list(n for n in notes.iter_notes(
42        parsed_elf_file, build_id_section['sh_offset'],
43        build_id_section['sh_size']))
44
45    if len(section_notes) != 1:
46        raise GnuBuildIdError('GNU build ID section contains multiple notes')
47
48    build_id_note = section_notes[0]
49    if build_id_note['n_name'] != 'GNU':
50        raise GnuBuildIdError('GNU build ID note name invalid')
51
52    if build_id_note['n_type'] != 'NT_GNU_BUILD_ID':
53        raise GnuBuildIdError('GNU build ID note type invalid')
54
55    return bytes.fromhex(build_id_note['n_desc'])
56
57
58def _addr_is_in_segment(addr: int, segment) -> bool:
59    """Checks if the provided address resides within the provided segment."""
60    # Address references uninitialized memory. Can't read.
61    if addr >= segment['p_vaddr'] + segment['p_filesz']:
62        raise GnuBuildIdError('GNU build ID is runtime-initialized')
63
64    return addr in range(segment['p_vaddr'], segment['p_memsz'])
65
66
67def _read_build_id_from_offset(elf, offset: int) -> bytes:
68    """Attempts to read a GNU build ID from an offset in an elf file."""
69    note = elftools.common.utils.struct_parse(elf.structs.Elf_Nhdr,
70                                              elf.stream,
71                                              stream_pos=offset)
72    elf.stream.seek(offset + elf.structs.Elf_Nhdr.sizeof())
73    name = elf.stream.read(note['n_namesz'])
74
75    if name != b'GNU\0':
76        raise GnuBuildIdError('GNU build ID note name invalid')
77
78    return elf.stream.read(note['n_descsz'])
79
80
81def read_build_id_from_symbol(elf_file: BinaryIO) -> Optional[bytes]:
82    """Reads a GNU build ID using gnu_build_id_begin to locate the data."""
83    parsed_elf_file = elffile.ELFFile(elf_file)
84
85    matching_syms = None
86    for section in parsed_elf_file.iter_sections():
87        if not isinstance(section, sections.SymbolTableSection):
88            continue
89        matching_syms = section.get_symbol_by_name(_PW_BUILD_ID_SYM_NAME)
90        if matching_syms is not None:
91            break
92    if matching_syms is None:
93        return None
94
95    if len(matching_syms) != 1:
96        raise GnuBuildIdError('Multiple GNU build ID start symbols defined')
97
98    gnu_build_id_sym = matching_syms[0]
99    section_number = gnu_build_id_sym['st_shndx']
100
101    if section_number == 'SHN_UNDEF':
102        raise GnuBuildIdError('GNU build ID start symbol undefined')
103
104    matching_section = parsed_elf_file.get_section(section_number)
105
106    build_id_start_addr = gnu_build_id_sym['st_value']
107    for segment in parsed_elf_file.iter_segments():
108        if segment.section_in_segment(matching_section):
109            offset = build_id_start_addr - segment['p_vaddr'] + segment[
110                'p_offset']
111            return _read_build_id_from_offset(parsed_elf_file, offset)
112
113    return None
114
115
116def read_build_id(elf_file: BinaryIO) -> Optional[bytes]:
117    """Reads a GNU build ID from an ELF binary."""
118    # Prefer to read the build ID from a dedicated section.
119    maybe_build_id = read_build_id_from_section(elf_file)
120    if maybe_build_id is not None:
121        return maybe_build_id
122
123    # If there's no dedicated section, try and use symbol information to find
124    # the build info.
125    return read_build_id_from_symbol(elf_file)
126
127
128def find_matching_elf(uuid: bytes, search_dir: Path) -> Optional[Path]:
129    """Recursively searches a directory for an ELF file with a matching UUID."""
130    elf_file_paths = search_dir.glob('**/*.elf')
131    for elf_file in elf_file_paths:
132        try:
133            candidate_id = read_build_id(open(elf_file, 'rb'))
134        except GnuBuildIdError:
135            continue
136        if candidate_id is None:
137            continue
138        if candidate_id == uuid:
139            return elf_file
140
141    return None
142
143
144def _main(elf_file: BinaryIO) -> int:
145    logging.basicConfig(format='%(message)s', level=logging.INFO)
146    build_id = read_build_id(elf_file)
147    if build_id is None:
148        _LOG.error('Error: No GNU build ID found.')
149        return 1
150
151    _LOG.info(build_id.hex())
152    return 0
153
154
155def _parse_args():
156    """Parses command-line arguments."""
157
158    parser = argparse.ArgumentParser(description=__doc__)
159    parser.add_argument('elf_file',
160                        type=argparse.FileType('rb'),
161                        help='The .elf to parse build info from')
162
163    return parser.parse_args()
164
165
166if __name__ == '__main__':
167    sys.exit(_main(**vars(_parse_args())))
168