GNU IFUNC and Indirect Functions
A GNU indirect function (IFUNC) is a symbol whose final address is not chosen by the linker or written into the file, but is computed at load time by a small resolver function that the toolchain attaches to it. When the dynamic linker processes the binary, it calls the resolver, and whatever pointer the resolver returns becomes the symbol’s address for the rest of the program’s life. This one-time indirection is how a single
libc.socan ship a dozen hand-tuned versions ofmemcpyand pick the fastest one for the CPU it happens to be running on — without any per-call branch. IFUNC is a GNU extension to ELF built on a dedicated symbol type,STT_GNU_IFUNC, and a dedicated relocation,R_*_IRELATIVE; it has been supported since binutils 2.20.1 and glibc 2.11.1 (JasonCC). It is also, as this note explains, one of the sharpest edges in the whole userspace runtime: resolvers run in a hostile, half-relocated environment, and getting that wrong crashes the process beforemain.
For the general machinery this note sits on — how the Global Offset Table (GOT) and Procedure Linkage Table (PLT) work, how relocations are applied, and how ld.so resolves symbols — see those notes. This note is specifically about the indirect case.
Mental Model
Think of an ordinary exported function as a fixed address: the linker knows (or the dynamic linker resolves) exactly where memcpy lives, and every caller ends up pointing there. An IFUNC replaces that fixed address with a promise to compute an address later. The symbol table entry does not say “the function is here”; it says “run this resolver, and use whatever it hands back.” The resolver is ordinary code — it can read CPU feature flags, environment, or anything else available at load time — and it returns a plain function pointer. The dynamic linker invokes it once, stores the result in the GOT, and from then on every call goes straight to the chosen implementation with zero extra overhead.
flowchart TB subgraph LOAD["Load time (inside ld.so, before main)"] IREL["R_*_IRELATIVE reloc<br/>on a GOT slot"] --> CALL["ld.so CALLS the resolver<br/>(elf_ifunc_invoke)"] CALL --> RES["resolver():<br/>read CPU features<br/>(AVX2? SSE4.1?)"] RES --> PICK["return &memcpy_avx2<br/>(chosen impl pointer)"] PICK --> GOT["write pointer into<br/>GOT slot"] end subgraph RUN["Run time (every later call)"] C1["call memcpy@plt"] --> J["jmp *GOT slot"] J --> IMPL["memcpy_avx2<br/>(direct, no dispatch)"] end GOT -. "slot now holds<br/>chosen address" .-> J
What it shows: the resolver runs exactly once, at load time, inside the dynamic linker; its return value is baked into the GOT so that every subsequent call is a single indirect jump to the selected implementation. The insight to take: IFUNC moves a dispatch decision from every call site to a single load-time event, trading a tiny bit of startup work for zero steady-state cost — but it also means arbitrary user code runs inside ld.so before the program is fully relocated.
Mechanical Walk-through
The symbol type. An IFUNC is marked with the ELF symbol type STT_GNU_IFUNC (value 10), which distinguishes it from a regular function STT_FUNC (MaskRay). Because this is a GNU extension rather than base ELF, a file that uses it advertises the fact in its ELF header’s OS/ABI byte: “On Linux, GNU tools set ELFOSABI_GNU when ifunc is present” (MaskRay). At the assembly level the marking is .type foo, @gnu_indirect_function followed by .set foo, resolver, and compilers expose it through a function attribute — void *ifunc(void) __attribute__((ifunc("resolver"))) — where resolver is the name of a local function returning the real implementation’s address.
Two relocation flavours, chosen by preemptibility. The whole point of a symbol is what relocation refers to it, and IFUNC uses two (MaskRay):
- A non-preemptible IFUNC — one whose definition is fixed within the module, typically defined in the same executable or
.so— is referenced byR_*_IRELATIVE. This relocation has no associated symbol; the location it patches simply holds the resolver’s address. The x86-64 psABI definesR_X86_64_IRELATIVEas being likeR_X86_64_RELATIVE(which computes base-plus-addend,B + A) except that the value stored is “the program address returned by the function, which takes no arguments, at the address of the result” — i.e. the linker fills the slot by callingB + Aand storing what it returns. MaskRay renders it compactly as “call (B + A) as a function”. Its stated purpose is “to avoid name lookup for locally definedSTT_GNU_IFUNCsymbols at load-time” — no symbol string, no hash lookup, just call and store. - A preemptible IFUNC — one that could be overridden by another module (see Symbol Interposition and LD_PRELOAD) — is referenced through the ordinary PLT with
R_*_JUMP_SLOT. Whenld.soresolves that slot, “the relocation resolver checks whether theR_*_JUMP_SLOTrelocation refers to an ifunc. If it does, instead of filling the GOT entry with the target address, the resolver calls the target address as an indirect function” (MaskRay).
The IPLT. Call sites emit ordinary instructions like call foo, so even a local IFUNC needs a PLT-like stub to forward through. Ian Lance Taylor, who implemented the feature in the GNU linker, notes that all IFUNC references “go through the PLT” unconditionally, “even for a local symbol, although local symbols normally do not require PLT entries” (Taylor). The linker collects these into a synthetic .iplt section with R_*_IRELATIVE relocations in .rela.plt (or a dedicated range).
Where the return value goes. During relocation, “the return value of the resolver will be placed in the GOT entry and the PLT entry will load the address” (MaskRay). glibc’s actual dispatch point is elf_ifunc_invoke: in elf/dl-runtime.c’s _dl_fixup, once the symbol is looked up, “If the symbol is indirect function type (STT_GNU_IFUNC), the code invokes value = elf_ifunc_invoke (DL_FIXUP_VALUE_ADDR (value))” — that is, it calls the resolver and uses the returned pointer as the real target (see Lazy Binding for the surrounding _dl_fixup flow).
Ordering is the crux. IRELATIVE relocations are processed last, after R_*_RELATIVE and R_*_GLOB_DAT (MaskRay). This is deliberate: a resolver may legitimately read a global whose GOT slot was filled by an earlier GLOB_DAT, so those must be done first. But it is also fragile: within a module glibc processes RELATIVE → other relocations → IRELATIVE last, and between modules “if A has an ifunc referencing B, generally B needs to be relocated before A.” When that ordering cannot be guaranteed — for instance an executable’s IFUNC called from a shared object whose resolver runs while “the executable’s relocations haven’t been resolved” — you get a load-time crash.
Static and static-PIE binaries. With no dynamic linker present, a static executable has to apply its own IRELATIVE relocations. The linker brackets them with the symbols __rela_iplt_start / __rela_iplt_end (or __rel_iplt_* on 32-bit REL targets), and the C startup code walks that range and calls each resolver (Taylor). For -static-pie, glibc’s _dl_relocate_static_pie “takes care of R_*_IRELATIVE” and expects the __rela_iplt_start/end pair to be empty so the relocations are not applied twice (MaskRay). This connects to Position-Independent Code and PIE.
Code and Specification
The canonical use is CPU-dispatched string/memory routines. Here is a self-contained IFUNC with the modern attribute syntax:
#include <stddef.h>
/* The two concrete implementations. */
static void *memcpy_generic(void *d, const void *s, size_t n) { /* portable */ }
static void *memcpy_avx2(void *d, const void *s, size_t n) { /* uses AVX2 */ }
/* The resolver: ordinary code that returns a function POINTER.
__builtin_cpu_init() must be called before __builtin_cpu_supports()
when you use GCC's built-in CPU detection. */
static void *resolve_memcpy(void)
{
__builtin_cpu_init();
if (__builtin_cpu_supports("avx2"))
return memcpy_avx2;
return memcpy_generic;
}
/* The public symbol. Its type is that of the IMPLEMENTATIONS, not the
resolver. Calling my_memcpy(...) transparently reaches the chosen impl. */
void *my_memcpy(void *d, const void *s, size_t n)
__attribute__((ifunc("resolve_memcpy")));Line by line: resolve_memcpy is declared static and takes no arguments in this portable form; it returns void * (a pointer to whichever implementation it selects). __builtin_cpu_init() populates GCC’s internal CPU-feature record from CPUID, and __builtin_cpu_supports("avx2") — available since GCC 4.8 (GCC manual) — tests one feature. The __attribute__((ifunc("resolve_memcpy"))) on my_memcpy is what makes the compiler emit my_memcpy as STT_GNU_IFUNC with resolve_memcpy as its resolver. A subtlety the toolchain enforces: the type of my_memcpy as seen by callers is the implementation’s type, so if the resolver and the implementations disagree you get confusing errors — Will Newton’s example deliberately defines the caller-visible function “in a separate file to prevent type confusion” (willnewton).
The resolver’s argument, per architecture. On AArch64 and 32-bit ARM the resolver receives the hardware-capability bitmask directly:
/* 32-bit ARM style: resolver gets HWCAP as an argument. */
void *resolve_func1(unsigned long int hwcap)
{
if (hwcap & HWCAP_ARM_NEON) return func1_neon;
if (hwcap & HWCAP_ARM_VFP) return func1_vfp;
return func1_arm;
}This is the exact shape from willnewton, which also warns that “on some architectures, like x86_64, it [hwcap] is not passed at all, and you have to determine hardware capabilities by hand.” On x86-64 the historical convention is therefore to use __builtin_cpu_supports (as above) or glibc’s internal __get_cpu_features(). glibc’s own resolvers read a process-wide record GLRO(dl_x86_cpu_features) populated at startup from CPUID.
Uncertain
Verify: the exact x86-64 IFUNC resolver calling convention for user code in current glibc. Reason: the 2013 willnewton source says x86-64 passes nothing; later glibc extended the resolver ABI (glibc 2.30 NEWS documents the AArch64 second argument =
AT_HWCAP2value via__ifunc_arg_t), but I could not pin an equivalent NEWS entry stating x86-64 user resolvers receive HWCAP + acpu_featurespointer, nor the precise version. Internal glibc x86-64 resolvers demonstrably use__get_cpu_features()/CPU_FEATURE_USABLErather than a passed argument. To resolve: readsysdeps/x86_64/dl-irel.h,sysdeps/x86/cpu-features.c, and the x86-64 psABI IFUNC section for the current argument convention. uncertain
Inspecting IFUNC in a binary. readelf -r shows the R_X86_64_IRELATIVE entries (note there is no symbol name — the “value” column is the resolver’s address), and readelf --dyn-syms shows the symbol with type IFUNC:
$ readelf -r a.out | grep IRELATIVE
0000004010 ... R_X86_64_IRELATIVE 0000000000401136
$ readelf --dyn-syms libc.so.6 | grep -i memcpy
... IFUNC GLOBAL DEFAULT 14 memcpy@@GLIBC_2.14
Failure Modes
The resolver runs in a half-relocated world. The single most common bug: a resolver that calls another function or reads a global that is not yet relocated. The guidelines are blunt — resolvers “must not access global data or call functions in other modules including the libc” and “must not access tls” (MaskRay). Concretely, with lazy binding enabled “calling a preemptible function in an ifunc resolver will crash due to accessing an unresolved GOTPLT entry,” and with static linking a resolver that touches thread-local storage “attempts to access TLS before it’s properly set up by __libc_setup_tls.” The symptom is a SIGSEGV inside ld.so or __libc_start_main before your main ever runs, often with a backtrace deep in _dl_relocate_object / elf_machine_rela.
Lazy binding makes it worse — or -z now masks it. Because a resolver might indirectly call an unresolved PLT entry, whether a buggy resolver crashes can depend on the binding mode. Linking with -z now (or running with LD_BIND_NOW=1) forces eager relocation, which changes the ordering hazards — MaskRay notes FreeBSD’s loader is more robust here because it runs multiple phases that “initialize states needed by ifunc resolvers” before dependent relocations. The practical lesson: keep resolvers pure — read CPU flags via a built-in, branch, return. Nothing else.
Cross-module ordering. An executable that defines an IFUNC referenced by a shared library it links against can crash because the library’s relocations run before the executable’s are complete. There is no general fix in the GNU loader beyond keeping resolvers self-contained; MaskRay’s article catalogues the corner cases.
Weaponization: IFUNC as a hook. Because a resolver is arbitrary code that runs automatically inside the linker before main, it is an attractive place to hide malicious behaviour. The infamous xz/liblzma backdoor (CVE-2024-3094, 2024) used exactly this: it planted an IFUNC resolver so that its code executed during relocation and hijacked a function pointer used by OpenSSH’s RSA_public_decrypt path. The ifuncd-up write-up documents “GNU IFUNC is the real culprit behind CVE-2024-3094” (robertdfrench). The security takeaway is that IFUNC turns “loading a library” into “executing attacker-chosen code during load,” which is a meaningfully larger attack surface than ordinary constructors.
Alternatives and When to Choose Them
- Function multi-versioning (
__attribute__((target_clones(...)))/target). GCC and Clang can auto-generate multiple versions of a function and dispatch among them. Under the hood on GNU systems this is implemented with IFUNC, so it shares the load-time-resolution model but hides the resolver boilerplate. Choose this when you just want “compile this hot function for SSE4.2 and AVX2 and pick automatically.” - A plain function pointer initialized once. A
static void (*impl)(void)set in a constructor or on first call is simpler, portable to non-GNU toolchains, and avoids the resolver-runs-in-ld.so hazards — at the cost of one indirect load per call (the pointer is not baked into the GOT the way an IFUNC target is) and a branch on first use. Choose this when portability matters more than shaving the last cycle, or when the decision needs data not available during relocation. - Explicit dispatch at the call site. For a single hot loop, an ordinary
if (have_avx2) ... else ...around two code paths is transparent and debuggable. IFUNC wins only when the same routine is called from many sites and you want the dispatch cost amortized to zero.
Because IFUNC is “an extension to the ELF standard and therefore will not be portable to other toolchains or systems” and is “not possible in Windows” (JasonCC), cross-platform libraries usually keep a portable function-pointer fallback.
Production Notes
In production, IFUNC’s flagship user is glibc itself. glibc “uses this feature to implement multiple versions of a few memory and string functions, including memmove, memset, memcpy, strcmp, strstr” (JasonCC); it also resolves things like __gettimeofday between a vDSO implementation and a syscall implementation. This is why straceing a fresh process shows the loader touching CPU features, and why the same libc.so.6 binary can be fast on both a 2012 Sandy Bridge and a 2024 server: the AVX-512 memcpy and the SSE2 memcpy are both in the file, and the resolver picks per-boot. The micro-architecture “levels” that distros now target (x86-64-v2, -v3, -v4) interact with this: a resolver may pick an implementation that requires a level the CPU lacks, which is one path to the “CPU does not support x86-64-v2” class of fatal loader errors when a distro’s glibc is built for a baseline the hardware does not meet.
Operationally, if a program crashes before main on some machines but not others, suspect an IFUNC resolver making an unsafe assumption (calling into another module, or selecting an instruction set the CPU does not have). LD_DEBUG=reloc and LD_DEBUG=symbols (see The Dynamic Linker) show relocation processing, and gdb with a breakpoint on the resolver — set via its local name, since the public symbol is the IFUNC — is the standard way to inspect what a resolver returns.
See Also
- Relocation Processing — the general relocation pass IFUNC’s
R_*_IRELATIVEslots into (processed last) - The Global Offset Table — where the resolver’s return value is stored
- The Procedure Linkage Table — the
.ipltstub every IFUNC call forwards through - Lazy Binding —
_dl_fixup, which invokeself_ifunc_invokeforSTT_GNU_IFUNCsymbols - Symbol Interposition and LD_PRELOAD — preemptible vs non-preemptible IFUNC (JUMP_SLOT vs IRELATIVE)
- Copy Relocations — a sibling special relocation with its own load-time behaviour
- Position-Independent Code and PIE — static-PIE self-relocation of IRELATIVE via
_dl_relocate_static_pie - ELF Relocation Entries — the on-disk
Elf64_Relaformat IRELATIVE uses - Symbol Resolution and Lookup Scope — how preemptible IFUNCs are looked up
- Linux Userspace Runtime MOC — parent map