From: Jan Janssen Date: Thu, 28 Sep 2023 14:09:42 +0000 (+0200) Subject: elf2efi: Rework ELF section conversion X-Git-Tag: v255-rc1~379^2~6 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=142f0c61a37091e233b80f02375cff1114dab24a;p=thirdparty%2Fsystemd.git elf2efi: Rework ELF section conversion 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. --- diff --git a/meson.build b/meson.build index 90705b31583..32af599f366 100644 --- a/meson.build +++ b/meson.build @@ -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') diff --git a/src/boot/efi/log.c b/src/boot/efi/log.c index dd651bf18e8..364471e6a28 100644 --- a/src/boot/efi/log.c +++ b/src/boot/efi/log.c @@ -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 diff --git a/src/boot/efi/meson.build b/src/boot/efi/meson.build index 4abd9d5c49a..2fe82525519 100644 --- a/src/boot/efi/meson.build +++ b/src/boot/efi/meson.build @@ -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, diff --git a/src/boot/efi/util.c b/src/boot/efi/util.c index 79fa525175c..685715f36e7 100644 --- a/src/boot/efi/util.c +++ b/src/boot/efi/util.c @@ -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"); diff --git a/src/boot/efi/util.h b/src/boot/efi/util.h index d190db9f3fc..de0c83ddb9b 100644 --- a/src/boot/efi/util.h +++ b/src/boot/efi/util.h @@ -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 index 6e9eff0763a..00000000000 --- a/tools/elf2efi.lds +++ /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) - } -} diff --git a/tools/elf2efi.py b/tools/elf2efi.py index 39026e1b424..f1fbf1e630f 100755 --- a/tools/elf2efi.py +++ b/tools/elf2efi.py @@ -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