memory leak in dlopen / dlcose, or user error?

Calling dlopen then dlclose causes an increase in the amount of memory used by the program. If I create a loop that calls dlopen / dlclose repeatedly on the same dynamic library, memory usage increases continuously. Is this a bug, or am I using dlopen / dlclose incorrectly?

I can reproduce this by modifying the sample code in the Apple Developer docs Creating Dynamic Libraries. If I modify Runtime.c, changing the line void *lib_handle = dlopen(lib_name, RTLD_NOW); to add the infinite loop, as below:

void *lib_handle = dlopen(lib_name, RTLD_NOW);
for (int ii = 0; ; ++ii) {
    printf("loop %i\n", ii);
    int close_err = dlclose(lib_handle);
    printf("close error: %i\n", close_err);
    printf("dlopen(%s, RTLD_NOW)\n", lib_name);
    lib_handle = dlopen(lib_name, RTLD_NOW);
}

then opening and closing the dynamic library will succeed, but memory usage (as reported by top) will rapidly increase.

I'm running on x86_64 macOS 13.6.6. Full code for the modified Runtime.c is attached, the rest of the code is available in the Apple Developer docs.

Any suggestions?

Many thanks, Chris

Answered by DTS Engineer in 866955022

I tried this here in my office and didn’t see any leaks or unbounded memory growth. Here’s what I did:

  1. Using Xcode 26.1 on macOS 15.7.1 [1], I created a new project from the macOS > Command Line Tool template.

  2. I then added a dynamic library target to that.

  3. I populated them with the code shown at the end of this post.

  4. I build both targets using Xcode.

  5. I then ran the tool from Terminal:

    % ./Test806035
    will run
    will open and close, iteration: 0
    did close and close
    …
    will open and close, iteration: 999
    did close and close
    did run, press return to loop, pid: 47371
    
  6. In other Terminal window, I rans leaks against the process. It showed no leaks.

  7. I ran heap against the process:

    % heap Test806035
       …
       COUNT      BYTES       AVG   CLASS_NAME                                        TYPE    BINARY
       =====      =====       ===   ==========                                        ====    ======
         134       4288      32.0   Class.data (class_rw_t)                           C       libobjc.A.dylib
          27      11360     420.7   non-object                                                
           5        512     102.4   xpc_string_t (Storage)                            C       libxpc.dylib
           5        256      51.2   xpc_dictionary_t (Storage)                        C       libxpc.dylib
           5        240      48.0   xpc_string_t                                      ObjC    libxpc.dylib
           4        128      32.0   Class.methodCache._buckets (bucket_t)             C       libobjc.A.dylib
           2        192      96.0   xpc_dictionary_t                                  ObjC    libxpc.dylib
           1        128     128.0   dispatch_queue_t (serial)                         ObjC    libdispatch.dylib
           1         64      64.0   xpc_array_t (Storage)                             C       libxpc.dylib
           1         64      64.0   xpc_pipe_t                                        ObjC    libxpc.dylib
           1         48      48.0   Class.data.extended (class_rw_ext_t)              C       libobjc.A.dylib
           1         48      48.0   xpc_array_t                                       ObjC    libxpc.dylib
    
  8. Back in the tool window, I repeatedly pressed return and waited for it to complete.

  9. I repeated step 7.

  10. I compared the heap output from step 7 and 9. It was identical.

I recommend that you repeat this test to confirm my result. If you see anything different, let me know. Otherwise, you can start comparing my code to your code to see why your code shows unbounded memory growth when mine doesn’t.

If I had to guess, I’d say that this is most likely caused by the library you’re loading. Does it have any library initialiser functions? A common cause of those is C++ global constructors? If so, is it possible that those are leaking? Or that they’re not be destructed correctly?

You can use the -Wglobal-constructor option to check for C++ global constructors.

https://clang.llvm.org/docs/DiagnosticsReference.html#wglobal-constructors

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] You need to update (-: Seriously though, the chances of this being different between 15.6.1 and 15.7.1 are very low.


#include <stdio.h>
#include <dlfcn.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main(int argc, char **argv) {
    #pragma unused(argc)
    #pragma unused(argv)
    printf("will run\n");

    while (true) {
        for (int ii = 0; ii < 1000; ++ii) {
            printf("will open and close, iteration: %d\n", ii);

            void * lib_handle = dlopen("./libLeakTest.dylib", RTLD_NOW);
            if (lib_handle == NULL) {
                printf("did not open, error: %s\n", dlerror());
                abort();
            }
            int err = dlclose(lib_handle);
            if (err != 0) {
                printf("close failed, error: %s\n", strerror(err));
                abort();
            }
            
            printf("did close and close\n");
        }
        printf("did run, press return to re-run, pid: %d\n", (int) getpid());
        char junk[16];
        fgets(junk, sizeof(junk), stdin);
    }
    return 0;
}

