]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
elf2efi: Rework ELF section conversion
authorJan Janssen <medhefgo@web.de>
Thu, 28 Sep 2023 14:09:42 +0000 (16:09 +0200)
committerJan Janssen <medhefgo@web.de>
Fri, 29 Sep 2023 14:56:30 +0000 (16:56 +0200)
The main reason we need to apply a whole lot of logic to the section
conversion logic is because PE sections have to be aligned to the page
size (although, currently not even EDK2 enforces this). The process of
achieving this with a linker script is fraught with errors, they are a
pain to set up correctly and suck in general. They are also not
supported by mold, which requires us to forcibly use bfd, which also
means that linker feature detection is easily at odds as meson has a
differnt idea of what linker is in use.

Instead of forcing a manual ELF segment layout with a linker script we
just let the linker do its thing. We then simply copy/concatenate the
sections while observing proper page boundaries.
Note that we could just copy the ELF load *segments* directly and
achieve the same result. Doing this manually allows us to strip sections
we don't need at runtime like the dynamic linking information (the
elf2efi conversion is effectively the dynamic loader).

Important sections like .sbat that we emit directly from code will
currently *not* be exposed as individual PE sections as they are
contained within the ELF segments. A future commit will fix this.

meson.build
src/boot/efi/log.c
src/boot/efi/meson.build
src/boot/efi/util.c
src/boot/efi/util.h
tools/elf2efi.lds [deleted file]
tools/elf2efi.py

index 90705b31583a79ea3c73169be03f1d3fd73297be..32af599f3667d6a1dbcb31e303050610ef661381 100644 (file)
@@ -1788,7 +1788,6 @@ conf.set10('ENABLE_UKIFY', want_ukify)
 
 ############################################################
 
-elf2efi_lds = project_source_root / 'tools/elf2efi.lds'
 elf2efi_py = find_program('tools/elf2efi.py')
 export_dbus_interfaces_py = find_program('tools/dbus_exporter.py')
 generate_gperfs = find_program('tools/generate-gperfs.py')
index dd651bf18e83c1c2248717935489c2fa771fbb0f..364471e6a280930f5da210b23f9b9fb79b1be8b1 100644 (file)
@@ -81,7 +81,7 @@ void __stack_chk_guard_init(void) {
                 (void) rng->GetRNG(rng, NULL, sizeof(__stack_chk_guard), (void *) &__stack_chk_guard);
         else
                 /* Better than no extra entropy. */
-                __stack_chk_guard ^= (intptr_t) &__ImageBase;
+                __stack_chk_guard ^= (intptr_t) __executable_start;
 }
 #endif
 
index 4abd9d5c49a8a5a692a809a6c2427d04a0278c31..2fe82525519404a13cde113b6e3269a29112488f 100644 (file)
@@ -167,7 +167,7 @@ efi_c_ld_args = [
 
         '-z', 'noexecstack',
         '-z', 'norelro',
-        '-T' + elf2efi_lds,
+        '-z', 'separate-code',
 ]
 
 # On CentOS 8 the nopack-relative-relocs linker flag is not supported, and we get:
@@ -316,7 +316,6 @@ foreach archspec : efi_archspecs
                 'c_args' : archspec['c_args'],
                 'link_args' : archspec['link_args'],
                 'link_with' : libefi,
-                'link_depends' : elf2efi_lds,
                 'gnu_symbol_visibility' : 'hidden',
                 'override_options' : efi_override_options,
                 'pie' : true,
