LD_PRELOAD โ€” Preloading Shared Libraries

 

LD_PRELOAD โ€” Preloading Shared Libraries
Chapter 42.5 โ€” The Linux Programming Interface | EmbeddedPathashala
๐Ÿ“Œ Topic
Function Interposition
๐Ÿงช Examples
3 Code Demos
๐ŸŽฏ Level
Intermediate
๐Ÿ“– Source
TLPI Ch 42.5

Key Concepts
LD_PRELOAD /etc/ld.so.preload Function interposition Dynamic linker search order RTLD_NEXT dlsym() SUID/SGID security

What is LD_PRELOAD?

LD_PRELOAD is an environment variable that tells the dynamic linker to load one or more shared libraries before any other libraries โ€” including the C standard library. Because these libraries are searched first, if they define a function that also exists in another library, their version wins.

This technique is called function interposition โ€” you intercept a function call and replace it with your own implementation. This is extremely useful for:

  • Testing: mock out a function without modifying source code
  • Debugging: log all calls to malloc(), open(), etc.
  • Patching: fix a bug in a third-party library without recompiling it
  • Profiling: count how many times a function is called

Program calls x1() โ†’ Dynamic Linker
Searches in order:
โ†’ libalt.so (PRELOADED)
Found! Use this x1()
Without LD_PRELOAD โ†’ linker would find x1() in libdemo.so instead

Example 1 โ€” Basic Function Override with LD_PRELOAD

Suppose we have a library libdemo.so with functions x1() and x2(), and a program that calls both. We want to replace x1() with our own version without touching the program or libdemo.so.

/* mod1.c โ€” part of libdemo.so */
#include <stdio.h>
void x1(void) { printf("Called mod1-x1 DEMO\n"); }
/* mod2.c โ€” part of libdemo.so */
#include <stdio.h>
void x2(void) { printf("Called mod2-x2 DEMO\n"); }
# Build libdemo.so
gcc -g -c -fPIC -Wall mod1.c mod2.c
gcc -g -shared -o libdemo.so mod1.o mod2.o
/* prog.c โ€” program using libdemo */
#include <stdio.h>
void x1(void);
void x2(void);

int main(void) {
    x1();
    x2();
    return 0;
}
# Build prog against libdemo
gcc -g -o prog prog.c -L. -ldemo
LD_LIBRARY_PATH=. ./prog

# Output (without preload):
# Called mod1-x1 DEMO
# Called mod2-x2 DEMO
/* libalt.c โ€” our override library that replaces x1() only */
#include <stdio.h>
void x1(void) { printf("Called mod1-x1 ALT\n"); }
/* Note: x2() is NOT defined here โ€” linker will fall through to libdemo.so */
# Build the override library
gcc -g -c -fPIC -Wall libalt.c
gcc -g -shared -o libalt.so libalt.o

# Preload libalt.so โ€” its x1() overrides libdemo's x1()
LD_PRELOAD=libalt.so LD_LIBRARY_PATH=. ./prog

# Output (with preload):
# Called mod1-x1 ALT     โ† from libalt.so
# Called mod2-x2 DEMO    โ† still from libdemo.so (x2 not in libalt)
Key behavior: Only functions you define in the preloaded library are overridden. Any function not present in libalt.so continues to be resolved from the original library.

Example 2 โ€” Wrapping malloc() to Log Allocations

A classic use of LD_PRELOAD is wrapping standard library functions like malloc() to add logging or debugging. We use dlsym(RTLD_NEXT, ...) to get a pointer to the original function, then call it from our wrapper.

/* malloc_trace.c โ€” wraps malloc() to log every allocation */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>

/* Function pointer to hold the real malloc */
static void *(*real_malloc)(size_t size) = NULL;

/* Our constructor runs first โ€” grab the real malloc before anything */
void __attribute__((constructor)) init_tracer(void)
{
    real_malloc = dlsym(RTLD_NEXT, "malloc");
    if (!real_malloc) {
        fprintf(stderr, "Cannot find real malloc: %s\n", dlerror());
        exit(1);
    }
    fprintf(stderr, "[tracer] malloc wrapper installed\n");
}

