Skip to content

[ELFLOADER] Initialize the x86-64 PLT resolver before relocation - #4373

Merged
ptitSeb merged 1 commit into
ptitSeb:mainfrom
zengdage:fix-ifunc
Sep 9, 2026
Merged

[ELFLOADER] Initialize the x86-64 PLT resolver before relocation#4373
ptitSeb merged 1 commit into
ptitSeb:mainfrom
zengdage:fix-ifunc

Conversation

@zengdage

@zengdage zengdage commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

GOT[0] and GOT[1] are initialized only after the RelocateElfRELA function returns. However, GOT[0] and GOT[1] may already be required while processing R_X86_64_IRELATIVE inside RelocateElfRELA.

This patch always initialize GOT[1] and GOT[2] before processing x86-64 dynamic relocations, so PLT0 has valid elf header and resolver during IFUNC and IRELATIVE handling.

Before:

     |_ RelocateElf64
     |_ RelocateElfPlt64
     |     |_ JUMP_SLOT strlen
     |     |     |_ *need_resolv = 1
     │     |_ ifunc resolver
     │     |     |_ ifunc resolver call strcmp
     │     |            |_ strcmp call PLT -> GOT[x] not filled -> fallback → PLT[0]
     │     |                  |_ PLT[0]: push [GOT[1]] = 0
     │     |                             jmp  [GOT[2]] = 0
     |     |                                    |_ ERROR
     |     |_ SeedPltResolver64: GOT[1] = head, GOT[2] = pltResolver64

After:

     |_ RelocateElf64
     |     |_ SeedPltResolver64: GOT[1] = head, GOT[2] = pltResolver64
     |_ RelocateElfPlt64
     │     |_ ifunc resolver
     │           |_ ifunc resolver call strcmp
     │                 |_ strcmp call PLT -> GOT[x] not filled -> fallback → PLT[0]
     │                       |_ PLT[0]: push [GOT[1]] = head
     │                                  jmp  [GOT[2]] = pltResolver64

@zengdage

zengdage commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

I meeted this issue while debugging the QQ app. A simplified test case is shown below. Support for R_386_IRELATIVE needs to be added for the BOX32 handling, this will be submitted in the next PR.

Build command: gcc -O2 -fno-builtin-strlen -fplt -Wl,-z,lazy test.c -o test

#include <stdio.h>
#include <string.h>

typedef int (*ifunc_impl_t)(void);
static volatile const char *selected_text = "box64-irelative";

static int selected_impl(void)
{
    return 42;
}

static ifunc_impl_t selected_resolver(void)
    __attribute__((noinline, optimize("O0")));

static ifunc_impl_t selected_resolver(void)
{
    return strlen((const char *)selected_text) ? selected_impl : NULL;
}

extern int selected(void) __attribute__((ifunc("selected_resolver")));

int main(void)
{
    int result = selected();
#if defined(__i386__)
    printf("R_386_IRELATIVE result: %d\n", result);
#else
    printf("R_X86_64_IRELATIVE result: %d\n", result);
#endif
    return result == 42 ? 0 : 1;
}

@ptitSeb

ptitSeb commented Sep 9, 2026

Copy link
Copy Markdown
Owner

Previously, the PltResolver was injected only when needed, now this is systematic, I don't like that. Also the need_resolv paramter from RelocateElfRELA(...) is now useles. So either the mecanism is change to inject the resolver on 1st use (or on 1st need), but this variable in the function is probably unneded now and should be removed.

GOT[0] and GOT[1] are initialized only after the `RelocateElfRELA` function
returns. However, `GOT[0]` and `GOT[1]` may already be required while processing
`R_X86_64_IRELATIVE` inside `RelocateElfRELA`.

This patch always initialize GOT[1] and GOT[2] before processing x86-64 dynamic
relocations, so PLT0 has valid elf header and resolver during IFUNC and IRELATIVE
handling.

Before:
```
     |_ RelocateElf64
     |_ RelocateElfPlt64
     |     |_ JUMP_SLOT strlen
     |     |     |_ *need_resolv = 1
     │     |_ ifunc resolver
     │     |     |_ ifunc resolver call strcmp
     │     |            |_ strcmp call PLT -> GOT[x] not filled -> fallback → PLT[0]
     │     |                  |_ PLT[0]: push [GOT[1]] = 0
     │     |                             jmp  [GOT[2]] = 0
     |     |                                    |_ ERROR
     |     |_ SeedPltResolver64: GOT[1] = head, GOT[2] = pltResolver64
```

After:
```
     |_ RelocateElf64
     |     |_ SeedPltResolver64: GOT[1] = head, GOT[2] = pltResolver64
     |_ RelocateElfPlt64
     │     |_ ifunc resolver
     │           |_ ifunc resolver call strcmp
     │                 |_ strcmp call PLT -> GOT[x] not filled -> fallback → PLT[0]
     │                       |_ PLT[0]: push [GOT[1]] = head
     │                                  jmp  [GOT[2]] = pltResolver64
```
@zengdage

zengdage commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

Previously, the PltResolver was injected only when needed, now this is systematic, I don't like that. Also the need_resolv paramter from RelocateElfRELA(...) is now useles. So either the mecanism is change to inject the resolver on 1st use (or on 1st need), but this variable in the function is probably unneded now and should be removed.

I removed the useless parameter.

I think if the GOT[0] and GOT[1] are initialized only upon first need, every time we hit a JUMP_SLOT or IRELATIVE, we would need to check whether they have already been filled, which introduces many extra conditional branches. This has a greater performance impact than filling them before RelocateElf.

From the perspective of the ld.so source code, GOT[0] and GOT[1] are filled before relocations. Even if they are filled without ever being used, doing so is harmless.

@ptitSeb

ptitSeb commented Sep 9, 2026

Copy link
Copy Markdown
Owner

Ok, LGTM

@ptitSeb
ptitSeb merged commit 92f5a7f into ptitSeb:main Sep 9, 2026
28 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants