1#!/usr/bin/env python3 2# Copyright 2020 The Pigweed Authors 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); you may not 5# use this file except in compliance with the License. You may obtain a copy of 6# the License at 7# 8# https://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13# License for the specific language governing permissions and limitations under 14# the License. 15"""Tests the ELF reader Python module.""" 16 17import io 18import os 19import re 20import unittest 21 22from pw_tokenizer import elf_reader 23 24# Output from the following command: 25# 26# readelf -WS elf_reader_test_binary.elf 27# 28TEST_READELF_OUTPUT = (""" 29There are 33 section headers, starting at offset 0x1758: 30 31Section Headers: 32 [Nr] Name Type Address Off Size ES Flg Lk Inf Al 33 [ 0] NULL 0000000000000000 000000 000000 00 0 0 0 34 [ 1] .interp PROGBITS 0000000000000238 000238 00001c 00 A 0 0 1 35 [ 2] .note.ABI-tag NOTE 0000000000000254 000254 000020 00 A 0 0 4 36 [ 3] .note.gnu.build-id NOTE 0000000000000274 000274 000024 00 A 0 0 4 37 [ 4] .dynsym DYNSYM 0000000000000298 000298 0000a8 18 A 5 1 8 38 [ 5] .dynstr STRTAB 0000000000000340 000340 00009b 00 A 0 0 1 39 [ 6] .gnu.hash GNU_HASH 00000000000003e0 0003e0 00001c 00 A 4 0 8 40 [ 7] .gnu.version VERSYM 00000000000003fc 0003fc 00000e 02 A 4 0 2 41 [ 8] .gnu.version_r VERNEED 000000000000040c 00040c 000020 00 A 5 1 4 42 [ 9] .rela.dyn RELA 0000000000000430 000430 0000d8 18 A 4 0 8 43 [10] .rela.plt RELA 0000000000000508 000508 000018 18 AI 4 12 8 44 [11] .init PROGBITS 0000000000000520 000520 000017 00 AX 0 0 4 45 [12] .plt PROGBITS 0000000000000540 000540 000020 10 AX 0 0 16 46 [13] .text PROGBITS 0000000000000560 000560 000151 00 AX 0 0 16 47 [14] .fini PROGBITS 00000000000006b4 0006b4 000009 00 AX 0 0 4 48 [15] .rodata PROGBITS 00000000000006c0 0006c0 000004 04 AM 0 0 4 49 [16] .test_section_1 PROGBITS 00000000000006d0 0006d0 000010 00 A 0 0 16 50 [17] .test_section_2 PROGBITS 00000000000006e0 0006e0 000004 00 A 0 0 4 51 [18] .eh_frame X86_64_UNWIND 00000000000006e8 0006e8 0000d4 00 A 0 0 8 52 [19] .eh_frame_hdr X86_64_UNWIND 00000000000007bc 0007bc 00002c 00 A 0 0 4 53 [20] .fini_array FINI_ARRAY 0000000000001d80 000d80 000008 08 WA 0 0 8 54 [21] .init_array INIT_ARRAY 0000000000001d88 000d88 000008 08 WA 0 0 8 55 [22] .dynamic DYNAMIC 0000000000001d90 000d90 000220 10 WA 5 0 8 56 [23] .got PROGBITS 0000000000001fb0 000fb0 000030 00 WA 0 0 8 57 [24] .got.plt PROGBITS 0000000000001fe0 000fe0 000020 00 WA 0 0 8 58 [25] .data PROGBITS 0000000000002000 001000 000010 00 WA 0 0 8 59 [26] .tm_clone_table PROGBITS 0000000000002010 001010 000000 00 WA 0 0 8 60 [27] .bss NOBITS 0000000000002010 001010 000001 00 WA 0 0 1 61 [28] .comment PROGBITS 0000000000000000 001010 00001d 01 MS 0 0 1 62 [29] .note.gnu.gold-version NOTE 0000000000000000 001030 00001c 00 0 0 4 63 [30] .symtab SYMTAB 0000000000000000 001050 000390 18 31 21 8 64 [31] .strtab STRTAB 0000000000000000 0013e0 000227 00 0 0 1 65 [32] .shstrtab STRTAB 0000000000000000 001607 00014a 00 0 0 1 66Key to Flags: 67 W (write), A (alloc), X (execute), M (merge), S (strings), I (info), 68 L (link order), O (extra OS processing required), G (group), T (TLS), 69 C (compressed), x (unknown), o (OS specific), E (exclude), 70 l (large), p (processor specific) 71""") 72 73TEST_ELF_PATH = os.path.join(os.path.dirname(__file__), 74 'elf_reader_test_binary.elf') 75 76 77class ElfReaderTest(unittest.TestCase): 78 """Tests the elf_reader.Elf class.""" 79 def setUp(self): 80 super().setUp() 81 self._elf_file = open(TEST_ELF_PATH, 'rb') 82 self._elf = elf_reader.Elf(self._elf_file) 83 84 def tearDown(self): 85 super().tearDown() 86 self._elf_file.close() 87 88 def _section(self, name): 89 return next(self._elf.sections_with_name(name)) 90 91 def test_readelf_comparison_using_the_readelf_binary(self): 92 """Compares elf_reader to readelf's output.""" 93 94 parse_readelf_output = re.compile(r'\s+' 95 r'\[\s*(?P<number>\d+)\]\s+' 96 r'(?P<name>\.\S*)?\s+' 97 r'(?P<type>\S+)\s+' 98 r'(?P<addr>[0-9a-fA-F]+)\s+' 99 r'(?P<offset>[0-9a-fA-F]+)\s+' 100 r'(?P<size>[0-9a-fA-F]+)\s+') 101 102 readelf_sections = [] 103 for number, name, _, addr, offset, size in parse_readelf_output.findall( 104 TEST_READELF_OUTPUT): 105 readelf_sections.append(( 106 int(number), 107 name or '', 108 int(addr, 16), 109 int(offset, 16), 110 int(size, 16), 111 )) 112 113 self.assertEqual(len(readelf_sections), 33) 114 self.assertEqual(len(readelf_sections), len(self._elf.sections)) 115 116 for (index, 117 section), readelf_section in zip(enumerate(self._elf.sections), 118 readelf_sections): 119 readelf_index, name, address, offset, size = readelf_section 120 121 self.assertEqual(index, readelf_index) 122 self.assertEqual(section.name, name) 123 self.assertEqual(section.address, address) 124 self.assertEqual(section.offset, offset) 125 self.assertEqual(section.size, size) 126 127 def test_dump_single_section(self): 128 self.assertEqual(self._elf.dump_section_contents(r'\.test_section_1'), 129 b'You cannot pass\0') 130 self.assertEqual(self._elf.dump_section_contents(r'\.test_section_2'), 131 b'\xef\xbe\xed\xfe') 132 133 def test_dump_multiple_sections(self): 134 if (self._section('.test_section_1').address < 135 self._section('.test_section_2').address): 136 contents = b'You cannot pass\0\xef\xbe\xed\xfe' 137 else: 138 contents = b'\xef\xbe\xed\xfeYou cannot pass\0' 139 140 self.assertIn(self._elf.dump_section_contents(r'.test_section_\d'), 141 contents) 142 143 def test_read_values(self): 144 address = self._section('.test_section_1').address 145 self.assertEqual(self._elf.read_value(address), b'You cannot pass') 146 147 int32_address = self._section('.test_section_2').address 148 self.assertEqual(self._elf.read_value(int32_address, 4), 149 b'\xef\xbe\xed\xfe') 150 151 def test_read_string(self): 152 bytes_io = io.BytesIO( 153 b'This is a null-terminated string\0No terminator!') 154 self.assertEqual(elf_reader.read_c_string(bytes_io), 155 b'This is a null-terminated string') 156 self.assertEqual(elf_reader.read_c_string(bytes_io), b'No terminator!') 157 self.assertEqual(elf_reader.read_c_string(bytes_io), b'') 158 159 def test_compatible_file_for_elf(self): 160 self.assertTrue(elf_reader.compatible_file(self._elf_file)) 161 self.assertTrue(elf_reader.compatible_file(io.BytesIO(b'\x7fELF'))) 162 163 def test_compatible_file_for_elf_start_at_offset(self): 164 self._elf_file.seek(13) # Seek ahead to get out of sync 165 self.assertTrue(elf_reader.compatible_file(self._elf_file)) 166 self.assertEqual(13, self._elf_file.tell()) 167 168 def test_compatible_file_for_invalid_elf(self): 169 self.assertFalse(elf_reader.compatible_file(io.BytesIO(b'\x7fELVESF'))) 170 171 172def _archive_file(data: bytes) -> bytes: 173 return ('FILE ID 90123456' 174 'MODIFIED 012' 175 'OWNER ' 176 'GROUP ' 177 'MODE 678' 178 f'{len(data):10}' # File size -- the only part that's needed. 179 '`\n'.encode() + data) 180 181 182class ArchiveTest(unittest.TestCase): 183 """Tests reading from archive files.""" 184 def setUp(self): 185 super().setUp() 186 187 with open(TEST_ELF_PATH, 'rb') as fd: 188 self._elf_data = fd.read() 189 190 self._archive_entries = b'blah', b'hello', self._elf_data 191 192 self._archive_data = elf_reader.ARCHIVE_MAGIC + b''.join( 193 _archive_file(f) for f in self._archive_entries) 194 self._archive = io.BytesIO(self._archive_data) 195 196 def test_compatible_file_for_archive(self): 197 self.assertTrue(elf_reader.compatible_file(io.BytesIO(b'!<arch>\n'))) 198 self.assertTrue(elf_reader.compatible_file(self._archive)) 199 200 def test_compatible_file_for_invalid_archive(self): 201 self.assertFalse(elf_reader.compatible_file(io.BytesIO(b'!<arch>'))) 202 203 def test_iterate_over_files(self): 204 for expected, size in zip(self._archive_entries, 205 elf_reader.files_in_archive(self._archive)): 206 self.assertEqual(expected, self._archive.read(size)) 207 208 def test_iterate_over_empty_archive(self): 209 with self.assertRaises(StopIteration): 210 next(iter(elf_reader.files_in_archive(io.BytesIO(b'!<arch>\n')))) 211 212 def test_iterate_over_invalid_archive(self): 213 with self.assertRaises(elf_reader.FileDecodeError): 214 for _ in elf_reader.files_in_archive( 215 io.BytesIO(b'!<arch>blah blahblah')): 216 pass 217 218 def test_extra_newline_after_entry_is_ignored(self): 219 archive = io.BytesIO(elf_reader.ARCHIVE_MAGIC + 220 _archive_file(self._elf_data) + b'\n' + 221 _archive_file(self._elf_data)) 222 223 for size in elf_reader.files_in_archive(archive): 224 self.assertEqual(self._elf_data, archive.read(size)) 225 226 def test_two_extra_newlines_parsing_fails(self): 227 archive = io.BytesIO(elf_reader.ARCHIVE_MAGIC + 228 _archive_file(self._elf_data) + b'\n\n' + 229 _archive_file(self._elf_data)) 230 231 with self.assertRaises(elf_reader.FileDecodeError): 232 for size in elf_reader.files_in_archive(archive): 233 self.assertEqual(self._elf_data, archive.read(size)) 234 235 def test_iterate_over_archive_with_invalid_size(self): 236 data = elf_reader.ARCHIVE_MAGIC + _archive_file(b'$' * 3210) 237 file = io.BytesIO(data) 238 239 # Iterate over the file normally. 240 for size in elf_reader.files_in_archive(file): 241 self.assertEqual(b'$' * 3210, file.read(size)) 242 243 # Replace the size with a hex number, which is not valid. 244 with self.assertRaises(elf_reader.FileDecodeError): 245 for _ in elf_reader.files_in_archive( 246 io.BytesIO(data.replace(b'3210', b'0x99'))): 247 pass 248 249 def test_elf_reader_dump_single_section(self): 250 elf = elf_reader.Elf(self._archive) 251 self.assertEqual(elf.dump_section_contents(r'\.test_section_1'), 252 b'You cannot pass\0') 253 self.assertEqual(elf.dump_section_contents(r'\.test_section_2'), 254 b'\xef\xbe\xed\xfe') 255 256 def test_elf_reader_read_values(self): 257 elf = elf_reader.Elf(self._archive) 258 address = next(elf.sections_with_name('.test_section_1')).address 259 self.assertEqual(elf.read_value(address), b'You cannot pass') 260 261 int32_address = next(elf.sections_with_name('.test_section_2')).address 262 self.assertEqual(elf.read_value(int32_address, 4), b'\xef\xbe\xed\xfe') 263 264 265if __name__ == '__main__': 266 unittest.main() 267