index 79fa525175c472f553b0559a69228962acd8d4e6..685715f36e737e1a00da67c5506ee292a51ff9be 100644 (file)
@@ -555,7 +555,7 @@ uint64_t get_os_indications_supported(void) {
 
 __attribute__((noinline)) void notify_debugger(const char *identity, volatile bool wait) {
 #ifdef EFI_DEBUG
-        printf("%s@%p %s\n", identity, &__ImageBase, GIT_VERSION);
+        printf("%s@%p %s\n", identity, __executable_start, GIT_VERSION);
         if (wait)
                 printf("Waiting for debugger to attach...\n");
 
index d190db9f3fcc8e87a57bb5670596f6bc69ec80dd..de0c83ddb9b85296154f7749f3e2ba0edc862be2 100644 (file)
@@ -6,8 +6,8 @@
 #include "proto/file-io.h"
 #include "string-util-fundamental.h"
 
-/* This is provided by linker script. */
-extern uint8_t __ImageBase;
+/* This is provided by the linker. */
+extern uint8_t __executable_start[];
 
 static inline void free(void *p) {
         if (!p)
diff --git a/tools/elf2efi.lds b/tools/elf2efi.lds
deleted file mode 100644 (file)
index 6e9eff0..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-SECTIONS {
-        __ImageBase = .;
-
-        /* We skip the first page because the space will be occupied by the PE headers after conversion. */
-        . = CONSTANT(MAXPAGESIZE);
-        .text ALIGN(CONSTANT(MAXPAGESIZE)) : {
-                *(.text .text.*)
-        }
-
-        /* When linking a minimal addon stub, the linker can merge .text and .dynsym, creating a RWE
-         * segment, and then rejects it. Ensure there's a gap so that we end up with two separate segments.
-         * The alignments for the next sections are only applied if the section exists, so they are not
-         * enough, and we need to have this unconditional one. */
-        . = ALIGN(CONSTANT(MAXPAGESIZE));
-
-        .rodata ALIGN(CONSTANT(MAXPAGESIZE)) : {
-                *(.rodata .rodata.*)
-                *(.srodata .srodata.*)
-        }
-        .data ALIGN(CONSTANT(MAXPAGESIZE)) : {
-                *(.data .data.*)
-                *(.sdata .sdata.*)
-                *(.got .got.*)
-                *(.got.plt .got.plt.*)
-
-                /* EDK2 says some firmware cannot handle BSS sections properly. */
-                *(.bss .bss.*)
-                *(.sbss .sbss.*)
-                *(COMMON)
-        }
-
-        .sdmagic ALIGN(CONSTANT(MAXPAGESIZE)) : { *(.sdmagic) }
-        .osrel   ALIGN(CONSTANT(MAXPAGESIZE)) : { *(.osrel) }
-        .sbat    ALIGN(CONSTANT(MAXPAGESIZE)) : { *(.sbat) }
-
-        /* These are used for PE conversion and then discarded. */
-        .dynsym   : { *(.dynsym) }
-        .dynstr   : { *(.dynstr) }
-        .dynamic  : { *(.dynamic) }
-        .rel.dyn  : { *(.rel.dyn) }
-        .rela.dyn : { *(.rela.dyn) }
-
-        /* These aren't needed and could be discarded. Just in case that they're useful to the debugger
-         * we keep them, but move them out of the way to keep the PE binary more compact. */
-        .ARM.exidx         : { *(.ARM.exidx) }
-        .eh_frame          : { *(.eh_frame) }
-        .eh_frame_hdr      : { *(.eh_frame_hdr) }
-        .gnu.hash          : { *(.gnu.hash) }
-        .hash              : { *(.hash) }
-        .note.gnu.build-id : { *(.note.gnu.build-id ) }
-
-        /DISCARD/ : {
-                *(.ARM.attributes)
-                *(.comment)
-                *(.note.*)
-                *(.riscv.attributes)
-        }
-}
index 39026e1b424d9099d50aac68122629e98554611a..f1fbf1e630f5f8d6e054112ffe4887abeb715019 100755 (executable)
@@ -39,7 +39,7 @@ from ctypes import (
 )
 
 from elftools.elf.constants import SH_FLAGS
-from elftools.elf.elffile import ELFFile, Section as ELFSection
+from elftools.elf.elffile import ELFFile
 from elftools.elf.enums import (
     ENUM_DT_FLAGS_1,
     ENUM_RELOC_TYPE_AARCH64,
@@ -204,6 +204,30 @@ assert sizeof(PeCoffHeader) == 20
 assert sizeof(PeOptionalHeader32) == 224
 assert sizeof(PeOptionalHeader32Plus) == 240
 
+PE_CHARACTERISTICS_RX = 0x60000020  # CNT_CODE|MEM_READ|MEM_EXECUTE
+PE_CHARACTERISTICS_RW = 0xC0000040  # CNT_INITIALIZED_DATA|MEM_READ|MEM_WRITE
+PE_CHARACTERISTICS_R = 0x40000040  # CNT_INITIALIZED_DATA|MEM_READ
+
+IGNORE_SECTIONS = [
+    ".eh_frame",
+    ".eh_frame_hdr",
+    ".ARM.exidx",
+]
+
+IGNORE_SECTION_TYPES = [
+    "SHT_DYNAMIC",
+    "SHT_DYNSYM",
+    "SHT_GNU_ATTRIBUTES",
+    "SHT_GNU_HASH",
+    "SHT_HASH",
+    "SHT_NOTE",
+    "SHT_REL",
+    "SHT_RELA",
+    "SHT_RELR",
+    "SHT_STRTAB",
+    "SHT_SYMTAB",
+]
+
 # EFI mandates 4KiB memory pages.
 SECTION_ALIGNMENT = 4096
 FILE_ALIGNMENT = 512
@@ -217,79 +241,78 @@ def align_to(x: int, align: int) -> int:
     return (x + align - 1) & ~(align - 1)
 
 
-def use_section(elf_s: ELFSection) -> bool:
-    # These sections are either needed during conversion to PE or are otherwise not needed
-    # in the final PE image.
-    IGNORE_SECTIONS = [
-        ".ARM.exidx",
-        ".dynamic",
-        ".dynstr",
-        ".dynsym",
-        ".eh_frame_hdr",
-        ".eh_frame",
-        ".gnu.hash",
-        ".hash",
-        ".note.gnu.build-id",
-        ".rel.dyn",
-        ".rela.dyn",
-    ]
-
-    # Known sections we care about and want to be in the final PE.
-    COPY_SECTIONS = [
-        ".data",
-        ".osrel",
-        ".rodata",
-        ".sbat",
-        ".sdmagic",
-        ".text",
-    ]
-
-    # By only dealing with allocating sections we effectively filter out debug sections.
-    if not elf_s["sh_flags"] & SH_FLAGS.SHF_ALLOC:
-        return False
+def align_down(x: int, align: int) -> int:
+    return x & ~(align - 1)
 
-    if elf_s.name in IGNORE_SECTIONS:
-        return False
 
-    # For paranoia we only handle sections we know of. Any new sections that come up should
-    # be added to IGNORE_SECTIONS/COPY_SECTIONS and/or the linker script.
-    if elf_s.name not in COPY_SECTIONS:
-        raise RuntimeError(f"Unknown section {elf_s.name}, refusing.")
+def iter_copy_sections(elf: ELFFile) -> typing.Iterator[PeSection]:
+    pe_s = None
 
-    if elf_s["sh_addr"] % SECTION_ALIGNMENT != 0:
-        raise RuntimeError(f"Section {elf_s.name} is not aligned.")
-    if len(elf_s.name) > 8:
-        raise RuntimeError(f"ELF section name {elf_s.name} too long.")
+    # This is essentially the same as copying by ELF load segments, except that we assemble them
+    # manually, so that we can easily strip unwanted sections. We try to only discard things we know
+    # about so that there are no surprises.
 
-    return True
+    for elf_s in elf.iter_sections():
+        if (
+            elf_s["sh_flags"] & SH_FLAGS.SHF_ALLOC == 0
+            or elf_s["sh_type"] in IGNORE_SECTION_TYPES
+            or elf_s.name in IGNORE_SECTIONS
+        ):
+            continue
+        if elf_s["sh_type"] not in ["SHT_PROGBITS", "SHT_NOBITS"]:
+            raise RuntimeError(f"Unknown section {elf_s.name}.")
 
+        if elf_s["sh_flags"] & SH_FLAGS.SHF_EXECINSTR:
+            rwx = PE_CHARACTERISTICS_RX
+        elif elf_s["sh_flags"] & SH_FLAGS.SHF_WRITE:
+            rwx = PE_CHARACTERISTICS_RW
+        else:
+            rwx = PE_CHARACTERISTICS_R
 
-def convert_elf_section(elf_s: ELFSection) -> PeSection:
-    pe_s = PeSection()
-    pe_s.Name = elf_s.name.encode()
-    pe_s.VirtualSize = elf_s.data_size
-    pe_s.VirtualAddress = elf_s["sh_addr"]
-    pe_s.SizeOfRawData = align_to(elf_s.data_size, FILE_ALIGNMENT)
-    pe_s.data = bytearray(elf_s.data())
+        if pe_s and pe_s.Characteristics != rwx:
+            yield pe_s
+            pe_s = None
 
-    if elf_s["sh_flags"] & SH_FLAGS.SHF_EXECINSTR:
-        pe_s.Characteristics = 0x60000020  # CNT_CODE|MEM_READ|MEM_EXECUTE
-    elif elf_s["sh_flags"] & SH_FLAGS.SHF_WRITE:
-        pe_s.Characteristics = 0xC0000040  # CNT_INITIALIZED_DATA|MEM_READ|MEM_WRITE
-    else:
-        pe_s.Characteristics = 0x40000040  # CNT_INITIALIZED_DATA|MEM_READ
+        if pe_s:
+            # Insert padding to properly align the section.
+            pad_len = elf_s["sh_addr"] - pe_s.VirtualAddress - len(pe_s.data)
+            pe_s.data += bytearray(pad_len) + elf_s.data()
+        else:
+            pe_s = PeSection()
+            pe_s.VirtualAddress = elf_s["sh_addr"]
+            pe_s.Characteristics = rwx
+            pe_s.data = elf_s.data()
 
-    return pe_s
+    if pe_s:
+        yield pe_s
 
 
-def copy_sections(elf: ELFFile, opt: PeOptionalHeader) -> typing.List[PeSection]:
+def convert_sections(elf: ELFFile, opt: PeOptionalHeader) -> typing.List[PeSection]:
+    last_vma = 0
     sections = []
 
-    for elf_s in elf.iter_sections():
-        if not use_section(elf_s):
-            continue
+    for pe_s in iter_copy_sections(elf):
+        # Truncate the VMA to the nearest page and insert appropriate padding. This should not
+        # cause any overlap as this is pretty much how ELF *segments* are loaded/mmapped anyways.
+        # The ELF sections inside should also be properly aligned as we reuse the ELF VMA layout
+        # for the PE image.
+        vma = pe_s.VirtualAddress
+        pe_s.VirtualAddress = align_down(vma, SECTION_ALIGNMENT)
+        pe_s.data = bytearray(vma - pe_s.VirtualAddress) + pe_s.data
+
+        pe_s.VirtualSize = len(pe_s.data)
+        pe_s.SizeOfRawData = align_to(len(pe_s.data), FILE_ALIGNMENT)
+        pe_s.Name = {
+            PE_CHARACTERISTICS_RX: b".text",
+            PE_CHARACTERISTICS_RW: b".data",
+            PE_CHARACTERISTICS_R: b".rodata",
+        }[pe_s.Characteristics]
+
+        # This can happen if not building with `-z separate-code`.
+        if pe_s.VirtualAddress < last_vma:
+            raise RuntimeError("Overlapping PE sections.")
+        last_vma = pe_s.VirtualAddress + pe_s.VirtualSize
 
-        pe_s = convert_elf_section(elf_s)
         if pe_s.Name == b".text":
             opt.BaseOfCode = pe_s.VirtualAddress
             opt.SizeOfCode += pe_s.VirtualSize
@@ -333,7 +356,7 @@ def apply_elf_relative_relocation(
 def convert_elf_reloc_table(
     elf: ELFFile,
     elf_reloc_table: ElfRelocationTable,
-    image_base: int,
+    elf_image_base: int,
     sections: typing.List[PeSection],
     pe_reloc_blocks: typing.Dict[int, PeRelocationBlock],
 ):
@@ -361,7 +384,7 @@ def convert_elf_reloc_table(
 
         if reloc["r_info_type"] == RELATIVE_RELOC:
             apply_elf_relative_relocation(
-                reloc, image_base, sections, elf.elfclass // 8
+                reloc, elf_image_base, sections, elf.elfclass // 8
             )
 
             # Now that the ELF relocation has been applied, we can create a PE relocation.
@@ -381,7 +404,10 @@ def convert_elf_reloc_table(
 
 
 def convert_elf_relocations(
-    elf: ELFFile, opt: PeOptionalHeader, sections: typing.List[PeSection]
+    elf: ELFFile,
+    opt: PeOptionalHeader,
+    sections: typing.List[PeSection],
+    minimum_sections: int,
 ) -> typing.Optional[PeSection]:
     dynamic = elf.get_section_by_name(".dynamic")
     if dynamic is None:
@@ -391,14 +417,42 @@ def convert_elf_relocations(
     if not flags_tag["d_val"] & ENUM_DT_FLAGS_1["DF_1_PIE"]:
         raise RuntimeError("ELF file is not a PIE.")
 
+    opt.SizeOfHeaders = align_to(
+        PE_OFFSET
+        + len(PE_MAGIC)
+        + sizeof(PeCoffHeader)
+        + sizeof(opt)
+        + sizeof(PeSection) * max(len(sections) + 1, minimum_sections),
+        FILE_ALIGNMENT,
+    )
+
+    # We use the basic VMA layout from the ELF image in the PE image. This could cause the first
+    # section to overlap the PE image headers during runtime at VMA 0. We can simply apply a fixed
+    # offset relative to the PE image base when applying/converting ELF relocations. Afterwards we
+    # just have to apply the offset to the PE addresses so that the PE relocations work correctly on
+    # the ELF portions of the image.
+    segment_offset = 0
+    if sections[0].VirtualAddress < opt.SizeOfHeaders:
+        segment_offset = align_to(
+            opt.SizeOfHeaders - sections[0].VirtualAddress, SECTION_ALIGNMENT
+        )
+
+    opt.AddressOfEntryPoint = elf["e_entry"] + segment_offset
+    opt.BaseOfCode += segment_offset
+    if isinstance(opt, PeOptionalHeader32):
+        opt.BaseOfData += segment_offset
+
     pe_reloc_blocks: typing.Dict[int, PeRelocationBlock] = {}
     for reloc_type, reloc_table in dynamic.get_relocation_tables().items():
         if reloc_type not in ["REL", "RELA"]:
             raise RuntimeError("Unsupported relocation type {elf_reloc_type}.")
         convert_elf_reloc_table(
-            elf, reloc_table, opt.ImageBase, sections, pe_reloc_blocks
+            elf, reloc_table, opt.ImageBase + segment_offset, sections, pe_reloc_blocks
         )
 
+    for pe_s in sections:
+        pe_s.VirtualAddress += segment_offset
+
     if len(pe_reloc_blocks) == 0:
         return None
 
@@ -413,6 +467,7 @@ def convert_elf_relocations(
             n_relocs += 1
             block.entries.append(PeRelocationEntry())
 
+        block.PageRVA += segment_offset
         block.BlockSize = (
             sizeof(PeRelocationBlock) + sizeof(PeRelocationEntry) * n_relocs
         )
@@ -495,8 +550,8 @@ def elf2efi(args: argparse.Namespace):
     else:
         opt.ImageBase = (0x100000000 + opt.ImageBase) & 0x1FFFF0000
 
-    sections = copy_sections(elf, opt)
-    pe_reloc_s = convert_elf_relocations(elf, opt, sections)
+    sections = convert_sections(elf, opt)
+    pe_reloc_s = convert_elf_relocations(elf, opt, sections, args.minimum_sections)
 
     coff.Machine = pe_arch
     coff.NumberOfSections = len(sections)
@@ -506,7 +561,6 @@ def elf2efi(args: argparse.Namespace):
     # and (32BIT_MACHINE or LARGE_ADDRESS_AWARE)
     coff.Characteristics = 0x30E if elf.elfclass == 32 else 0x22E
 
-    opt.AddressOfEntryPoint = elf["e_entry"]
     opt.SectionAlignment = SECTION_ALIGNMENT
     opt.FileAlignment = FILE_ALIGNMENT
     opt.MajorImageVersion = args.version_major
@@ -518,15 +572,6 @@ def elf2efi(args: argparse.Namespace):
     opt.SizeOfImage = align_to(
         sections[-1].VirtualAddress + sections[-1].VirtualSize, SECTION_ALIGNMENT
     )
-
-    opt.SizeOfHeaders = align_to(
-        PE_OFFSET
-        + len(PE_MAGIC)
-        + sizeof(PeCoffHeader)
-        + coff.SizeOfOptionalHeader
-        + sizeof(PeSection) * max(coff.NumberOfSections, args.minimum_sections),
-        FILE_ALIGNMENT,
-    )
     # DYNAMIC_BASE|NX_COMPAT|HIGH_ENTROPY_VA or DYNAMIC_BASE|NX_COMPAT
     opt.DllCharacteristics = 0x160 if elf.elfclass == 64 else 0x140