diff --git a/meson.build b/meson.build index 90705b3158..32af599f36 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 dd651bf18e..364471e6a2 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 4abd9d5c49..2fe8252551 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 79fa525175..685715f36e 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 d190db9f3f..de0c83ddb9 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 6e9eff0763..0000000000 --- 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 39026e1b42..f1fbf1e630 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 - - 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.") - - 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.") - - return True +def align_down(x: int, align: int) -> int: + return x & ~(align - 1) -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()) +def iter_copy_sections(elf: ELFFile) -> typing.Iterator[PeSection]: + 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 - - return pe_s - - -def copy_sections(elf: ELFFile, opt: PeOptionalHeader) -> typing.List[PeSection]: - sections = [] + # 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. for elf_s in elf.iter_sections(): - if not use_section(elf_s): + 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 + + if pe_s and pe_s.Characteristics != rwx: + yield pe_s + pe_s = None + + 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() + + if pe_s: + yield pe_s + + +def convert_sections(elf: ELFFile, opt: PeOptionalHeader) -> typing.List[PeSection]: + last_vma = 0 + sections = [] + + 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