views:

136

answers:

2

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");
    pclose(fp);
}

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.

+2  A: 
Pavel Shved
avner
are you saying that after `exec` the loaded libraries persist? I see in an strace that even libc is re-opened again after `exec`, can't imagine some other library will prevail.
mvds
@mvds, being re-opened and being unloaded/loaded are different things. You can reopen an already loaded library.
Pavel Shved
I just wondered how a loaded library would survive (quoting execve(2)): "execve() does not return on success, and the text, data, bss, and stack of the calling process are overwritten by that of the program loaded."
mvds
bellow is example captured using "LD_DEBUG=symbols". It clearly shows that symbols are searched in mylib.so before libc and any other place: 4779: symbol=__cxa_finalize; lookup in file=ls [0] 4779: symbol=__cxa_finalize; lookup in file=/usr/lib64/libmylib.so [0] 4779: symbol=__cxa_finalize; lookup in file=/lib64/librt.so.1 [0] 4779: symbol=__cxa_finalize; lookup in file=/lib64/libacl.so.1 [0] 4779: symbol=__cxa_finalize; lookup in file=/lib64/libselinux.so.1 [0] 4779: symbol=__cxa_finalize; lookup in file=/lib64/libc.so.6 [0]
avner
@mvds, thanks a lot for your educational answer. It puzzled me and helped me a lot to focus my question. I managed to write it in 4 lines of code. I edited my question and provided exact code that reproduce the problem!
avner
As for the survival of loaded libraries theory, I think we may safely reject it.
mvds
@mvds, ok, fixed the answer :-)
Pavel Shved
+3  A: 

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]);
fflush(stdout);
FILE *fp = popen("ls", "r");
pclose(fp);
}

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

Q.E.D.

mvds
LD_PRELOAD is not in the environment of the child process (neither in the parent after the unsetenv).Pavel's answer hints about the reason: LD_PRELOAD is used by the loader and not by my program. mylib.so is already loaded and the loader probably keeps its behavior without re-reading LD_PRELOAD.fork however, popen is much preffered since it let me read the output of the child-process in a simple way.strace gace a lot of info that didn't help me.I used: export LD_DEBUG=symnols and saw clearly that all symbols are searched in mylib.so before any other lib.
avner
this is getting ever more interesting, just updated my answer with some more suggestions
mvds
@avner: even more suggestions in the answer, built my own `.so` and still cannot see what you are seeing...
mvds
@avner: even with `popen("/bin/ls","r");` as you did I get the same results. are you very sure you didn't miss anything else?
mvds
@mvds - first thanks a lot for the detailed examples and explanations. Your code work for me as you wrote (In other words LD_PRELOAD had no effect on the child as you expected).Your answer surely worth voting up. I tried to vote up, but failed because voting up requires 15 reputation which i don't have yet )-:Anyhow, I am now puzzled why in my code (the real one, not the short example) I still suffer from that problem.one difference is that my problem is with the 'bash' that popen starts and not with the command itself. Still, it doesn't explain enough the diffence in behavior.
avner
I think you have 16 rep now ;-) I made a sample here with `popen()` as I stated and that ok as well. Even with `popen("/bin/sh testscript","r");` and `testscript` containing `export` and `/bin/ls`. What On Earth are you doing man?! have you messed with `/etc/ld.so.*`?
mvds
Have you tried `export LD_PRELOAD=` and then `ldd ./prog` to check if it is not linked for some other reason?
mvds
Solved! See my updated answer. See revision history for examples of preloading, I wiped them all.
mvds
@mvds, I am ipressed! 1) your explanation clarified the mystery 2) your workaround "my_unsetenv" worked like a charm 3) I found the following in the output I got previously with LD_DEBUG=all 1.15414- 15414: calling init: ./mylib.so 1.15414- 15414: 1.15414: 15414: symbol=unsetenv; lookup in file=bash [0] 1.15414- 15414: binding file ./mylib.so [0] to bash [0]: normal symbol `unsetenv' [GLIBC_2.2.5] This certainly proove your solution!!
avner
good to hear! time to accept don't you think ;-)
mvds