2

I am writing a toy malloc(3) implementation, loaded with LD_PRELOAD, as an exercise. I have a function annotated with __attribute__((destructor)) to dump a list of allocations and their status on exit for debugging purposes but I found it doesn't run in some cases. Specifically, it does run on locally-compiled code but not against system binaries like /bin/ls (on Arch Linux). A constructor-tagged function does work for both cases, though.

A simple repro of the problem is:

main.c:

#include <stdio.h>

// compile with: clang -o main main.c

int main() {
    printf("main\n");
}

wrap.c:

#include <stdio.h>

// compile with: clang -o wrap -shared -fPIC wrap.c

void __attribute__((constructor)) say_hi() {
    printf("hi y'all\n");
}

void __attribute__((destructor)) say_bye() {
    printf("bye y'all\n");
}

Destructor runs with main.c:

$ LD_PRELOAD=./wrap ./main
hi y'all
main
bye y'all

Destructor doesn't run with /bin/ls:

$ LD_PRELOAD=./wrap /bin/ls
hi y'all
main  main.c  wrap  wrap.c

I can't work out how to debug this. LD_DEBUG=all doesn't show anything useful. ldd main and ldd /bin/ls look comparable. Both binaries are dynamically linked. The GCC docs don't list any caveats I should be aware of, as far as I can see.

markdrayton
  • 485
  • 6
  • 11
  • 1
    Your destructor probably runs, and it could be that you don't see it because standard output was closed. Try adding something like `open("path/to/bogus/file/that/does/not/exist/foo/bar", O_RDONLY);` in your destructor and see if that appears in the output of `strace env LD_PRELOAD=./wrap /bin/ls`. If not, then `ls` might be exiting early with `_Exit()` or `_exit()`, which do not run destructors. – Marco Bonelli May 10 '22 at 17:57

1 Answers1

4

For some reason, GNU ls closes stdout before it exits, via an atexit handler that presumably gets run before your destructor.

So probably your destructor runs just fine, but doesn't print anything because you are writing to a stream that is closed.

You'll probably need to have your destructor, and your wrapper in general, do its logging in some more robust fashion. Opening a raw fd and write()ing to it would be better, but even so, some programs will do for (i = 0; i < 1024; i++) close(i); which would close your log fd as well. The only really safe way might be to open/write/close every time, or to maintain your own log buffer somewhere in memory and manually write it out when full.


From comments, the likely reason is that stdout may be directed to a file, and since it is buffered, some of the data may not be written out until the stream is closed. An error could occur at that point (full disk, I/O error, etc). If ls were to leave the closing of stdout to the C library startup/shutdown code as your simple main.c does, then this error would go undetected and ls would still exit with status 0; there would be no way for the parent process to know that the output file was incomplete. So by calling fclose(stdout) explicitly, ls can handle the failure, report it if possible, and exit with nonzero status to ensure that the parent knows not to trust the output.

As ShadowRanger points out, all the GNU coreutils have this behavior.

Nate Eldredge
  • 48,811
  • 6
  • 54
  • 82
  • 2
    *"It's important to detect such failures and exit nonzero because many tools (most notably 'make' and other build-management systems) depend on being able to detect failure in other tools via their exit status."* As best I can tell they are registering to do this via `atexit` in case they crash for some reason because they *must* fail if something goes wrong so the scripts stop. [source](https://github.com/coreutils/gnulib/blob/master/lib/closeout.c#L117). It's worth remembering that crashing is a valid successful exit in many cases. – Mgetz May 10 '22 at 18:02
  • 2
    Oh, so I bet it's in case you do `ls > somefile`, and `somefile` incurs an I/O error when the stdout buffer is flushed. By explicitly closing `stdout`, then `ls` has a chance to handle the error or at least exit with nonzero status. – Nate Eldredge May 10 '22 at 18:08
  • 1
    And to be clear, it's not just `ls`; [all the core utils do this](https://github.com/wertarbyte/coreutils/search?q=close_stdout) (even ones like `cat` that bypass `stdout` in favor of direct use of `STDOUT_FILENO` for everything but argument parsing). – ShadowRanger May 10 '22 at 18:08
  • @NateEldredge based on what I saw on github yes... there were a few exceptions but they were rare. But unlike most programs these *must* have an exit code even if it's just an OOPS because the alternative is worse – Mgetz May 10 '22 at 18:11
  • To see that it really is running, doing `dprintf(STDIN_FILENO, "bye y'all\n");` instead will result in the message being printed. – Joseph Sible-Reinstate Monica May 10 '22 at 18:42