LD_PRELOAD does not work as expected

2020-02-26 03:24发布

问题:

Consider the following library which can be preloaded before any program execution:

// g++ -std=c++11 -shared -fPIC preload.cpp -o preload.so
// LD_PRELOAD=./preload.so <command>
#include <iostream>

struct Goodbye {
    Goodbye() {std::cout << "Hello\n";}
    ~Goodbye() {std::cout << "Goodbye!\n";}
} goodbye;

The problem is that, while the constructor of the global variable goodbye is always called, the destructor is not called for some programs, like ls:

$ LD_PRELOAD=./preload.so ls
Hello

For some other programs, the destructor is called as expected:

$ LD_PRELOAD=./preload.so man
Hello
What manual page do you want?
Goodbye!

Can you explain why the destructor is not called in the first case? EDIT: the above question has been already answered, that is a program might well use _exit(), abort() to exit.

However:

Is there a way to force a given function being called when a preloaded program exits?

回答1:

ls has atexit (close_stdout); as its initialisation code. When it finishes, it closes stdout (i.e. close(1)), so your cout, printf or write(1, ... operations will not print anything. It doesn't mean destructor isn't called. You can verify this by e.g. creating a new file in your destructor.

http://git.savannah.gnu.org/cgit/coreutils.git/tree/src/ls.c#n1285 here is the line in GNU coreutils ls.

It is not just ls, most of coreutils do that. Unfortunately, I don't know exact reason why they prefer to close it.

Side note on how this could be found (or at least what I did) - may help next time or with program with no source code access:

Destructor message is printed with /bin/true (simplest program I could think of), but isn't printed with ls or df. I started with strace /bin/true and strace /bin/ls and compared latest system calls. It shown close(1) and close(2) for ls, but none for true. After that things started to make sense and I just had to verify that destructor is called.



回答2:

If the program exits via _exit (POSIX) or _Exit (C99) or abnormal program termination (abort, fatal signals, etc.) then there's no way destructors could be called. I don't see any way around this.



回答3:

Like others said, a program might call via _exit(), _Exit() or abort() and your destructors won't even notice. To solve these cases you could override these functions by just writing a wrapper like the following the example below:

void
_exit(int status)
{
    void (*real__exit)(int) __attribute__((noreturn));
    const char *errmsg;

    /* Here you should call your "destructor" function. */
    destruct();

    (void)dlerror();
    real__exit = (void(*)(int))dlsym(RTLD_NEXT, "_exit");
    errmsg = dlerror();
    if (errmsg) {
        fprintf(stderr, "dlsym: _exit: %s\n", errmsg);
        abort();
    }

    real__exit(status);
}

But this wouldn't solve all the possibilities of a program escaping without your library's knowledge, because those are not the only exit points an application could have. It could also trigger the exit system call via the syscall() function and to avoid it you would have to wrap it too.

Another way a program could exit is by receiving an unhandled signal, so you should also handle (or wrap?) all signals that could trigger the death of a program. Read the signal(2) man page for more information but please be aware that signals like SIGKILL (9) cannot be handled and an application could destroy itself by calling kill(). With that being said and unless you don't expect to handle insane applications written by crazy monkeys you should wrap kill() too.

Another system call you'd have to wrap is execve().

Anyway, a system call (like _exit) could also be triggered directly via an assembly int 0x80 instruction or the obsolete _syscallX() macro. How'd you wrap it if not from outside the application (like strace or valgrind)? Well, if you expect this kind of behaviour in you programs I suggest you drop the LD_PRELOAD technique and start thinking about doing like strace and valgrind do (using ptrace() from another process) or creating a Linux kernel module to trace it.