/* Our malloc โ€” logs the call then delegates to the real malloc */
void *malloc(size_t size)
{
    void *ptr = real_malloc(size);
    fprintf(stderr, "[malloc] size=%zu  ptr=%p\n", size, ptr);
    return ptr;
}
# Build the tracing library
gcc -g -c -fPIC -Wall malloc_trace.c
gcc -g -shared -o libmtrace.so malloc_trace.o -ldl

# Run ANY program with malloc tracing (no source changes needed!)
LD_PRELOAD=./libmtrace.so ls /tmp

# Example output:
# [tracer] malloc wrapper installed
# [malloc] size=1024  ptr=0x55a3f2b01260
# [malloc] size=64    ptr=0x55a3f2b012a0
# ... (followed by normal ls output)
RTLD_NEXT: When you pass RTLD_NEXT as the first argument to dlsym(), it finds the next occurrence of the symbol in the library search order โ€” i.e., the original function in the library that comes after your preloaded library. This is how you call the real function from your wrapper.

Example 3 โ€” System-wide Preload via /etc/ld.so.preload

The LD_PRELOAD environment variable works per-process: only the process where you set it gets the preloaded library. For system-wide preloading (affecting all processes), use the file /etc/ld.so.preload.

# /etc/ld.so.preload
# List library paths separated by whitespace (one per line or space-separated)
# These are loaded for EVERY process on the system

/usr/lib/libsystem_audit.so
/usr/local/lib/libsecure_malloc.so
Method Scope Priority
LD_PRELOAD=lib.so ./prog Per-process Loaded first (highest)
/etc/ld.so.preload System-wide (all processes) After LD_PRELOAD
Normal library search Per normal linker rules After both above
Security: For security reasons, the kernel ignores LD_PRELOAD for set-user-ID (SUID) and set-group-ID (SGID) programs. An attacker cannot use LD_PRELOAD to hijack a SUID binary like /usr/bin/passwd. The system-wide /etc/ld.so.preload is still applied to SUID binaries, so be careful what you put there.

Preloading Multiple Libraries

You can preload multiple libraries at once. Separate them with spaces or colons:

# Space-separated
LD_PRELOAD="libfoo.so libbar.so" ./prog

# Colon-separated
LD_PRELOAD=libfoo.so:libbar.so ./prog

# Multiple with full paths
LD_PRELOAD=/usr/local/lib/libfoo.so:/home/ravi/libbar.so ./prog

Libraries listed earlier in LD_PRELOAD are searched first. If both libfoo.so and libbar.so define x1(), the version from libfoo.so wins.

๐ŸŽฏ Interview Questions & Answers
Q1. What is LD_PRELOAD and what is it used for?
LD_PRELOAD is an environment variable that instructs the dynamic linker to load specified shared libraries before any others. Functions in the preloaded library override same-named functions from other libraries. Common uses include: testing (mocking functions), debugging (logging all malloc calls), and patching third-party binaries.
Q2. If LD_PRELOAD overrides only x1() but not x2(), what happens when the program calls x2()?
The dynamic linker searches libraries in order. Since x2() is not defined in the preloaded library, the linker continues searching and finds it in the original library. The original x2() is called normally.
Q3. How do you call the original function from inside an LD_PRELOAD wrapper?
Use dlsym(RTLD_NEXT, "function_name"). RTLD_NEXT tells dlsym() to find the next occurrence of the symbol after the current library in the linker’s search order โ€” that is, the original function in the underlying library.
Q4. Does LD_PRELOAD work for SUID programs like /usr/bin/passwd?
No. For security reasons, the dynamic linker ignores LD_PRELOAD for set-user-ID and set-group-ID programs. This prevents privilege escalation attacks via function interposition.
Q5. What is the difference between LD_PRELOAD and /etc/ld.so.preload?
LD_PRELOAD applies per-process (only to the current shell/execution). /etc/ld.so.preload is a system-wide file that affects every process started on the system. Libraries from LD_PRELOAD are loaded before those in /etc/ld.so.preload.
Q6. What is function interposition? Give a practical use case.
Function interposition means replacing a function call at link time with a different implementation โ€” typically using LD_PRELOAD. A practical use case is wrapping malloc() to log all memory allocations for debugging memory leaks, without modifying the application’s source code.

Leave a Reply

Your email address will not be published. Required fields are marked *