Search code examples
gccmemory-leaksgstreamerglibleak-sanitizer

GStreamer minimal program leaks memory


I have a minimal GStreamer program:

#include <gst/gst.h>

int main() {
   gst_init(NULL, NULL);
   gst_deinit();
}

I build it with gcc test.c $(pkg-config --cflags --libs gstreamer-1.0) -fsanitize=address (gcc is version 12.1.0), run it and get the following output from the address sanitizer:

==87326==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 16384 byte(s) in 1 object(s) allocated from:
    #0 0x7f53e28bfa89 in __interceptor_malloc /usr/src/debug/gcc/libsanitizer/asan/asan_malloc_linux.cpp:69
    #1 0x7f53e26c1b19 in g_malloc (/usr/lib/libglib-2.0.so.0+0x5db19)

SUMMARY: AddressSanitizer: 16384 byte(s) leaked in 1 allocation(s).

I'm new to GStreamer and GLib. Is this normal for GStreamer programs? And if it is, what would be an elegant way to ignore this leak when running unit tests with sanitizers?


Solution

  • The issue is that there are some versions of g_intern_string, for example, the one in libglib-2.0.so.0.6400.6, that have a bug where, when they grow the buckets array for a hash map, they fail to free the old array.

    The leak is on the last instruction shown in this disassembly of g_intern_string:

       0x7ffff7b44a70 <g_intern_string>:    endbr64 
       0x7ffff7b44a74 <g_intern_string+4>:  push   %r12
       0x7ffff7b44a76 <g_intern_string+6>:  push   %rbp
       0x7ffff7b44a77 <g_intern_string+7>:  push   %rbx
       0x7ffff7b44a78 <g_intern_string+8>:  test   %rdi,%rdi
       0x7ffff7b44a7b <g_intern_string+11>: 
        je     0x7ffff7b44b88 <g_intern_string+280>
       0x7ffff7b44a81 <g_intern_string+17>: mov    %rdi,%rbp
       0x7ffff7b44a84 <g_intern_string+20>: 
        lea    0xc5225(%rip),%rdi        # 0x7ffff7c09cb0
       0x7ffff7b44a8b <g_intern_string+27>: callq  0x7ffff7b814d0 <g_mutex_lock>
       0x7ffff7b44a90 <g_intern_string+32>: 
        mov    0xc5211(%rip),%rdi        # 0x7ffff7c09ca8
       0x7ffff7b44a97 <g_intern_string+39>: mov    %rbp,%rsi
       0x7ffff7b44a9a <g_intern_string+42>: 
        callq  0x7ffff7b21710 <g_hash_table_lookup>
       0x7ffff7b44a9f <g_intern_string+47>: test   %eax,%eax
       0x7ffff7b44aa1 <g_intern_string+49>: 
        je     0x7ffff7b44ad0 <g_intern_string+96>
       0x7ffff7b44aa3 <g_intern_string+51>: mov    %eax,%eax
       0x7ffff7b44aa5 <g_intern_string+53>: lea    0x0(,%rax,8),%rbx
       0x7ffff7b44aad <g_intern_string+61>: 
        mov    0xc51ec(%rip),%rax        # 0x7ffff7c09ca0
       0x7ffff7b44ab4 <g_intern_string+68>: 
        lea    0xc51f5(%rip),%rdi        # 0x7ffff7c09cb0
       0x7ffff7b44abb <g_intern_string+75>: mov    (%rax,%rbx,1),%r12
       0x7ffff7b44abf <g_intern_string+79>: callq  0x7ffff7b81500 <g_mutex_unlock>
       0x7ffff7b44ac4 <g_intern_string+84>: pop    %rbx
       0x7ffff7b44ac5 <g_intern_string+85>: pop    %rbp
       0x7ffff7b44ac6 <g_intern_string+86>: mov    %r12,%rax
       0x7ffff7b44ac9 <g_intern_string+89>: pop    %r12
       0x7ffff7b44acb <g_intern_string+91>: retq   
       0x7ffff7b44acc <g_intern_string+92>: nopl   0x0(%rax)
       0x7ffff7b44ad0 <g_intern_string+96>: mov    %rbp,%rdi
       0x7ffff7b44ad3 <g_intern_string+99>: callq  0x7ffff7b446e0
       0x7ffff7b44ad8 <g_intern_string+104>:    
        mov    0xc51ba(%rip),%edi        # 0x7ffff7c09c98
       0x7ffff7b44ade <g_intern_string+110>:    mov    %rax,%rbp
       0x7ffff7b44ae1 <g_intern_string+113>:    mov    %edi,%edx
       0x7ffff7b44ae3 <g_intern_string+115>:    test   $0x7ff,%edi
       0x7ffff7b44ae9 <g_intern_string+121>:    
        jne    0x7ffff7b44b2f <g_intern_string+191>
       0x7ffff7b44aeb <g_intern_string+123>:    add    $0x800,%edi
       0x7ffff7b44af1 <g_intern_string+129>:    mov    $0x8,%esi
       0x7ffff7b44af6 <g_intern_string+134>:    xor    %r12d,%r12d
       0x7ffff7b44af9 <g_intern_string+137>:    movslq %edi,%rdi
       0x7ffff7b44afc <g_intern_string+140>:    
        callq  0x7ffff7b3a020 <g_malloc_n>
       0x7ffff7b44b01 <g_intern_string+145>:    
        movslq 0xc5190(%rip),%rdi        # 0x7ffff7c09c98
       0x7ffff7b44b08 <g_intern_string+152>:    mov    %rax,%rbx
       0x7ffff7b44b0b <g_intern_string+155>:    test   %edi,%edi
       0x7ffff7b44b0d <g_intern_string+157>:    
        jne    0x7ffff7b44b68 <g_intern_string+248>
       0x7ffff7b44b0f <g_intern_string+159>:    mov    $0x4000,%edx
       0x7ffff7b44b14 <g_intern_string+164>:    lea    (%rbx,%r12,1),%rdi
       0x7ffff7b44b18 <g_intern_string+168>:    xor    %esi,%esi
       0x7ffff7b44b1a <g_intern_string+170>:    
        callq  0x7ffff7aff2a0 <memset@plt>
       0x7ffff7b44b1f <g_intern_string+175>:    
        mov    %rbx,0xc517a(%rip)        # 0x7ffff7c09ca0
    

    It just happens that libgstreamer uses that function enough that the buckets array requires growth, triggering the leak.

    A shallow look at the glib source, for example on github, suggests that this code has changed. The best way to actually avoid the leak would be to try to use a newer version of libglib.

    The specific value that is leaked in your test is actually the initial buckets array, which is allocated when libglib is initialized:

    #0  __GI___libc_malloc (bytes=16384) at malloc.c:3023
    #1  0x00007ffff7b39e99 in g_malloc () at /lib/x86_64-linux-gnu/libglib-2.0.so.0
    #2  0x00007ffff7b447d4 in  () at /lib/x86_64-linux-gnu/libglib-2.0.so.0
    #3  0x00007ffff7fe0b9a in call_init
        (l=<optimized out>, argc=argc@entry=1, argv=argv@entry=0x7fffffffe0d8, env=env@entry=0x7fffffffe0e8) at dl-init.c:72
    #4  0x00007ffff7fe0ca1 in call_init
        (env=0x7fffffffe0e8, argv=0x7fffffffe0d8, argc=1, l=<optimized out>)
        at dl-init.c:30
    #5  _dl_init
        (main_map=0x7ffff7ffe190, argc=1, argv=0x7fffffffe0d8, env=0x7fffffffe0e8)
        at dl-init.c:119
    #6  0x00007ffff7fd013a in _dl_start_user () at /lib64/ld-linux-x86-64.so.2
    
    (gdb) x/2i 0x00007ffff7b447d4
       0x7ffff7b447d4:  movl   $0x1,0xc54ba(%rip)        # 0x7ffff7c09c98
       0x7ffff7b447de:  mov    %rax,0xc54bb(%rip)        # 0x7ffff7c09ca0
    

    So to suppress such errors in a sanitizer, as opposed to avoiding the leak, you'd either have to tell it to ignore any allocations with g_malloc as part of the stack (which may leave you subject to missing some more significant leaks) or to tell it to ignore at least that specific allocation made when glib is initialized and possibly any stacks involving g_intern_string+140, depending on whether your program eventually causes g_intern_string to be called sufficiently many times that the buckets array grows more than once.