Search code examples
cllvmaddress-sanitizerleak-sanitizer

Runtime check for LeakSanitizer (detect_leaks=1)


I have an issue where any Leak Sanitizer backtraces that go through dynamically loaded libraries report Unknown Module for any function calls within that library.

Direct leak of 48 byte(s) in 1 object(s) allocated from:
    #0 0x4e3e36 in malloc (/usr/sbin/radiusd+0x4e3e36)
    #1 0x7fb406e95f69  (<unknown module>)
    #2 0x7fb406eafc36  (<unknown module>)
    #3 0x7fb406eafd40  (<unknown module>)
    #4 0x7fb406ea3364  (<unknown module>)
    #5 0x7fb4063de7d4  (<unknown module>)
    #6 0x7fb4063c61c4  (<unknown module>)
    #7 0x7fb406617863  (<unknown module>)
    #8 0x7fb415620681 in dl_load_func /usr/src/debug/freeradius-server-4.0.0/src/main/dl.c:194:34
    #9 0x7fb41561edab in dl_symbol_init_walk /usr/src/debug/freeradius-server-4.0.0/src/main/dl.c:301:7
    #10 0x7fb41561df1e in dl_module /usr/src/debug/freeradius-server-4.0.0/src/main/dl.c:748:6
    #11 0x7fb41561f3db in dl_instance /usr/src/debug/freeradius-server-4.0.0/src/main/dl.c:853:20
    #12 0x7fb41564f4ab in module_bootstrap /usr/src/debug/freeradius-server-4.0.0/src/main/module.c:827:6
    #13 0x7fb41564ed56 in modules_bootstrap /usr/src/debug/freeradius-server-4.0.0/src/main/module.c:1070:14
    #14 0x5352bb in main /usr/src/debug/freeradius-server-4.0.0/src/main/radiusd.c:561:6
    #15 0x7fb41282ab34 in __libc_start_main (/lib64/libc.so.6+0x21b34)
    #16 0x4204ab in _start (/usr/sbin/radiusd+0x4204ab)

I've had an almost identical issue with valgrind before, and I know it's due to the libraries being unloaded with dlclose on exit, and the symbols being unavailable when the symbolizer runs.

With valgrind the fix is simple

/*
 *  Only dlclose() handle if we're *NOT* running under valgrind
 *  as it unloads the symbols valgrind needs.
 */
if (!RUNNING_ON_VALGRIND) dlclose(module->handle);        /* ignore any errors */

RUNNING_ON_VALGRIND being a macro provided by the valgrind library for detecting if the program is being valground.

I can't see anything in the LSAN docs for a similar feature for when ASAN_OPTIONS=detect_leaks=1 is set.

Does anyone know if it's possible to perform a runtime check for running under LSAN?


Solution

  • The LSAN interface headers allow the user to define a callback __lsan_is_turned_off to allow the program to disable the leak checker. This callback is only executed if LSAN is enabled.

    #include <sanitizer/lsan_interface.h>
    
    static bool running_under_lsan = false;
    
    int __attribute__((used)) __lsan_is_turned_off(void)
    {
        running_under_lsan = true;
        return 0;
    }
    

    EDIT: It's actually more complicated than that. As @yugr commented It appears __lsan_is_turned_off is only executed when a process or child process exits.

    There is however a solution!

    #include <stdio.h>
    #include <stdlib.h>
    #include <stdbool.h>
    #include <unistd.h>
    
    #include <string.h>
    #include <errno.h>
    
    #include <sanitizer/common_interface_defs.h>
    
    static int from_child[2] = {-1, -1};
    static int pid;
    
    int __attribute__((used)) __lsan_is_turned_off(void)
    {
        uint8_t ret = 1;
    
        /* Parent */
        if (pid != 0) return 0;
    
        /* Child */
        if (write(from_child[1], &ret, sizeof(ret)) < 0) {
            fprintf(stderr, "Writing LSAN status failed: %s", strerror(errno));
        }
        close(from_child[1]);
        return 0;
    }
    
    int main(int argc, char **argv)
    {
        uint8_t ret = 0;
    
        if (pipe(from_child) < 0) {
            fprintf(stderr, "Failed opening internal pipe: %s", strerror(errno));
            exit(EXIT_FAILURE);
        }
    
        pid = fork();
        if (pid == -1) {
            fprintf(stderr, "Error forking: %s", strerror(errno));
            exit(EXIT_FAILURE);
        }
    
        /* Child */
        if (pid == 0) {
            close(from_child[0]);   /* Close parent's side */
            exit(EXIT_SUCCESS);
        }
    
        /* Parent */
        close(from_child[1]);       /* Close child's side */
    
        while ((read(from_child[0], &ret, sizeof(ret)) < 0) && (errno == EINTR));
    
        close(from_child[0]);       /* Close our side (so we don't leak FDs) */
    
        /* Collect child */
        waitpid(pid, NULL, 0);
    
        if (ret) {
            printf("Running under LSAN\n");
        } else {
            printf("Not running under LSAN\n");
        }
    
        exit(EXIT_SUCCESS);
    }
    

    Example:

    clang -g3 -fsanitize=address foo.c
    
    ASAN_OPTIONS='detect_leaks=1' ./a.out
    Running under LSAN
    
    ASAN_OPTIONS='detect_leaks=0' ./a.out
    Not running under LSAN