#include <stdio.h>

extern void LTDummyRun(void);

extern void LTDummyRun(void) {
    printf("LTDummyRun");
}
I'm running on x86_64 macOS 13.6.6

Are you able to reproduce this on a more modern version of macOS?

There’s a couple of reasons why this matters:

  • The dynamic linker has received significant updates in the last few years, so it’s possible that this is fixed already.
  • If that’s the case, there’s no point filing a bug against macOS 13 because there’s no release vehicle for such a fix.

Notwithstanding the above, be aware that dlclose has a significant limitation on macOS: In many cases its unable to actually unload the library. This limitation is fundamental to the system, rather than a bug. The dlclose man page lists some of the causes, but those are just the start.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Thanks for the response! We've now tried this on macOS Sequoia Version 15.6.1 (24G90) (arm64) and still see the increasing memory usage.

I appreciate that dlclose might not unload the library, but I wouldn't expect it to then consume more resources on a reload.

I tried this here in my office and didn’t see any leaks or unbounded memory growth. Here’s what I did:

  1. Using Xcode 26.1 on macOS 15.7.1 [1], I created a new project from the macOS > Command Line Tool template.

  2. I then added a dynamic library target to that.

  3. I populated them with the code shown at the end of this post.

  4. I build both targets using Xcode.

  5. I then ran the tool from Terminal:

    % ./Test806035
    will run
    will open and close, iteration: 0
    did close and close
    …
    will open and close, iteration: 999
    did close and close
    did run, press return to loop, pid: 47371
    
  6. In other Terminal window, I rans leaks against the process. It showed no leaks.

  7. I ran heap against the process:

    % heap Test806035
       …
       COUNT      BYTES       AVG   CLASS_NAME                                        TYPE    BINARY
       =====      =====       ===   ==========                                        ====    ======
         134       4288      32.0   Class.data (class_rw_t)                           C       libobjc.A.dylib
          27      11360     420.7   non-object                                                
           5        512     102.4   xpc_string_t (Storage)                            C       libxpc.dylib
           5        256      51.2   xpc_dictionary_t (Storage)                        C       libxpc.dylib
           5        240      48.0   xpc_string_t                                      ObjC    libxpc.dylib
           4        128      32.0   Class.methodCache._buckets (bucket_t)             C       libobjc.A.dylib
           2        192      96.0   xpc_dictionary_t                                  ObjC    libxpc.dylib
           1        128     128.0   dispatch_queue_t (serial)                         ObjC    libdispatch.dylib
           1         64      64.0   xpc_array_t (Storage)                             C       libxpc.dylib
           1         64      64.0   xpc_pipe_t                                        ObjC    libxpc.dylib
           1         48      48.0   Class.data.extended (class_rw_ext_t)              C       libobjc.A.dylib
           1         48      48.0   xpc_array_t                                       ObjC    libxpc.dylib
    
  8. Back in the tool window, I repeatedly pressed return and waited for it to complete.

  9. I repeated step 7.

  10. I compared the heap output from step 7 and 9. It was identical.

I recommend that you repeat this test to confirm my result. If you see anything different, let me know. Otherwise, you can start comparing my code to your code to see why your code shows unbounded memory growth when mine doesn’t.

If I had to guess, I’d say that this is most likely caused by the library you’re loading. Does it have any library initialiser functions? A common cause of those is C++ global constructors? If so, is it possible that those are leaking? Or that they’re not be destructed correctly?

You can use the -Wglobal-constructor option to check for C++ global constructors.

https://clang.llvm.org/docs/DiagnosticsReference.html#wglobal-constructors

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] You need to update (-: Seriously though, the chances of this being different between 15.6.1 and 15.7.1 are very low.


#include <stdio.h>
#include <dlfcn.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main(int argc, char **argv) {
    #pragma unused(argc)
    #pragma unused(argv)
    printf("will run\n");

    while (true) {
        for (int ii = 0; ii < 1000; ++ii) {
            printf("will open and close, iteration: %d\n", ii);

            void * lib_handle = dlopen("./libLeakTest.dylib", RTLD_NOW);
            if (lib_handle == NULL) {
                printf("did not open, error: %s\n", dlerror());
                abort();
            }
            int err = dlclose(lib_handle);
            if (err != 0) {
                printf("close failed, error: %s\n", strerror(err));
                abort();
            }
            
            printf("did close and close\n");
        }
        printf("did run, press return to re-run, pid: %d\n", (int) getpid());
        char junk[16];
        fgets(junk, sizeof(junk), stdin);
    }
    return 0;
}

#include <stdio.h>

extern void LTDummyRun(void);

extern void LTDummyRun(void) {
    printf("LTDummyRun");
}
memory leak in dlopen / dlcose, or user error?
 
 
Q