← All posts
Security May 17, 2026 7 min read

LD_PRELOAD - how Linux lets you win the symbol race (and why that is dual-use)

The dynamic linker does not care whether you are debugging memory corruption, faking time for a test suite, or hiding a directory entry. It loads shared objects in an order you can influence, resolves symbols once, and y

View source on GitHub →

LD_PRELOAD: how Linux lets you win the symbol race

The dynamic linker does not care whether you are debugging memory corruption, faking time for a test suite, or hiding a directory entry. It loads shared objects in an order you can influence, resolves symbols once, and your code runs. This article walks through that mechanism with animated diagrams and ends on why the primitive itself is neutral.

Contents

  1. Static vs dynamic linking
  2. What LD_PRELOAD changes at runtime
  3. A minimal hook: replace a libc function
  4. Building a shared library
  5. Running a program with your library injected
  6. Calling the “real” function: dlsym(RTLD_NEXT, …)
  7. Why naive forwarding can recurse forever
  8. Load order and “first symbol wins”
  9. Not every program uses the same syscall path
  10. Legitimate uses in development and QA
  11. Valgrind and malloc interception
  12. fakeroot and libfaketime
  13. Dual use: the same linker rule in hostile software
  14. Typical hook surface for hiding behavior
  15. Detection ideas (imperfect but useful)
  16. Closing: neutral mechanism, human intent

1. Static vs dynamic linking

When you build a program, the linker can copy library code into the executable (static), or record names and load shared libraries at startup (dynamic). Dynamic linking is what makes LD_PRELOAD possible: at process start, the dynamic loader maps additional ELF objects and binds symbols.

Animated diagram comparing static and dynamic linking.

Static linking bakes dependencies into the binary; dynamic linking resolves them when the process starts.

2. What LD_PRELOAD changes at runtime

LD_PRELOAD (and the ld.so configuration that honors it) tells the loader to map one or more shared objects early. If your object exports a function with the same symbol name as one in libc or another dependency, the first definition along the link order can “win” for unresolved references from the main program and its other libraries. The diagram below is the high-level picture: your library sits on the path between the program and the usual shared libraries.

Diagram of ls calling through LD_PRELOAD into libc.

Conceptual view: a preloaded library can interpose on calls the program would otherwise resolve to libc.

3. A minimal hook: replace a libc function

The smallest educational example is often something like strcmp: export a function with the same C ABI, log or modify behavior, then (eventually) forward to the real implementation. That is enough to show students that symbol names are the contract the dynamic linker matches, not the source file you “meant” to call.

Animated flowchart of a strcmp hook.

Hook pattern: your strcmp runs first; forwarding to libc requires care (see dlsym below).

4. Building a shared library

On typical Linux toolchains you compile position-independent code and link a shared object. Flags like -shared and -fPIC are the mechanical requirements so the loader can place the mapping where the address space allows and still fix up relocations.

Diagram of gcc flags for shared libraries.

-fPIC + -shared produce a .so the dynamic linker can map into arbitrary processes.

5. Running a program with your library injected

From a shell, LD_PRELOAD=/path/to/hook.so ./program is the everyday form. Environment variables are inherited by child processes, which is convenient for tests and dangerous if an attacker can control the environment of privileged code (many services scrub the environment for this reason).

Split terminal: checker vs program with LD_PRELOAD.

Split view: one side runs the target with LD_PRELOAD; the other observes or grades behavior.

6. Calling the “real” function: dlsym(RTLD_NEXT, …)

If your hook needs to call the original implementation, you cannot usually just invoke strcmp by name from inside your hook: that resolves to your symbol again. The usual pattern is to look up the next symbol in the search order with dlsym(RTLD_NEXT, "strcmp") (or the appropriate function), stash the function pointer, and call through it.

Code diagram for dlsym RTLD_NEXT.

RTLD_NEXT means “skip the current object and find the next match” along the loader’s symbol search.

7. Why naive forwarding can recurse forever

A common homework bug is to call the libc function by name from the hook body, which loops through the hook forever, or to initialize the real pointer lazily in a way that retriggers the hook. Storing a stable function pointer from dlsym once (with correct linkage and initialization ordering) is the fix the diagrams emphasize.

Recursion vs real pointer diagram.

Contrasting infinite recursion through the hook with a saved “real” pointer obtained via dlsym.

8. Load order and “first symbol wins”

For a given reference, the dynamic linker walks a defined search order (simplified in the figure): preloads, dependencies, and so on. The first suitable definition bound for that reference is what your program calls. That single rule powers both safety tools and interposition malware.

Load order and symbol resolution for readdir.

Preload libraries are mapped early; interposition on readdir is a classic teaching example.

9. Not every program uses the same syscall path

Hooking readdir affects directory listing tools that go through the C library’s directory iteration. A program that uses open + read on file contents does not necessarily touch readdir at all. Defenders and attackers both have to model actual call paths, not just familiar function names.

ls uses readdir; cat uses open and read.

ls vs cat: different APIs, different interposition surfaces.

10. Legitimate uses in development and QA

Beyond classroom hooks, engineers use interposition for tracing, mocking time, substituting implementations in tests, and running dynamic analysis tools. The ecosystem is full of benign preload libraries; the mechanism is not exotic.

Cards for Valgrind, sanitizers, strace-style uses.

Representative legitimate uses: instrumentation, sanitizers, and observability.

11. Valgrind and malloc interception

Tools like Valgrind’s Memcheck interpose on allocation and memory operations so they can track definedness and errors. Conceptually, that is the same “load my object first and resolve malloc to my version” story, backed by a lot of engineering.

Valgrind malloc interception diagram.

Valgrind-style shadowing: allocator entry points become observation and bookkeeping sites.

12. fakeroot and libfaketime

Packaging workflows use fakeroot so build scripts that expect elevated ownership metadata can run unprivileged. Test suites use time libraries to make wall-clock behavior deterministic. Both are everyday reminders that “what the program thinks happened” can be deliberately rewritten at the library layer.

fakeroot and libfaketime diagram.

Environment-driven behavior changes without modifying the original binary.

13. Dual use: the same linker rule in hostile software

Because the loader is policy-blind, interposition can lie to user-space tools: filter directory entries, tamper with /proc-backed views, or wrap network-related APIs. Teaching this honestly means showing the symmetry: the rule that helps you test is the rule an implant can exploit in user space.

Dual use defender vs attacker diagram.

Same linker primitive; divergent goals and ethics.

14. Typical hook surface for hiding behavior

Educational malware analysis courses often list libc and pseudo-file interfaces: anything that turns kernel state into strings your admin tools parse. The figure below collects a few recurring names; real samples vary widely and defenders should treat this as intuition, not a checklist gospel.

Hooks for ls, find, proc tcp, readdir on proc.

Examples of APIs whose output shapes an administrator’s mental model of the system.

15. Detection ideas (imperfect but useful)

User-space interposition is fragile against determined investigation: static binaries, direct syscalls, or reading kernel data without going through hooked wrappers can bypass a given library. Still, checking for unexpected LD_PRELOAD in the environment, inspecting /proc/pid/maps and memory maps, and comparing tool behavior across execution paths are practical layers in depth.

Detection techniques diagram.

Illustrative detection angles: environment, static tooling, and library tracing.

16. Closing: neutral mechanism, human intent

The fascinating part is not that Linux has a “security bug” here. It is that a single, simple rule (ordered shared object mapping and symbol binding) creates enormous leverage for both builders and breakers. The kernel does not grade intent; people and organizations do.

Neutral primitive, both sides of the table.

Capability and documentation are neutral; responsibility sits with the humans using them.

Join Discord 1,582 members