IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    lld linked musl on PowerPC64

    MaskRay发表于 2022-11-11 20:24:46
    love 0

    I was asked about a segfault related to lld linked musl libc.so on PowerPC64.

    • /usr/lib/ld-musl-powerpc64le.so.1 /path/to/thing worked. The kernel ELF loader loads rtld and rtld loads the executable.
    • /path/to/thing segfaulted. The kernel ELF loader loads both rtld and the executable.

    Therefore the bug is likely due to a difference between the two modes.

    The section and program header dump from readelf looked like the following. I annotated the interesting lines with !!!.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    There are 36 section headers, starting at offset 0x2e4890:

    Section Headers:
    [Nr] Name Type Address Off Size ES Flg Lk Inf Al
    [ 0] NULL 0000000000000000 000000 000000 00 0 0 0
    [ 1] .note.gnu.build-id NOTE 0000000000000270 000270 000018 00 A 0 0 4
    [ 2] .dynsym DYNSYM 0000000000000288 000288 009fc0 18 A 5 1 8
    [ 3] .gnu.hash GNU_HASH 000000000000a248 00a248 003150 00 A 2 0 8
    [ 4] .hash HASH 000000000000d398 00d398 003548 04 A 2 0 4
    [ 5] .dynstr STRTAB 00000000000108e0 0108e0 004564 00 A 0 0 1
    [ 6] .rela.dyn RELA 0000000000014e48 014e48 000e28 18 A 2 0 8
    [ 7] .rela.plt RELA 0000000000015c70 015c70 000090 18 AI 2 17 8
    [ 8] .rodata PROGBITS 0000000000015d00 015d00 0336fd 00 AMS 0 0 16
    [ 9] .eh_frame_hdr PROGBITS 0000000000049400 049400 00001c 00 A 0 0 4
    [10] .eh_frame PROGBITS 0000000000049420 049420 00004c 00 A 0 0 8
    [11] .text PROGBITS 0000000000059480 049480 089308 00 AX 0 0 32
    [12] .glink PROGBITS 00000000000e2788 0d2788 000054 00 AX 0 0 4
    [13] .data.rel.ro PROGBITS 00000000000f27e0 0d27e0 000408 00 WA 0 0 8
    [14] .dynamic DYNAMIC 00000000000f2be8 0d2be8 000140 10 WA 5 0 8
    [15] .got PROGBITS 00000000000f2d28 0d2d28 000008 00 WA 0 0 8
    [16] .toc PROGBITS 00000000000f2d30 0d2d30 0002a0 00 WA 0 0 8
    [17] .plt NOBITS 00000000000f2fd0 0d2fd0 000040 00 WA 0 0 8 !!!
    [18] .data PROGBITS 0000000000103010 0d3010 0003c0 00 WA 0 0 8
    [19] .bss NOBITS 00000000001033d0 0d33d0 002ac8 00 WA 0 0 16
    [20] .branch_lt NOBITS 0000000000105e98 0d33d0 000000 00 WA 0 0 8
    [21] .debug_loclists PROGBITS 0000000000000000 0d33d0 046be2 00 0 0 1
    [22] .debug_abbrev PROGBITS 0000000000000000 119fb2 0466c3 00 0 0 1
    [23] .debug_info PROGBITS 0000000000000000 160675 089b79 00 0 0 1
    [24] .debug_rnglists PROGBITS 0000000000000000 1ea1ee 006693 00 0 0 1
    [25] .debug_str_offsets PROGBITS 0000000000000000 1f0881 02783c 00 0 0 1
    [26] .debug_str PROGBITS 0000000000000000 2180bd 013f8b 01 MS 0 0 1
    [27] .debug_addr PROGBITS 0000000000000000 22c048 011780 00 0 0 1
    [28] .comment PROGBITS 0000000000000000 23d7c8 000029 01 MS 0 0 1
    [29] .debug_frame PROGBITS 0000000000000000 23d7f8 0176f0 00 0 0 8
    [30] .debug_line PROGBITS 0000000000000000 254ee8 0657e4 00 0 0 1
    [31] .debug_line_str PROGBITS 0000000000000000 2ba6cc 0089ee 01 MS 0 0 1
    [32] .debug_aranges PROGBITS 0000000000000000 2c30ba 0001b0 00 0 0 1
    [33] .symtab SYMTAB 0000000000000000 2c3270 016b90 18 35 2175 8
    [34] .shstrtab STRTAB 0000000000000000 2d9e00 00016f 00 0 0 1
    [35] .strtab STRTAB 0000000000000000 2d9f6f 00a91f 00 0 0 1
    Key to Flags:
    W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
    L (link order), O (extra OS processing required), G (group), T (TLS),
    C (compressed), x (unknown), o (OS specific), E (exclude),
    p (processor specific)

    Elf file type is DYN (Shared object file)
    Entry point 0xdb8bc
    There are 10 program headers, starting at offset 64

    Program Headers:
    Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
    PHDR 0x000040 0x0000000000000040 0x0000000000000040 0x000230 0x000230 R 0x8
    LOAD 0x000000 0x0000000000000000 0x0000000000000000 0x04946c 0x04946c R 0x10000
    LOAD 0x049480 0x0000000000059480 0x0000000000059480 0x08935c 0x08935c R E 0x10000
    LOAD 0x0d27e0 0x00000000000f27e0 0x00000000000f27e0 0x0007f0 0x000830 RW 0x10000 !!!
    LOAD 0x0d3010 0x0000000000103010 0x0000000000103010 0x0003c0 0x002e88 RW 0x10000
    DYNAMIC 0x0d2be8 0x00000000000f2be8 0x00000000000f2be8 0x000140 0x000140 RW 0x8
    GNU_RELRO 0x0d27e0 0x00000000000f27e0 0x00000000000f27e0 0x0007f0 0x001820 R 0x1
    GNU_EH_FRAME 0x049400 0x0000000000049400 0x0000000000049400 0x00001c 0x00001c R 0x4
    GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW 0x0
    NOTE 0x000270 0x0000000000000270 0x0000000000000270 0x000018 0x000018 R 0x4

    Section to Segment mapping:
    Segment Sections...
    00
    01 .note.gnu.build-id .dynsym .gnu.hash .hash .dynstr .rela.dyn .rela.plt .rodata .eh_frame_hdr .eh_frame
    02 .text .glink
    03 .data.rel.ro .dynamic .got .toc .plt
    04 .data .bss
    05 .dynamic
    06 .data.rel.ro .dynamic .got .toc .plt
    07 .eh_frame_hdr
    08
    09 .note.gnu.build-id
    None .branch_lt .debug_loclists .debug_abbrev .debug_info .debug_rnglists .debug_str_offsets .debug_str .debug_addr .comment .debug_frame .debug_line .debug_line_str .debug_aranges .symtab .shstrtab .strtab

    There were two PT_LOAD program headers with the PF_R|PF_W flags and the unusual property p_filesz < p_memsz. For both RW PT_LOAD program headers, we had roundUp(p_vaddr+p_filesz, pagesz) < roundUp(p_vaddr+p_memsz, pagesz). It turns out that when the Linux kernel loads an interpreter (PT_INTERP; see fs/binfmt_elf.c:load_elf_interp), it only supports one PT_LOAD with p_filesz < p_memsz.

    Note: it is typical for lld output to have two RW PT_LOAD program headers, one for RELRO sections (PT_GNU_RELRO) and the other for non-RELRO sections. This may look unusual at the first glance but it avoids an alignment padding as used in GNU ld's single RW PT_LOAD layout. See Explain GNU style linker options#-z relro.

    In the PowerPC ELFv2 ABI, .plt is like GOTPLT on other architectures (it holds resolved addresses for PLT entries) and has the SHT_NOBITS type. With -z now, .plt can be eagerly resolved and become read-only after relocation resolving, therefore it is part of PT_GNU_RELRO. When lld layouts sections, it is part of the first RW PT_LOAD. In the unlucky libc.so, .plt is 64 bytes (2 reserved pointer entries plus 6 pointer entries for malloc/calloc/realloc/memalign/aligned_alloc/free). p_memsz = p_filesz - 64. If roundUp(p_vaddr+p_filesz, pagesz) < roundUp(p_vaddr+p_memsz, pagesz), relocation resolving will access an unmapped memory page and segfault. If the comparison result is equal and we just have p_filesz < p_memsz, the kernel will fail to zero some bytes but the bytes will be overwritten by rtld anyway.

    Clang passes -z now to ld for Alpine Linux. Chimera Linux has patched Clang Driver to pass -z now for all musl target triples.

    • Pros: GOTPLT is part of RELRO and provides security hardening values. In addition, the DF_1_NOW flag avoids an allocation in its rtld. See this commit emulate lazy relocation as deferrable relocation.
    • Cons: There is a slight size increase of .dynamic: it will always have a DT_FLAGS holding DF_NOW. In most cases DT_FLAGS can actually be absent if -z now is not used.

    Workarounds

    There are multiple ways to work around the issue.

    -z lazy

    The easiest is to build musl with LDFLAGS=-Wl,-z,lazy to override driver specified -z now. I verified with a local cross-compilation build.

    1
    2
    3
    mkdir out/ppc64le && cd out/ppc64le
    ../../configure --target=powerpc64le-linux-gnu CC=clang CFLAGS='--target=powerpc64le-linux-gnu -mlong-double-64' LDFLAGS=-fuse-ld=lld
    make -j$(nproc)

    Cons: loses some security hardening.

    (If you use GCC's powerpc64 port, avoid -Os. lld has not implemented _savefpr* and _restfpr* functions.)

    SHT_PROGBITS .plt

    The linker synthesized .plt has the SHT_NOBITS type. We can link a relocatable object file with an empty SHT_PROGBITS .plt.

    1
    .section .plt,"awR",@progbits

    The output section will have the SHT_NOBITS type.

    Note R for SHF_RETAIN. Without the flag, the linker option --gc-sections drops the input .plt so that it cannot affect the output section type.

    Prevent roundUp(p_vaddr+p_filesz, pagesz) < roundUp(p_vaddr+p_memsz, pagesz)

    The musl build system forces -Wl,--hash-style=both. We can specify LDFLAGS=-Wl,--hash-style=gnu to drop .hash.

    Alternatively, we may pad .plt or any preceding output section so that the property no longer holds.

    1
    2
    // a.lds
    OVERWRITE_SECTIONS { .plt : { *(.plt) QUAD(0) QUAD(0) } };

    Use clang -o libc.so ... a.lds.

    You may be attempted to keep -z now and link libc.so with a linker script:

    1
    SECTIONS { .plt : {} } INSERT AFTER .bss;

    Unfortunately that would create discontinued RELRO sections, which is unsupported by linkers and most rtld implementations.

    glibc

    glibc adopts a separate rtld and libc.so design. Its rtld has no JUMP_SLOT (JMP_SLOT) relocations.

    The powerpc64 port has been buildable since lld 13. There is no .plt section, therefore the first RW PT_LOAD has p_filesz == p_memsz. The built rtld works with Linux kernel.

    I got powerpc64le-linux-gnu-gcc and binutils from system packages. I have installed /usr/local/bin/powerpc64le-linux-gnu-ld.lld so that powerpc64le-linux-gnu-gcc -fuse-ld=lld works.

    1
    2
    3
    mkdir out/ppc64le && cd out/ppc64le
    LDFLAGS=-fuse-ld=lld ../../configure --prefix=/tmp/glibc/ppc64le --host=powerpc64le-linux-gnu --with-default-link --enable-hardcoded-path-in-tests
    LDFLAGS=-fuse-ld=lld make -j $(nproc)

    Reliable reproduce

    Here is the main trick: assemble the following assembly file toc.s and link it into musl lib/libc.so.

    1
    2
    3
    4
    .section .toc,"aw",@nobits
    .globl toc
    toc:
    .space 4096

    .toc is recognized as a RELRO section in ld.lld, even if the architecture is not PowerPC64:)

    The section is 4096 (page size), therefore we can ensure roundUp(p_vaddr+p_filesz, pagesz) < roundUp(p_vaddr+p_memsz, pagesz).

    Compile the following C program, link it with toc.o using -Wl,--dynamic-linker=path/to/libc.so.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #include <assert.h>
    #include <stdio.h>

    extern const char toc[];

    int main(void) {
    assert(toc[4096-1] == 0);
    puts("hello");
    }

    The output will have two RW PT_LOAD program headers with p_filesz < p_memsz.

    You can add custom sections to the PT_GNU_RELRO program header using a full linker script with DATA_SEGMENT_ALIGN and DATA_SEGMENT_RELRO_END (implemented in ld.lld 15).

    1
    2
    3
    4
    5
    . = DATA_SEGMENT_ALIGN (CONSTANT (MAXPAGESIZE), CONSTANT (COMMONPAGESIZE));

    /* RELRO sections */

    . = DATA_SEGMENT_RELRO_END (0, .);

    Stress test

    To test that the kernel ELF loader can handle more RW PT_LOAD program headers, we can add a few more SHF_ALLOC|SHF_WRITE sections (abbreviated as RW below). We can place a read-only section after .bss followed by a RW section. The read-only section will form a read-only PT_LOAD and the RW section will form a RW PT_LOAD.

    Create some files. If you have split-file (a test utility from llvm-project), you may place the following content into a.txt.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    #--- a.c
    #include <assert.h>
    #include <stdio.h>

    extern const char toc[];
    extern char nobits0[], nobits1[];

    int main(void) {
    assert(toc[4096-1] == 0);
    for (int i = 0; i < 1024; i++) {
    assert(nobits0[i] == 0);
    nobits0[i] = 1;
    }
    for (int i = 0; i < 8192; i++) {
    assert(nobits1[i] == 0);
    nobits1[i] = 1;
    }

    puts("hello");
    }

    #--- toc.s
    .globl toc, nobits0, nobits1

    .section .toc,"aw",@nobits; toc: .space 4096

    .section .ro0,"a"; .byte 255
    .section .nobits0,"aw",@nobits; nobits0: .space 1024
    .section .ro1,"a"; .byte 255
    .section .nobits1,"aw",@nobits; nobits1: .space 8192

    #--- a.lds
    SECTIONS { .ro0 : {} .nobits0 : {} .ro1 : {} .nobits1 : {} } INSERT AFTER .bss;

    Then run:

    1
    2
    split-file a.txt a
    path/to/musl-gcc -Wl,--dynamic-linker=/lib/libc.so a/a.c a/a.lds -o toy

    Note: when a SHT_NOBITS section is followed by another section, the SHT_NOBITS section behaves as if it occupies the file offset range. This is because ld.lld does not implement a file size optimization.

    Test a patched kernel

    Pedro Falcato has a kernel patch [PATCH] fs/binfmt_elf: Fix memsz > filesz handling to fix the issue. Let's verify it.

    1
    2
    3
    4
    // In linux
    patch -p1 -i /tmp/c/0001-fs-binfmt_elf-Fix-memsz-filesz-handling.patch
    make -j $(nproc) O=/tmp/linux/x86_64 defconfig all
    # Specify LLVM=1 if you want to use clang lld llvm-{ar,nm,objcopy,objdump,readelf,strings}

    Now prepare an initrd image with the test program. https://github.com/ClangBuiltLinux/boot-utils has a prebuilt image and adding extra files is convenient.

    1
    2
    mkdir /tmp/initrd && cd /tmp/initrd
    sudo cpio -i -F ~/Dev/ClangBuiltLinux/boot-utils/images/x86_64/rootfs.cpio

    Copy musl lib/libc.so to /tmp/initrd/lib/libc.so and our toy program to /tmp/initrd/toy. Edit /tmp/initrd/init to run /toy || echo failed: $?. Rebuild the initrd image.

    1
    find . | sudo cpio -o --format=newc | zstd > ~/Dev/ClangBuiltLinux/boot-utils/images/x86_64/rootfs.cpio.zst

    With an unpatched kernel, /toy segfaults as expected:

    1
    2
    3
    4
    5
    % ~/Dev/ClangBuiltLinux/boot-utils/boot-qemu.py -a x86_64 -k /tmp/linux/x86_64
    ...
    Error relocating /lib/libc.so: RELRO protection failed: No error information
    failed: 127
    ...

    With a patched kernel, /toy succeeds.

    1
    2
    3
    4
    % ~/Dev/ClangBuiltLinux/boot-utils/boot-qemu.py -a x86_64 -k /tmp/linux/x86_64
    ...
    hello
    ...



沪ICP备19023445号-2号
友情链接