avner avner - 3 months ago 37x
Linux Question

LD_PRELOAD affects new child even after unsetenv("LD_PRELOAD")

my code is as follows: preload.c, with the following content:

#include <stdio.h>
#include <stdlib.h>

int __attribute__((constructor)) main_init(void)
printf("Unsetting LD_PRELOAD: %x\n",unsetenv("LD_PRELOAD"));
FILE *fp = popen("ls", "r");

then in the shell (do the 2nd command with care!!):

gcc preload.c -shared -Wl,-soname,mylib -o mylib.so -fPIC
LD_PRELOAD=./mylib.so bash

!!! be carefull with the last command it will result with endless loop of forking "sh -c ls". Stop it after 2 seconds with ^C, (or better ^Z and then see ps).

More info

  1. This problem relate to bash in some way; either as the command that the user run, or as the bash the popen execute.

  2. additional Key factors: 1) perform the popen from the pre-loaded library, 2) probably need to do the popen in the initialization section of the library.

  3. if you use:

    LD_DEBUG=all LD_DEBUG_OUTPUT=/tmp/ld-debug LD_PRELOAD=./mylib.so bash

    instead of the last command, you will get many ld-debug files, named /tmp/ld-debug.*. One for each forked process. IN ALL THESE FILES you'll see that symbols are first searched in mylib.so even though LD_PRELOAD was removed from the environment.


edit: so the problem/question actually was: howcome can't you unset LD_PRELOAD reliably using a preloaded main_init() from within bash.

The reason is that execve, which is called after you popen, takes the environment from (probably)

extern char **environ;

which is some global state variable that points to your environment. unsetenv() normally modifies your environment and will therefore have an effect on the contents of **environ.

If bash tries to do something special with the environment (well... would it? being a shell?) then you may be in trouble.

Appearantly, bash overloads unsetenv() even before main_init(). Changing the example code to:

extern char**environ;

int  __attribute__((constructor))  main_init(void)
int i;
printf("Unsetting LD_PRELOAD: %x\n",unsetenv("LD_PRELOAD"));
printf("LD_PRELOAD: \"%s\"\n",getenv("LD_PRELOAD"));
printf("Environ: %lx\n",environ);
printf("unsetenv: %lx\n",unsetenv);
for (i=0;environ[i];i++ ) printf("env: %s\n",environ[i]);
FILE *fp = popen("ls", "r");

shows the problem. In normal runs (running cat, ls, etc) I get this version of unsetenv:

unsetenv: 7f4c78fd5290
unsetenv: 7f1127317290
unsetenv: 7f1ab63a2290

however, running bash or sh:

unsetenv: 46d170

So, there you have it. bash has got you fooled ;-)

So just modify the environment in place using your own unsetenv, acting on **environ:

for (i=0;environ[i];i++ )
    if ( strstr(environ[i],"LD_PRELOAD=") )
         printf("hacking out LD_PRELOAD from environ[%d]\n",i);
         environ[i][0] = 'D';

which can be seen to work in the strace:

execve("/bin/sh", ["sh", "-c", "ls"], [... "DD_PRELOAD=mylib.so" ...]) = 0