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