From 142f0c61a37091e233b80f02375cff1114dab24a Mon Sep 17 00:00:00 2001 From: Jan Janssen Date: Thu, 28 Sep 2023 16:09:42 +0200 Subject: [PATCH 1/7] 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. --- meson.build | 1 - src/boot/efi/log.c | 2 +- src/boot/efi/meson.build | 3 +- src/boot/efi/util.c | 2 +- src/boot/efi/util.h | 4 +- tools/elf2efi.lds | 58 ----------- tools/elf2efi.py | 211 ++++++++++++++++++++++++--------------- 7 files changed, 133 insertions(+), 148 deletions(-) delete mode 100644 tools/elf2efi.lds 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 - - 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 From 5713c50d84296aa3d986f31fed04f970f93965d9 Mon Sep 17 00:00:00 2001 From: Jan Janssen Date: Thu, 28 Sep 2023 16:22:13 +0200 Subject: [PATCH 2/7] elf2efi: Check ELF image base if possible --- tools/elf2efi.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tools/elf2efi.py b/tools/elf2efi.py index f1fbf1e630f..8dbf881a0c5 100755 --- a/tools/elf2efi.py +++ b/tools/elf2efi.py @@ -348,7 +348,6 @@ def apply_elf_relative_relocation( addend = target.data[addend_offset : addend_offset + addend_size] addend = int.from_bytes(addend, byteorder="little") - # This currently assumes that the ELF file has an image base of 0. value = (image_base + addend).to_bytes(addend_size, byteorder="little") target.data[addend_offset : addend_offset + addend_size] = value @@ -417,6 +416,13 @@ 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.") + # This checks that the ELF image base is 0. + symtab = elf.get_section_by_name(".symtab") + if symtab: + exe_start = symtab.get_symbol_by_name("__executable_start") + if exe_start and exe_start[0]["st_value"] != 0: + raise RuntimeError("Unexpected ELF image base.") + opt.SizeOfHeaders = align_to( PE_OFFSET + len(PE_MAGIC) From 7d6fd7f0993c0f3d05da2b6b8aefca38a3bd7b60 Mon Sep 17 00:00:00 2001 From: Jan Janssen Date: Fri, 22 Sep 2023 12:13:09 +0200 Subject: [PATCH 3/7] elf2efi: Add next_section_address helper --- tools/elf2efi.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tools/elf2efi.py b/tools/elf2efi.py index 8dbf881a0c5..ce5b04e9945 100755 --- a/tools/elf2efi.py +++ b/tools/elf2efi.py @@ -245,6 +245,12 @@ def align_down(x: int, align: int) -> int: return x & ~(align - 1) +def next_section_address(sections: typing.List[PeSection]) -> int: + return align_to( + sections[-1].VirtualAddress + sections[-1].VirtualSize, SECTION_ALIGNMENT + ) + + def iter_copy_sections(elf: ELFFile) -> typing.Iterator[PeSection]: pe_s = None @@ -484,11 +490,9 @@ def convert_elf_relocations( pe_reloc_s = PeSection() pe_reloc_s.Name = b".reloc" pe_reloc_s.data = data + pe_reloc_s.VirtualAddress = next_section_address(sections) pe_reloc_s.VirtualSize = len(data) pe_reloc_s.SizeOfRawData = align_to(len(data), FILE_ALIGNMENT) - pe_reloc_s.VirtualAddress = align_to( - sections[-1].VirtualAddress + sections[-1].VirtualSize, SECTION_ALIGNMENT - ) # CNT_INITIALIZED_DATA|MEM_READ|MEM_DISCARDABLE pe_reloc_s.Characteristics = 0x42000040 @@ -575,9 +579,8 @@ def elf2efi(args: argparse.Namespace): opt.MinorSubsystemVersion = args.efi_minor opt.Subsystem = args.subsystem opt.Magic = 0x10B if elf.elfclass == 32 else 0x20B - opt.SizeOfImage = align_to( - sections[-1].VirtualAddress + sections[-1].VirtualSize, SECTION_ALIGNMENT - ) + opt.SizeOfImage = next_section_address(sections) + # DYNAMIC_BASE|NX_COMPAT|HIGH_ENTROPY_VA or DYNAMIC_BASE|NX_COMPAT opt.DllCharacteristics = 0x160 if elf.elfclass == 64 else 0x140 From 898e9edc469f87fdb6018128bac29eef0a5fe698 Mon Sep 17 00:00:00 2001 From: Jan Janssen Date: Fri, 22 Sep 2023 12:15:55 +0200 Subject: [PATCH 4/7] elf2efi: Add --copy-sections option This makes the special PE sections available again in our output EFI images. Since the compiler provides no way to mark a section as not allocated, we use GNU assembler syntax to emit the sections instead. This ensures the section data isn't emitted twice as load segments will only contain allocating input sections. --- src/boot/efi/addon.c | 5 ++--- src/boot/efi/boot.c | 17 ++++++++------- src/boot/efi/meson.build | 1 + src/boot/efi/stub.c | 2 +- src/fundamental/macro-fundamental.h | 10 +++++++-- tools/elf2efi.py | 33 +++++++++++++++++++++++++++++ 6 files changed, 54 insertions(+), 14 deletions(-) diff --git a/src/boot/efi/addon.c b/src/boot/efi/addon.c index 53e8812205a..95b29daf551 100644 --- a/src/boot/efi/addon.c +++ b/src/boot/efi/addon.c @@ -4,12 +4,11 @@ #include "version.h" /* Magic string for recognizing our own binaries */ -_used_ _section_(".sdmagic") static const char magic[] = - "#### LoaderInfo: systemd-addon " GIT_VERSION " ####"; +DECLARE_NOALLOC_SECTION(".sdmagic", "#### LoaderInfo: systemd-addon " GIT_VERSION " ####"); /* This is intended to carry data, not to be executed */ EFIAPI EFI_STATUS efi_main(EFI_HANDLE image, EFI_SYSTEM_TABLE *system_table); EFIAPI EFI_STATUS efi_main(EFI_HANDLE image, EFI_SYSTEM_TABLE *system_table) { - return EFI_UNSUPPORTED; + return EFI_UNSUPPORTED; } diff --git a/src/boot/efi/boot.c b/src/boot/efi/boot.c index 39b1c9abdba..6dad2bc3c4a 100644 --- a/src/boot/efi/boot.c +++ b/src/boot/efi/boot.c @@ -26,14 +26,15 @@ #include "vmm.h" /* Magic string for recognizing our own binaries */ -_used_ _section_(".sdmagic") static const char magic[] = - "#### LoaderInfo: systemd-boot " GIT_VERSION " ####"; +#define SD_MAGIC "#### LoaderInfo: systemd-boot " GIT_VERSION " ####" +DECLARE_NOALLOC_SECTION(".sdmagic", SD_MAGIC); /* Makes systemd-boot available from \EFI\Linux\ for testing purposes. */ -_used_ _section_(".osrel") static const char osrel[] = - "ID=systemd-boot\n" - "VERSION=\"" GIT_VERSION "\"\n" - "NAME=\"systemd-boot " GIT_VERSION "\"\n"; +DECLARE_NOALLOC_SECTION( + ".osrel", + "ID=systemd-boot\n" + "VERSION=\"" GIT_VERSION "\"\n" + "NAME=\"systemd-boot " GIT_VERSION "\"\n"); DECLARE_SBAT(SBAT_BOOT_SECTION_TEXT); @@ -1890,14 +1891,14 @@ static bool is_sd_boot(EFI_FILE *root_dir, const char16_t *loader_path) { assert(loader_path); err = pe_file_locate_sections(root_dir, loader_path, sections, &offset, &size); - if (err != EFI_SUCCESS || size != sizeof(magic)) + if (err != EFI_SUCCESS || size != sizeof(SD_MAGIC)) return false; err = file_read(root_dir, loader_path, offset, size, &content, &read); if (err != EFI_SUCCESS || size != read) return false; - return memcmp(content, magic, sizeof(magic)) == 0; + return memcmp(content, SD_MAGIC, sizeof(SD_MAGIC)) == 0; } static ConfigEntry *config_entry_add_loader_auto( diff --git a/src/boot/efi/meson.build b/src/boot/efi/meson.build index 2fe82525519..1591ff7f096 100644 --- a/src/boot/efi/meson.build +++ b/src/boot/efi/meson.build @@ -361,6 +361,7 @@ foreach efi_elf_binary : efi_elf_binaries '--efi-minor=1', '--subsystem=10', '--minimum-sections=' + minimum_sections, + '--copy-sections=.sbat,.sdmagic,.osrel', '@INPUT@', '@OUTPUT@', ]) diff --git a/src/boot/efi/stub.c b/src/boot/efi/stub.c index 6cd5ccb5d44..493e7ed4c2e 100644 --- a/src/boot/efi/stub.c +++ b/src/boot/efi/stub.c @@ -21,7 +21,7 @@ #include "vmm.h" /* magic string to find in the binary image */ -_used_ _section_(".sdmagic") static const char magic[] = "#### LoaderInfo: systemd-stub " GIT_VERSION " ####"; +DECLARE_NOALLOC_SECTION(".sdmagic", "#### LoaderInfo: systemd-stub " GIT_VERSION " ####"); DECLARE_SBAT(SBAT_STUB_SECTION_TEXT); diff --git a/src/fundamental/macro-fundamental.h b/src/fundamental/macro-fundamental.h index 7367bcb4117..02412705609 100644 --- a/src/fundamental/macro-fundamental.h +++ b/src/fundamental/macro-fundamental.h @@ -396,9 +396,15 @@ static inline size_t ALIGN_TO(size_t l, size_t ali) { type name[]; \ } +/* Declares an ELF read-only string section that does not occupy memory at runtime. */ +#define DECLARE_NOALLOC_SECTION(name, text) \ + asm(".pushsection " name ",\"S\"\n\t" \ + ".ascii " STRINGIFY(text) "\n\t" \ + ".zero 1\n\t" \ + ".popsection\n") + #ifdef SBAT_DISTRO - #define DECLARE_SBAT(text) \ - static const char sbat[] _used_ _section_(".sbat") = (text) + #define DECLARE_SBAT(text) DECLARE_NOALLOC_SECTION(".sbat", text) #else #define DECLARE_SBAT(text) #endif diff --git a/tools/elf2efi.py b/tools/elf2efi.py index ce5b04e9945..1e74dcfff2b 100755 --- a/tools/elf2efi.py +++ b/tools/elf2efi.py @@ -333,6 +333,32 @@ def convert_sections(elf: ELFFile, opt: PeOptionalHeader) -> typing.List[PeSecti return sections +def copy_sections( + elf: ELFFile, + opt: PeOptionalHeader, + input_names: str, + sections: typing.List[PeSection], +): + for name in input_names.split(","): + elf_s = elf.get_section_by_name(name) + if not elf_s: + continue + if elf_s.data_alignment > 1 and SECTION_ALIGNMENT % elf_s.data_alignment != 0: + raise RuntimeError(f"ELF section {name} is not aligned.") + if elf_s["sh_flags"] & (SH_FLAGS.SHF_EXECINSTR | SH_FLAGS.SHF_WRITE) != 0: + raise RuntimeError(f"ELF section {name} is not read-only data.") + + pe_s = PeSection() + pe_s.Name = name.encode() + pe_s.data = elf_s.data() + pe_s.VirtualAddress = next_section_address(sections) + pe_s.VirtualSize = len(elf_s.data()) + pe_s.SizeOfRawData = align_to(len(elf_s.data()), FILE_ALIGNMENT) + pe_s.Characteristics = PE_CHARACTERISTICS_R + opt.SizeOfInitializedData += pe_s.VirtualSize + sections.append(pe_s) + + def apply_elf_relative_relocation( reloc: ElfRelocation, image_base: int, @@ -561,6 +587,7 @@ def elf2efi(args: argparse.Namespace): opt.ImageBase = (0x100000000 + opt.ImageBase) & 0x1FFFF0000 sections = convert_sections(elf, opt) + copy_sections(elf, opt, args.copy_sections, sections) pe_reloc_s = convert_elf_relocations(elf, opt, sections, args.minimum_sections) coff.Machine = pe_arch @@ -647,6 +674,12 @@ def main(): default=0, help="Minimum number of sections to leave space for", ) + parser.add_argument( + "--copy-sections", + type=str, + default="", + help="Copy these sections if found", + ) elf2efi(parser.parse_args()) From 0b042d3de8e88effcd48255e4f17fa72ac2816bd Mon Sep 17 00:00:00 2001 From: Jan Janssen Date: Fri, 22 Sep 2023 14:41:47 +0200 Subject: [PATCH 5/7] boot: Lift linker requirements The biggest reason for forcing bfd was the use of linker scrips. Since we don't rely on those anymore we can lift the requirement. The biggest issue is gold as it does not understand -static-pie. Given that it's pretty much on life support it's safe to just declare it not supported anymore. Don't link addons with libefi as clang/lld is sometimes very eager to include memset etc., causing needless binary bloat and link errors with LTO. Fixes: #29165 --- src/boot/efi/meson.build | 40 +++++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/src/boot/efi/meson.build b/src/boot/efi/meson.build index 1591ff7f096..dfb8700547d 100644 --- a/src/boot/efi/meson.build +++ b/src/boot/efi/meson.build @@ -147,10 +147,6 @@ if get_option('mode') == 'developer' and get_option('debug') endif efi_c_ld_args = [ - # We only support bfd. gold is going away, lld has issues with LTO on x86 - # and mold does not support linker scripts. - '-fuse-ld=bfd', - '-lgcc', '-nostdlib', '-static-pie', @@ -170,9 +166,10 @@ efi_c_ld_args = [ '-z', 'separate-code', ] -# On CentOS 8 the nopack-relative-relocs linker flag is not supported, and we get: -# /usr/bin/ld.bfd: warning: -z nopack-relative-relocs ignored -efi_c_ld_args += cc.get_supported_link_arguments('-Wl,-z,nopack-relative-relocs') +efi_c_ld_args += cc.get_supported_link_arguments( + # binutils >= 2.38 + '-Wl,-z,nopack-relative-relocs', +) # efi_c_args is explicitly passed to targets so that they can override distro-provided flags # that should not be used for EFI binaries. @@ -213,11 +210,35 @@ efi_arch_c_args = { efi_arch_c_ld_args = { # libgcc is not compiled with -fshort-wchar, but it does not use it anyways, # so it's fine to link against it. - 'arm' : ['-Wl,--no-wchar-size-warning'], + 'arm' : cc.get_supported_link_arguments('-Wl,--no-wchar-size-warning'), 'x86_64' : ['-m64'], 'x86' : ['-m32'], } +linker_sanity_code = 'void a(void) {}; void _start(void) { a(); }' +linker_sanity_args = ['-nostdlib', '-Wl,--fatal-warnings'] +if not cc.links(linker_sanity_code, + name : 'linker supports -static-pie', + args : [linker_sanity_args, '-static-pie']) + error('Linker does not support -static-pie.') +endif + +# https://github.com/llvm/llvm-project/issues/67152 +if not cc.links(linker_sanity_code, + name : 'linker supports LTO with -nostdlib', + args : [linker_sanity_args, '-flto']) + efi_c_args += '-fno-lto' + efi_c_ld_args += '-fno-lto' +endif + +# https://github.com/llvm/llvm-project/issues/61101 +if efi_cpu_family_alt == 'x86' and not cc.links(linker_sanity_code, + name : 'linker supports LTO with -nostdlib (x86)', + args : [linker_sanity_args, '-flto', '-m32']) + efi_arch_c_args += { 'x86' : efi_arch_c_args['x86'] + '-fno-lto' } + efi_arch_c_ld_args += { 'x86' : efi_arch_c_ld_args['x86'] + '-fno-lto' } +endif + ############################################################ libefi_sources = files( @@ -315,7 +336,6 @@ foreach archspec : efi_archspecs 'include_directories' : efi_includes, 'c_args' : archspec['c_args'], 'link_args' : archspec['link_args'], - 'link_with' : libefi, 'gnu_symbol_visibility' : 'hidden', 'override_options' : efi_override_options, 'pie' : true, @@ -324,12 +344,14 @@ foreach archspec : efi_archspecs efi_elf_binaries += executable( 'systemd-boot' + archspec['arch'], sources : [systemd_boot_sources, version_h], + link_with : libefi, name_suffix : 'elf', kwargs : kwargs) efi_elf_binaries += executable( 'linux' + archspec['arch'], sources : [stub_sources, version_h], + link_with : libefi, name_suffix : 'elf.stub', kwargs : kwargs) From 79ae0d0b3e15aa32ed9635ce4c485b1beda3b6ed Mon Sep 17 00:00:00 2001 From: Jan Janssen Date: Mon, 25 Sep 2023 17:24:26 +0200 Subject: [PATCH 6/7] ci: Update compiler build matrix Given that gold is pretty much unmaintained and does not support `-static-pie` for bootloader components it should be safe to drop. Also switch to clang-17 while we're at it. --- .github/workflows/build_test.sh | 23 +++++++++++------------ .github/workflows/build_test.yml | 8 ++++---- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/.github/workflows/build_test.sh b/.github/workflows/build_test.sh index c0872117abc..176764246fc 100755 --- a/.github/workflows/build_test.sh +++ b/.github/workflows/build_test.sh @@ -45,7 +45,7 @@ PACKAGES=( libxkbcommon-dev libxtables-dev libzstd-dev - mold + # mold mount net-tools python3-evdev @@ -68,6 +68,14 @@ LINKER="${LINKER:?}" CRYPTOLIB="${CRYPTOLIB:?}" RELEASE="$(lsb_release -cs)" +# mold-2.2.0+ fixes some bugs breaking bootloader builds. +# TODO: Switch to distro mold with ubuntu-24.04 +if [[ "$LINKER" == mold ]]; then + wget https://github.com/rui314/mold/releases/download/v2.2.0/mold-2.2.0-x86_64-linux.tar.gz + echo "d66e0230c562c2ba0e0b789cc5034e0fa2369cc843d0154920de4269cd94afeb mold-2.2.0-x86_64-linux.tar.gz" | sha256sum -c + sudo tar -xz -C /usr --strip-components=1 -f mold-2.2.0-x86_64-linux.tar.gz +fi + # Note: As we use postfixed clang/gcc binaries, we need to override $AR # as well, otherwise meson falls back to ar from binutils which # doesn't work with LTO @@ -123,20 +131,11 @@ ninja --version for args in "${ARGS[@]}"; do SECONDS=0 - # mold < 1.1 does not support LTO. - if dpkg --compare-versions "$(dpkg-query --showformat='${Version}' --show mold)" ge 1.1; then - fatal "Newer mold version detected, please remove this workaround." - elif [[ "$args" == *"-Db_lto=true"* ]]; then - LD="gold" - else - LD="$LINKER" - fi - info "Checking build with $args" # shellcheck disable=SC2086 if ! AR="$AR" \ - CC="$CC" CC_LD="$LD" CFLAGS="-Werror" \ - CXX="$CXX" CXX_LD="$LD" CXXFLAGS="-Werror" \ + CC="$CC" CC_LD="$LINKER" CFLAGS="-Werror" \ + CXX="$CXX" CXX_LD="$LINKER" CXXFLAGS="-Werror" \ meson setup \ -Dtests=unsafe -Dslow-tests=true -Dfuzz-tests=true --werror \ -Dnobody-group=nogroup -Dcryptolib="${CRYPTOLIB:?}" -Ddebug=false \ diff --git a/.github/workflows/build_test.yml b/.github/workflows/build_test.yml index ccbbe26ecdc..dc9dc0bcb17 100644 --- a/.github/workflows/build_test.yml +++ b/.github/workflows/build_test.yml @@ -26,10 +26,10 @@ jobs: matrix: env: - { COMPILER: "gcc", COMPILER_VERSION: "11", LINKER: "bfd", CRYPTOLIB: "gcrypt" } - - { COMPILER: "gcc", COMPILER_VERSION: "13", LINKER: "gold", CRYPTOLIB: "openssl" } - - { COMPILER: "clang", COMPILER_VERSION: "14", LINKER: "mold", CRYPTOLIB: "gcrypt" } - - { COMPILER: "clang", COMPILER_VERSION: "15", LINKER: "lld", CRYPTOLIB: "openssl" } - - { COMPILER: "clang", COMPILER_VERSION: "16", LINKER: "bfd", CRYPTOLIB: "auto" } + - { COMPILER: "gcc", COMPILER_VERSION: "13", LINKER: "mold", CRYPTOLIB: "openssl" } + - { COMPILER: "clang", COMPILER_VERSION: "14", LINKER: "bfd", CRYPTOLIB: "gcrypt" } + - { COMPILER: "clang", COMPILER_VERSION: "15", LINKER: "mold", CRYPTOLIB: "openssl" } + - { COMPILER: "clang", COMPILER_VERSION: "17", LINKER: "lld", CRYPTOLIB: "auto" } env: ${{ matrix.env }} steps: - name: Repository checkout From 823bf39a49b55221e930c714f5c5e902cd3b5f7c Mon Sep 17 00:00:00 2001 From: Jan Janssen Date: Thu, 28 Sep 2023 17:54:35 +0200 Subject: [PATCH 7/7] elf2efi: Add GNU_RELRO support --- src/boot/efi/meson.build | 2 +- tools/elf2efi.py | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/boot/efi/meson.build b/src/boot/efi/meson.build index dfb8700547d..d168d2d553d 100644 --- a/src/boot/efi/meson.build +++ b/src/boot/efi/meson.build @@ -162,7 +162,7 @@ efi_c_ld_args = [ '-z', 'max-page-size=4096', '-z', 'noexecstack', - '-z', 'norelro', + '-z', 'relro', '-z', 'separate-code', ] diff --git a/tools/elf2efi.py b/tools/elf2efi.py index 1e74dcfff2b..54f64fa53c1 100755 --- a/tools/elf2efi.py +++ b/tools/elf2efi.py @@ -258,6 +258,13 @@ def iter_copy_sections(elf: ELFFile) -> typing.Iterator[PeSection]: # manually, so that we can easily strip unwanted sections. We try to only discard things we know # about so that there are no surprises. + relro = None + for elf_seg in elf.iter_segments(): + if elf_seg["p_type"] == "PT_LOAD" and elf_seg["p_align"] != SECTION_ALIGNMENT: + raise RuntimeError("ELF segments are not properly aligned.") + elif elf_seg["p_type"] == "PT_GNU_RELRO": + relro = elf_seg + for elf_s in elf.iter_sections(): if ( elf_s["sh_flags"] & SH_FLAGS.SHF_ALLOC == 0 @@ -275,6 +282,10 @@ def iter_copy_sections(elf: ELFFile) -> typing.Iterator[PeSection]: else: rwx = PE_CHARACTERISTICS_R + # PE images are always relro. + if relro and relro.section_in_segment(elf_s): + rwx = PE_CHARACTERISTICS_R + if pe_s and pe_s.Characteristics != rwx: yield pe_s pe_s = None