9

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.

avner
  • 775
  • 1
  • 8
  • 11
  • what language are we talking about? – mvds Jul 18 '10 at 09:54
  • we are talking about the C language – avner Jul 18 '10 at 09:59
  • @user395074, then, perhaps, you should have adjusted your tags to reflect the language (click the "edit" link). Also, [preloader] tag doesn't seem like reflecting the OS component we're discussing. – P Shved Jul 18 '10 at 10:01
  • please share your code, and could you replace `command` with a simple script containing `#!/bin/sh` and `export`? do you check the return value of `unsetenv()`? – mvds Jul 18 '10 at 10:03
  • 1
    what does "remain with the effect of LD_PRELOAD" mean? didn't you just compile against mylib.so, so it will be loaded anyway? use strace to see what's going on in linking stage. – mvds Jul 18 '10 at 10:05
  • please put the code in the question rather than in the comments. and please not start `ls` but a script outputting the environment so you can actually *see* what the environment is. – mvds Jul 18 '10 at 13:43
  • You can edit and retag your own questions, @avner, even as a complete newcomer to the site. I've transcribed your comment for you; I recommend deleting your comment above and then let me know when you've done it so I can remove this comment. – Jonathan Leffler Jul 18 '10 at 16:28
  • Do you execute `file`? Not `./file`? not to be a nitpick but I don't know your `$PATH` – mvds Jul 18 '10 at 17:32
  • I used ./file or ./prog (I editted the question to reflect that) – avner Jul 19 '10 at 12:40
  • DUDE you are doing weird stuff! And you cannot strace it without caution or you'll preload `strace` as well. If I run `strace -f -E LD_PRELOAD=mylib.so /bin/echo 2>&1 |less` I see `mylib.so` being opened only once, and only once I see "Unsetting LD_PRELOAD" being printed. (add `fflush(stdout)` to see the `printf` as you do it, it may be buffered otherwise!) – mvds Jul 20 '10 at 10:10
  • OK, reproduced it with `strace -f -s 10240 -v -E LD_PRELOAD=mylib.so /bin/bash 2>&1 |less`, where you clearly see the LD_PRELOAD in the second `execve`. You problem is that the `bash` binary appearantly does it's own special thing with the environment, so *within bash* you cannot rely on `unsetenv()`. Remember that you're messing around in the `bash` binary! (e.g. unsetenv() depends on some other variable than `extern char**environ` - dump environ after your `unsetenv` and you'll see LD_PRELOAD standing there, just the same...) – mvds Jul 20 '10 at 10:22
  • regarding my last comment, see my answer: to be precise, it is not `unsetenv()` not acting on `environ`, but `unsetenv` not being the "real" `unsetenv` within the `bash` binary, and only therefore not acting on `environ`. – mvds Jul 20 '10 at 11:10

3 Answers3

9

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
  • 45,755
  • 8
  • 102
  • 111
  • 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 & execle is an option; 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 Jul 19 '10 at 12:21
  • this is getting ever more interesting, just updated my answer with some more suggestions – mvds Jul 19 '10 at 12:45
  • @avner: even more suggestions in the answer, built my own `.so` and still cannot see what you are seeing... – mvds Jul 19 '10 at 13:29
  • @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 Jul 19 '10 at 13:53
  • @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 Jul 19 '10 at 21:28
  • 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 Jul 19 '10 at 21:45
  • Have you tried `export LD_PRELOAD=` and then `ldd ./prog` to check if it is not linked for some other reason? – mvds Jul 19 '10 at 21:46
  • Solved! See my updated answer. See revision history for examples of preloading, I wiped them all. – mvds Jul 20 '10 at 10:47
  • @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 Jul 20 '10 at 18:11
3

(The answer is a pure speculation, and may be is incorrect).

Perhaps, when you fork your process, the context of the loaded libraries persists. So, mylib.so was loaded when you invoked the main program via LD_PRELOAD. When you unset the variable and forked, it wasn't loaded again; however it already has been loaded by the parent process. Maybe, you should explicitly unload it after forking.

You may also try to "demote" symbols in mylib.so. To do this, reopen it via dlopen with flags that place it to the end of the symbol resolution queue:

dlopen("mylib.so", RTLD_NOLOAD | RTLD_LOCAL);

P Shved
  • 96,026
  • 17
  • 121
  • 165
  • Thanks for Pavel's comment. This sounds reasonable; however, in my case this workaround is impossible. I can't change anything after fork, since I am using popen which does the fork/exec for me. (Still, fork & exec are fallback option if I can't do it with popen). The parent process must remain with the LD_PRELOAD environment (it is a customer process with multiple threads). – avner Jul 18 '10 at 11:37
  • 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 Jul 18 '10 at 13:47
  • @mvds, being re-opened and being unloaded/loaded are different things. You can reopen an already loaded library. – P Shved Jul 18 '10 at 15:40
  • 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 Jul 18 '10 at 15:48
  • 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 Jul 19 '10 at 12:49
  • @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 Jul 20 '10 at 09:15
  • As for the survival of loaded libraries theory, I think we may safely reject it. – mvds Jul 20 '10 at 11:01
0

the answer from mvds is incorrect!

popen() will spawn child process which inherit the preloaded .so lied in parent process. this child process don't care LD_PRELOAD environment.

scz
  • 1
  • 2