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 diff --git a/meson.build b/meson.build index 44eac94fdf9..4ef68c7f1c3 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/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/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..d168d2d553d 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', @@ -166,13 +162,14 @@ efi_c_ld_args = [ '-z', 'max-page-size=4096', '-z', 'noexecstack', - '-z', 'norelro', - '-T' + elf2efi_lds, + '-z', 'relro', + '-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,8 +336,6 @@ foreach archspec : efi_archspecs 'include_directories' : efi_includes, '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, @@ -325,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) @@ -362,6 +383,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/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/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.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..54f64fa53c1 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,95 @@ 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()) - - 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 next_section_address(sections: typing.List[PeSection]) -> int: + return align_to( + sections[-1].VirtualAddress + sections[-1].VirtualSize, SECTION_ALIGNMENT + ) -def copy_sections(elf: ELFFile, opt: PeOptionalHeader) -> typing.List[PeSection]: - sections = [] +def iter_copy_sections(elf: ELFFile) -> typing.Iterator[PeSection]: + pe_s = None + + # 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. + + 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 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 + + # 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 + + 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 @@ -304,6 +344,32 @@ def copy_sections(elf: ELFFile, opt: PeOptionalHeader) -> typing.List[PeSection] 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, @@ -325,7 +391,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 @@ -333,7 +398,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 +426,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 +446,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 +459,49 @@ 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) + + 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 +516,7 @@ def convert_elf_relocations( n_relocs += 1 block.entries.append(PeRelocationEntry()) + block.PageRVA += segment_offset block.BlockSize = ( sizeof(PeRelocationBlock) + sizeof(PeRelocationEntry) * n_relocs ) @@ -423,11 +527,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 @@ -495,8 +597,9 @@ 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) + copy_sections(elf, opt, args.copy_sections, sections) + pe_reloc_s = convert_elf_relocations(elf, opt, sections, args.minimum_sections) coff.Machine = pe_arch coff.NumberOfSections = len(sections) @@ -506,7 +609,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 @@ -515,18 +617,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) - 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 @@ -593,6 +685,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())