EDIT 2025-06-09 Added libSegFault example as suggested by Jeremy from #include.
LD_PRELOAD is an environment variable on Unix-like operating systems that allows users to specify a shared library to be loaded before others when a program is executed. This can be used to override functions in existing libraries or to inject custom code into applications. This functionality is provided by system dynamic linker and works only for dynamically linked executables. Statically linked binaries skip this entirely.
This post will be focused on Linux and BSDs.
First, let's see what the dynamic linker is.
When a dynamically linked program is launched, before even calling the main function of the program a few things are done. One of the first things that the operating system does is that it reads the path of the dynamic linker from the executable image and then execute that as a program. This has to succeed or the whole program execution fails. When dynamic loader is executed, it loads the program image and all the dynamically loaded shared libraries that the executed program needs, and then starts the execution.
So basically, the dynamic linker is another program that handles execution of all other dynamically executed programs on the system.
On Linux the dynamic linker will be something like ld-linux.so, e.g. on my system it's /lib64/ld-linux-x86-64.so.2.
On FreeBSD it will be something like /libexec/ld-elf.so.1
So, how does LD_PRELOAD work?
It basically adds a list of shared libraries to be loaded by the dynamic linker after the program image is loaded, but before its shared object dependencies are loaded. This way, when a particular function symbol name is looked for, it will be found in preloaded shared object first. This is how function overriding is achieved.
Zlibc, also known as uncompress.so - facilitates transparent decompression when used through the LD_PRELOAD. It is therefore possible to read compressed (gzipped) files data as if they were uncompressed, essentially adding support for reading compressed files to any application.
Another interesting example is LibSegFault, which is preloadable library that handles segfaults and produce nice stack trace on crashes.
The following is a simple example of a library that overrides malloc function.
It uses dlsym(RTLD_NEXT, "malloc") to look up the real malloc that is being overridden and counts the number of all allocations.
#define _GNU_SOURCE
#include
#include
#include
size_t malloc_count = 0;
void *malloc(size_t size) {
static void *(*real_malloc)(size_t) = NULL;
if (!real_malloc) {
real_malloc = dlsym(RTLD_NEXT, "malloc");
}
void *p = real_malloc(size);
malloc_count++;
return p;
}
Note that you need to be careful when adding custom code to malloc, not to call itself recursively, which might be easier than you think.
This example is not very exciting, so let's take a look at something more interesting.
Another example would be a library that overrides read and write calls to add a sleep before doing the actual work:
This example comes from crawlio
This time the actual implementation of read and write is loaded in the constructor function. This is a function that is executed just after the library is loaded. This way the overloaded functions are loaded only once.
#define _GNU_SOURCE
#include
#include
#include
typedef ssize_t (*orig_read_t)(int fd, void *buf, size_t count);
typedef ssize_t (*orig_write_t)(int fd, const void *buf, size_t count);
static orig_read_t original_read = NULL;
static orig_write_t original_write = NULL;
static void sleep_ms(unsigned int ms) {
if (ms > 0) {
struct timespec ts;
ts.tv_sec = ms / 1000;
ts.tv_nsec = (ms % 1000) * 1000000;
nanosleep(&ts, NULL);
}
}
__attribute__((constructor))
static void init() {
original_read = (orig_read_t)dlsym(RTLD_NEXT, "read");
if (!original_read) {
fprintf(stderr, "Error loading original read function: %s\n", dlerror());
}
original_write = (orig_write_t)dlsym(RTLD_NEXT, "write");
if (!original_write) {
fprintf(stderr, "Error loading original write function: %s\n", dlerror());
}
}
ssize_t read(int fd, void *buf, size_t count) {
sleep_ms(200);
return original_read(fd, buf, count);
}
ssize_t write(int fd, const void *buf, size_t count) {
sleep_ms(200);
return original_write(fd, buf, count);
}
First, compile the code into a shared library:
$ gcc -shared -fPIC -o my_readwrite.so my_readwrite.c -ldl
Then run a program with the library preloaded:
LD_PRELOAD=./my_readwrite.so find /tmp
You should notice that find prints results slower than normally.
malloc.LD_PRELOAD environment variable.--preload command-line option when invoking the dynamic linker directly.ls one can do the same by executing dynamic linker and passing the executable as an argument: /lib64/ld-linux-x86-64.so.2 /bin/ls (full path to ls in required in this case). And this is what actually is being done by the system every time a dynamically linked executable is run. The dynamic linker accepts a few flags, one of them being --preload and it works the same as LD_PRELOAD environment variable.It's important to understand security implications of LD_PRELOAD. It can be used to override any system call, like open or execve and therefore can be used to hijack execution flow or hide malicious behaviour.
Linux provides some mitigations against that.
if the dynamic linker determines that a binary should be run in secure-execution mode (e.g. when executing a set-user-ID or set-group-ID program) preloading is very limited. E.g. pathnames containing slashes are ignored and shared objects are preloaded only from the standard search directories and only if they have set-user-ID mode bit enabled.
On FreeBSD LD_PRELOAD is ignored altogether for set-user-ID and set-group-ID programs.
Some of the uses described above might be achieved with the use of eBPF, but not al of them. eBPF is much more restricted, but also doesn't introduce the possible security issues of injecting arbitrary code.