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
- Static vs dynamic linking
- What
LD_PRELOADchanges at runtime - A minimal hook: replace a libc function
- Building a shared library
- Running a program with your library injected
- Calling the “real” function:
dlsym(RTLD_NEXT, …) - Why naive forwarding can recurse forever
- Load order and “first symbol wins”
- Not every program uses the same syscall path
- Legitimate uses in development and QA
- Valgrind and malloc interception
- fakeroot and libfaketime
- Dual use: the same linker rule in hostile software
- Typical hook surface for hiding behavior
- Detection ideas (imperfect but useful)
- 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.

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.

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.

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.

-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 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.

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.

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.

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 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.

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-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.

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.

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.

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.

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.

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