diff --git a/cle/backends/elf/elf.py b/cle/backends/elf/elf.py index 21a93e3b2..afeabee11 100644 --- a/cle/backends/elf/elf.py +++ b/cle/backends/elf/elf.py @@ -1442,6 +1442,7 @@ def __register_sections(self): for sec_readelf in self._reader.iter_sections(): remap_offset = 0 + occupies_memory = True if self.is_relocatable and sec_readelf.header["sh_flags"] & 2: # alloc flag # Relocatable objects' section addresses are meaningless (they are meant to be relocated anyway) # We thus have to map them manually to valid virtual addresses to emulate a linker's behaviour. @@ -1451,10 +1452,14 @@ def __register_sections(self): new_addr = (new_addr + (align - 1)) // align * align remap_offset = new_addr - sh_addr - if sec_readelf.header["sh_type"] not in _NON_ALLOCATED_SECTION_NAMES: + if sec_readelf.header["sh_type"] in _NON_ALLOCATED_SECTION_NAMES: + # No space is reserved for these and no backer is added below, so the address just + # computed belongs to whichever section comes next. + occupies_memory = False + else: new_addr += sec_readelf.header["sh_size"] - section = ELFSection(sec_readelf, remap_offset=remap_offset) + section = ELFSection(sec_readelf, remap_offset=remap_offset, occupies_memory=occupies_memory) sec_list.append((sec_readelf, section)) # Register sections first, process later - this is required by relocatable objects diff --git a/cle/backends/elf/regions.py b/cle/backends/elf/regions.py index c5f46fe0d..cd0531872 100644 --- a/cle/backends/elf/regions.py +++ b/cle/backends/elf/regions.py @@ -46,7 +46,7 @@ class ELFSection(Section): SHF_STRINGS = 0x20 SHT_NULL = "SHT_NULL" - def __init__(self, readelf_sec, remap_offset=0): + def __init__(self, readelf_sec, remap_offset=0, occupies_memory=True): super().__init__( maybedecode(readelf_sec.name), readelf_sec.header.sh_offset, @@ -61,6 +61,7 @@ def __init__(self, readelf_sec, remap_offset=0): self.info = readelf_sec.header.sh_info self.align = readelf_sec.header.sh_addralign self.remap_offset = remap_offset + self._occupies_memory = occupies_memory @property def is_readable(self): @@ -76,7 +77,7 @@ def is_writable(self): @property def occupies_memory(self): - return self.flags & self.SHF_ALLOC != 0 and self.memsize > 0 + return self._occupies_memory and self.flags & self.SHF_ALLOC != 0 and self.memsize > 0 @property def is_executable(self): diff --git a/tests/test_regions.py b/tests/test_regions.py index 38a381bc8..8376ce803 100644 --- a/tests/test_regions.py +++ b/tests/test_regions.py @@ -5,7 +5,7 @@ import cle from cle.address_translator import AT -from cle.backends import Section, Segment +from cle.backends import ELF, Section, Segment TESTS_BASE = os.path.join( os.path.dirname(os.path.realpath(__file__)), @@ -13,6 +13,11 @@ ) +def name_of(section: Section | None) -> str | None: + """The name of a section an address lookup returned, or None when it found nothing.""" + return None if section is None else section.name + + groundtruth = { ("x86_64", "allcmps"): { "sections": [ @@ -175,5 +180,48 @@ def test_segments(self): self._run_segments(arch, filename, data["segments"]) +class TestRelocatableSections(unittest.TestCase): + """ + A relocatable object carries no addresses of its own, so CLE assigns them. The assignment has to + agree with what CLE actually maps. + """ + + BINARY = os.path.join(TESTS_BASE, "x86_64", "switch_default_abort.o") + + def _load(self): + ld = cle.Loader(self.BINARY, auto_load_libs=False) + main = ld.main_object + assert isinstance(main, ELF) + self.assertTrue(main.is_relocatable) + return ld, main + + def test_every_mapped_address_resolves_to_its_section(self): + ld, main = self._load() + + mapped = sorted((s for s in main.sections if s.occupies_memory), key=lambda s: s.vaddr) + for lower, upper in zip(mapped, mapped[1:]): + self.assertLessEqual( + lower.vaddr + lower.memsize, + upper.vaddr, + f"{lower.name} overlaps {upper.name}", + ) + + # The lookup bisects on the section end addresses, so one overlap loses whole sections. + for section in mapped: + for addr in range(section.vaddr, section.vaddr + section.memsize): + self.assertEqual(name_of(main.sections.find_region_containing(addr)), section.name) + self.assertEqual(name_of(ld.find_section_containing(addr)), section.name) + + def test_unloaded_section_claims_no_address(self): + _, main = self._load() + + # No space is reserved for a note section and none of its bytes are loaded, so the address it + # would take belongs to the section that follows it. + note = main.sections_map[".note.gnu.property"] + self.assertEqual(note.type, "SHT_NOTE") + self.assertFalse(note.occupies_memory) + self.assertEqual(main.sections_map[".text"].vaddr, main.mapped_base) + + if __name__ == "__main__": unittest.main()