Skip to content

SIGSEGV when IFUNC resolver calls PLT function (new PLT scheme) #1550

@jam7

Description

@jam7

Summary

Programs linked with mold (new PLT scheme, tested 2.35.1 and 2.40.4) crash with SIGSEGV at load time when a shared library contains:

  1. An R_X86_64_IRELATIVE relocation in .rela.dyn
  2. The IFUNC resolver calls an external function via PLT in the same library

The crash does not occur with LD_BIND_NOW=1, -Wl,-z,now, or older mold versions that use the traditional PLT scheme (tested 2.30.0).

Root Cause

Mold's new PLT scheme uses this structure:

PLT[0]:  endbr64; push %r11; push GOT[1]; jmp *GOT[2]
PLT[n]:  mov $index, %r11d; jmp *GOT[n]

The initial GOT.PLT entries contain PLT[0]'s ELF virtual address (e.g., 0x16a0). When a lazy PLT stub is called, jmp *GOT[n] should jump to PLT[0] (the resolver). But the stored address is an unrelocated offset, not the runtime address.

The dynamic linker normally fixes this by adding the load base address (l_addr) during .rela.plt processing (elf_machine_lazy_rel does *reloc_addr += l_addr).

However, R_X86_64_IRELATIVE relocations are in .rela.dyn, which is processed before .rela.plt. When the IRELATIVE resolver runs, the .rela.plt lazy setup has not yet occurred, so GOT.PLT entries still contain unrelocated addresses. If the resolver calls any function via PLT, the PLT stub jumps to the unrelocated address (unmapped memory) and crashes.

Why traditional PLT works

The traditional PLT scheme (GNU ld, gold, older mold) uses:

PLT[n]:  jmp *GOT[n]; push $index; jmp PLT[0]

The initial GOT[n] points to the push $index instruction (the next instruction in the same PLT stub). The jmp *GOT[n] with this value falls through to push; jmp PLT[0], which works without any relocation because jmp PLT[0] uses relative encoding (e9 xx xx xx xx).

Why LD_BIND_NOW works

With LD_BIND_NOW=1, all JUMP_SLOT relocations are resolved eagerly. The dynamic linker fills GOT.PLT entries with actual function addresses before any IRELATIVE resolvers run.

Reproducer

helper.h

#ifdef __cplusplus
extern "C" {
#endif
int get_cpu_info(void);
#ifdef __cplusplus
}
#endif

helper.c

#include "helper.h"
int get_cpu_info(void) { return 42; }

repro.cpp

#include "helper.h"

static int compute_default(int x) { return x + 1; }
static int compute_fast(int x) { return x + 2; }

typedef int (*compute_fn)(int);
extern "C" compute_fn resolve_compute() {
    if (get_cpu_info() > 0)  // PLT call -> crash
        return compute_fast;
    return compute_default;
}

extern "C" __attribute__((visibility("hidden")))
int compute(int) __attribute__((ifunc("resolve_compute")));

extern "C" int wrapper(int x) { return compute(x); }

main.c

#include <stdio.h>
extern int wrapper(int x);
int main(void) {
    printf("wrapper(10) = %d\n", wrapper(10));
    return 0;
}

Build & test

# Build with mold
gcc -shared -fPIC -o libhelper.so helper.c -B/usr/local/libexec/mold
g++ -shared -fPIC -o libfoo.so repro.cpp -L. -lhelper -B/usr/local/libexec/mold
gcc -o main main.c -L. -lfoo -lhelper -Wl,-rpath,'$ORIGIN' -B/usr/local/libexec/mold

LD_LIBRARY_PATH=. ./main              # SIGSEGV
LD_LIBRARY_PATH=. LD_BIND_NOW=1 ./main # OK

# Build with GNU ld (works fine)
gcc -shared -fPIC -o libhelper.so helper.c
g++ -shared -fPIC -o libfoo.so repro.cpp -L. -lhelper
gcc -o main main.c -L. -lfoo -lhelper -Wl,-rpath,'$ORIGIN'
LD_LIBRARY_PATH=. ./main              # OK

Real-world Impact

LLVM/Clang (trunk, commit 146a919360eb) added __attribute__((target("default"))) and __attribute__((target("sse4.2"))) to fastParseASCIIIdentifier in clang/lib/Lex/Lexer.cpp. This creates an IFUNC with an auto-generated resolver that calls __cpu_indicator_init via PLT. When Clang is built with BUILD_SHARED_LIBS=ON and mold, the clang binary segfaults on startup.

Environment

Tested mold versions:

  • mold 2.35.1 - new PLT scheme, crashes
  • mold 2.40.4 - new PLT scheme, crashes
  • mold 2.30.0 - traditional PLT, works new PLT scheme, crashes

Other:

  • GCC 11.2.1 (CentOS 7), GCC 13.3.0 (Ubuntu 24.04)
  • glibc 2.17 (CentOS 7) and glibc 2.39 (Ubuntu 24.04) - both crash
  • x86_64 Linux

Suggested Fix

One of:

  1. Use R_X86_64_RELATIVE relocations for GOT.PLT initial values, so they are fixed up during .rela.dyn processing (before IRELATIVE)
  2. Revert to self-relative PLT fallback values that don't need relocation
  3. Process .rela.plt lazy setup before IRELATIVE relocations in .rela.dyn (this would require a dynamic linker change, not a mold change)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions