Add support for DT_RELR/SHT_RELR compressed relocation sections (#395)
authorAndreas Ziegler <andreas.ziegler@fau.de>
Mon, 14 Feb 2022 13:44:27 +0000 (14:44 +0100)
committerGitHub <noreply@github.com>
Mon, 14 Feb 2022 13:44:27 +0000 (05:44 -0800)
As more and more tools now support DT_RELR compressed relocations
(most notably, the just released GNU binutils 2.38 [0]), let's add
support for reading these relocations as well.

The original discussion about advantages of packe RELATIVE
relocations can be found at [1]. In a nutshell, the format
exploits the fact that RELATIVE relocations are often placed
next to each other and (for x86_64) stores up to 64 relocations
in two 8-byte words. In a regular .rela.dyn table, these would
take up 24 * 64 = 1536 bytes.

The compressed relocations work as follows:

The first word in the section describes a base address and
contains an offset for a relocation. This offset must always
lie at an even address. Following this entry can be one or
more bitmap(s) which have their least significant bit set to 1.
All other bits describe (in increasing order of significance) if
the following continuous offsets also contain a relocation. The
addends for existing relocations are stored at the corresponding
offsets in the file (that is, they work like REL relocations).
A good description of the history of this feature and its current
adoption is the following blog post [2].

[0]: https://lists.gnu.org/archive/html/info-gnu/2022-02/msg00009.html
[1]: https://groups.google.com/g/generic-abi/c/bX460iggiKg?pli=1
[2]: https://maskray.me/blog/2021-10-31-relative-relocations-and-relr

elftools/elf/descriptions.py
elftools/elf/elffile.py
elftools/elf/enums.py
elftools/elf/relocation.py
elftools/elf/structs.py
test/test_relr.py [new file with mode: 0644]
test/testfiles_for_unittests/lib_relro.so.elf [new file with mode: 0755]

index b14ea3e852147ff7288d4e23c7ab4237ed9f6b3e..0ccc9a19e6472ef708619b360aaed2fb856e000e 100644 (file)
@@ -412,6 +412,7 @@ _DESCR_SH_TYPE = dict(
     SHT_GNU_HASH='GNU_HASH',
     SHT_GROUP='GROUP',
     SHT_SYMTAB_SHNDX='SYMTAB SECTION INDICIES',
+    SHT_RELR='RELR',
     SHT_GNU_verdef='VERDEF',
     SHT_GNU_verneed='VERNEED',
     SHT_GNU_versym='VERSYM',
index e864a162551f95204c7b03f8d294f5d9744a4e40..244841a2e4cbf1abc24bbc4b219fdd93c5d9c7b3 100644 (file)
@@ -31,7 +31,8 @@ from .sections import (
         SymbolTableIndexSection, SUNWSyminfoTableSection, NullSection,
         NoteSection, StabSection, ARMAttributesSection)
 from .dynamic import DynamicSection, DynamicSegment
-from .relocation import RelocationSection, RelocationHandler
+from .relocation import (RelocationSection, RelocationHandler,
+        RelrRelocationSection)
 from .gnuversions import (
         GNUVerNeedSection, GNUVerDefSection,
         GNUVerSymSection)
@@ -595,6 +596,8 @@ class ELFFile(object):
             return self._make_elf_hash_section(section_header, name)
         elif sectype == 'SHT_GNU_HASH':
             return self._make_gnu_hash_section(section_header, name)
+        elif sectype == 'SHT_RELR':
+            return RelrRelocationSection(section_header, name, self)
         else:
             return Section(section_header, name, self)
 
index 61c3d42875ceed49b6bf1b4b26c3f1014aff7be8..8519f4e63e2a546c0dfdef850f40fe7f9d31c9ab 100644 (file)
@@ -295,7 +295,8 @@ ENUM_SH_TYPE_BASE = dict(
     SHT_PREINIT_ARRAY=16,
     SHT_GROUP=17,
     SHT_SYMTAB_SHNDX=18,
-    SHT_NUM=19,
+    SHT_RELR=19,
+    SHT_NUM=20,
     SHT_LOOS=0x60000000,
     SHT_GNU_ATTRIBUTES=0x6ffffff5,
     SHT_GNU_HASH=0x6ffffff6,
@@ -513,7 +514,11 @@ ENUM_D_TAG_COMMON = dict(
     DT_ENCODING=32,
     DT_PREINIT_ARRAY=32,
     DT_PREINIT_ARRAYSZ=33,
-    DT_NUM=34,
+    DT_SYMTAB_SHNDX=34,
+    DT_RELRSZ=35,
+    DT_RELR=36,
+    DT_RELRENT=37,
+    DT_NUM=38,
     DT_LOOS=0x6000000d,
     DT_ANDROID_REL=0x6000000f,
     DT_ANDROID_RELSZ=0x60000010,
index 8ca4ca1bc4e74350ba4eb69d3ef78a830eff5c9f..4008e282960f39c45120beffb350f4c4ae861a55 100644 (file)
@@ -15,6 +15,7 @@ from .enums import (
     ENUM_RELOC_TYPE_i386, ENUM_RELOC_TYPE_x64, ENUM_RELOC_TYPE_MIPS,
     ENUM_RELOC_TYPE_ARM, ENUM_RELOC_TYPE_AARCH64, ENUM_RELOC_TYPE_PPC64,
     ENUM_D_TAG)
+from ..construct import Container
 
 
 class Relocation(object):
@@ -106,6 +107,80 @@ class RelocationSection(Section, RelocationTable):
             'Expected sh_entsize of %s section to be %s' % (
                 header['sh_type'], self.entry_size))
 
+class RelrRelocationSection(Section):
+    """ RELR compressed relocation section. This stores relative relocations
+        in a compressed format. An entry with an even value serves as an
+        'anchor' that defines a base address. Following this entry are one or
+        more bitmaps for consecutive addresses after the anchor which determine
+        if the corresponding relocation exists (if the bit is 1) or if it is
+        skipped. Addends are stored at the respective addresses (as in REL
+        relocations).
+    """
+    def __init__(self, header, name, elffile):
+        Section.__init__(self, header, name, elffile)
+        self._offset = self['sh_offset']
+        self._size = self['sh_size']
+        self._relr_struct = self.elffile.structs.Elf_Relr
+        self._entrysize = self._relr_struct.sizeof()
+        self._cached_relocations = None
+
+    def iter_relocations(self):
+        """ Yield all the relocations in the section
+        """
+        limit = self._offset + self._size
+        relr = self._offset
+        # The addresses of relocations in a bitmap are calculated from a base
+        # value provided in an initial 'anchor' relocation.
+        base = None
+        while relr < limit:
+            entry = struct_parse(self._relr_struct,
+                                 self.elffile.stream,
+                                 stream_pos=relr)
+            entry_offset = entry['r_offset']
+            if (entry_offset & 1) == 0:
+                # We found an anchor, take the current value as the base address
+                # for the following bitmaps and move the 'where' pointer to the
+                # beginning of the first bitmap.
+                base = entry_offset
+                base += self._entrysize
+                yield Relocation(entry, self.elffile)
+            else:
+                # We're processing a bitmap.
+                elf_assert(base is not None, 'RELR bitmap without base address')
+                i = 0
+                while True:
+                    # Iterate over all bits except the least significant one.
+                    entry_offset = (entry_offset >> 1)
+                    if entry_offset == 0:
+                        break
+                    # if the current LSB is set, we have a relocation at the
+                    # corresponding address so generate a Relocation with the
+                    # matching offset
+                    if (entry_offset & 1) != 0:
+                        calc_offset = base + i * self._entrysize
+                        yield Relocation(Container(r_offset = calc_offset),
+                                         self.elffile)
+                    i += 1
+                # Advance 'base' past the current bitmap (8 == CHAR_BIT). There
+                # are 63 (or 31 for 32-bit ELFs) entries in each bitmap, and
+                # every bit corresponds to an ELF_addr-sized relocation.
+                base += (8 * self._entrysize - 1) * self.elffile.structs.Elf_addr('').sizeof()
+            # Advance to the next entry
+            relr += self._entrysize
+
+    def num_relocations(self):
+        """ Number of relocations in the section
+        """
+        if self._cached_relocations is None:
+            self._cached_relocations = list(self.iter_relocations())
+        return len(self._cached_relocations)
+
+    def get_relocation(self, n):
+        """ Get the relocation at index #n from the section (Relocation object)
+        """
+        if self._cached_relocations is None:
+            self._cached_relocations = list(self.iter_relocations())
+        return self._cached_relocations[n]
 
 class RelocationHandler(object):
     """ Handles the logic of relocations in ELF files.
index 6a1d2aaaf36022a9f498bc7c54bb1d987b8d9465..b437eec981f1750233c4bbb3e1877b04153457c1 100644 (file)
@@ -270,6 +270,12 @@ class ELFStructs(object):
                                *fields_and_addend
         )
 
+        # Elf32_Relr is typedef'd as Elf32_Word, Elf64_Relr as Elf64_Xword
+        # (see the glibc patch, for example:
+        # https://sourceware.org/pipermail/libc-alpha/2021-October/132029.html)
+        # For us, this is the same as self.Elf_addr (or self.Elf_xword).
+        self.Elf_Relr = Struct('Elf_Relr', self.Elf_addr('r_offset'))
+
     def _create_dyn(self):
         d_tag_dict = dict(ENUM_D_TAG_COMMON)
         if self.e_machine in ENUMMAP_EXTRA_D_TAG_MACHINE:
diff --git a/test/test_relr.py b/test/test_relr.py
new file mode 100644 (file)
index 0000000..69e39d6
--- /dev/null
@@ -0,0 +1,49 @@
+#-------------------------------------------------------------------------------
+# elftools tests
+#
+# Andreas Ziegler (andreas.ziegler@fau.de)
+# This code is in the public domain
+#-------------------------------------------------------------------------------
+# The lib_relro.so.elf file was generated as follows (on Debian 11):
+# $ cat lib_relro.c
+# int retfunc(){ return 1; }
+# int (*ptr1)() = retfunc;
+# int (*ptr2)() = retfunc;
+# <...>
+# int (*ptr100)() = retfunc;
+# $ clang-12 -c -o lib_relro.o -fPIC lib_relro.c
+# $ ld.lld-12 -o lib_relro.so.elf --pack-dyn-relocs=relr --shared -Bsymbolic-functions lib_relro.o
+
+import unittest
+import os
+
+from elftools.elf.elffile import ELFFile
+
+class TestRelr(unittest.TestCase):
+
+    def test_num_relocations(self):
+        """ Verify we can get the number of relocations in a RELR relocation
+            section.
+        """
+        path = os.path.join('test', 'testfiles_for_unittests',
+                            'lib_relro.so.elf')
+        with open(path, 'rb') as f:
+            elf = ELFFile(f)
+            relr_section = elf.get_section_by_name('.relr.dyn')
+            self.assertIsNotNone(relr_section)
+            self.assertEqual(relr_section.num_relocations(), 100)
+
+    def test_get_relocation(self):
+        """ Verify we can get a specific relocation in a RELR relocation
+            section.
+        """
+        path = os.path.join('test', 'testfiles_for_unittests',
+                            'lib_relro.so.elf')
+        with open(path, 'rb') as f:
+            elf = ELFFile(f)
+            relr_section = elf.get_section_by_name('.relr.dyn')
+            self.assertIsNotNone(relr_section)
+            reloc = relr_section.get_relocation(n=0)
+            self.assertEqual(reloc['r_offset'], 0x4540)
+            reloc = relr_section.get_relocation(n=65)
+            self.assertEqual(reloc['r_offset'], 0x4748)
diff --git a/test/testfiles_for_unittests/lib_relro.so.elf b/test/testfiles_for_unittests/lib_relro.so.elf
new file mode 100755 (executable)
index 0000000..16b8587
Binary files /dev/null and b/test/testfiles_for_unittests/lib_relro.so.elf differ