dlclose will not unload library when using macOS Frameworks

Hello,


I am encountering an unsual problem when linking a library to macOS Frameworks: as soon as I'm linking to some specific frameworks (detailed below), dlclose will no longer work, meaning that the said library will not be unloaded from process memory.

This is causing problems, because the update functionality of my application will not work - calling dlopen again will load the "old" library.


The build details are:

  • macOS High Sierra (10.13.6)
  • Xcode 9.4.1
    • C++ Language Dialect: C++14 [-std=c++14]
    • C++ Standard Library: libc++ (LLVM C++ standard library with C++11 support)


Steps to reproduce the problem:

  1. Library (dylib) is linked ("Link Binary with Libraries") to: CoreFoundation and IOKit, plus the following additional frameworks - OpenDirectory, SystemConfiguration, Security, CoreServices, ApplicationServices
  2. Compiling works fine, of course, and then the library is loaded, using dlopen by another application (Unix / terminal app)
  3. After calling dlclose, the library is still reported as being loaded in the process memory


The problem will only happen when:

  • the terminal app is ran from a terminal window (it cannot be reproduced when using Xcode directly to run / debug it)
  • the library is linked to CoreFoundation, IOKit plus any one of the other frameworks (OpenDirectory, SystemConfiguration, Security, CoreServices, ApplicationServices)


Does anyone know what may be causing this problem?


On Linux based systems, the "-fno-gnu-unique" flag will not fix the problem, but at least offer the option to use the new / updated library. Is there, maybe, a similar flag for macOS?


Any help would be greatly appreciated.


Code snippet below:


// Returns the full path of the application executable file
const char* getApplicationPath()
{
    static char s_szDir[1024] = {0};
    
    if (!*s_szDir) {
        char buffer[1024];
        char *answer = getcwd(buffer, sizeof(buffer));
        strncpy(s_szDir, answer, 1024);
        s_szDir[1024-1] = '\0';
    }
    return s_szDir;
}

// prints all loaded modules in the process memory (address space)
void printLoadedModules( )
{
    std::cout<<"\ndynamic libraries loaded in the process memory:";
    int count = _dyld_image_count();
    for(int  i = 0; i < count; i++ )
    {
        const char * imageName = _dyld_get_image_name(i);
        if( !imageName )
            continue;
        
        std::string strImageName = imageName;
        
        if( strImageName.find( "libwa" ) != std::string::npos )
            std::cout<<"\n   " << strImageName;
    }
}

int main(int argc, const char * argv[])
{
    std::string lib_path = getApplicationPath();
    lib_path += "/libtest.dylib";
    
    // Step 1: Print loaded libraries in the process memory before dlopen()
    std::cout<<"\n### Before dlopen \n";
    printLoadedModules();
    
    // Step 2: Load libtest.dylib
    void * lib_handle = (void*) dlopen( lib_path.c_str() , RTLD_LOCAL );
    if( !lib_handle )
    {
        std::cout<<"\nFailed to load libtest.dylib with error \n"<<dlerror();
        exit(1);
    }
    
    // Step 3: Print loaded libraries in the process memory after dlopen()
    std::cout<<"\n\n### After dlopen \n";
    printLoadedModules();
    
    // Step 4: Unload libtest.dylib
    int close_code = dlclose( lib_handle );
    lib_handle = nullptr;
    
    // Step 5: Print loaded libraries in the process memory after dlclose()
    std::cout<<"\n\n### After dlclose (closed code: " << close_code << ")\n";
    
    printLoadedModules();
    
    std::cout<<"\n\n";
    return 0;
}


Thank you.

Post not yet marked as solved Up vote post of oskard Down vote post of oskard
2.6k views

Replies

Unloading code has always been a challenge on macOS because of the Objective-C runtime. If a chunk of code registers with the runtime, the runtime maintains pointers into that code and there’s no way to disconnect those in order to unload the code. In addition, any code that works with

CFString
literals has similar issues [1].

I’m not 100% sure what’s causing the library to be marked as not unloadable in this case, although many of the frameworks you referenced heavily depend on Objective-C and thus it’s not hard to imagine how that’s come about. It’s possible that one of the

DYLD_PRINT_***
environment variables will offer some insight into this (see the
dyld
man page ) for details.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

[1] I vaguely recall an option that prevented

CFString
literals from leaving dangling pointers like this, but I wasn’t able to track that down.

Thank you @eskimo, for the quick answer.


Are there any general accepted solutions in this case? Maybe restarting the process after the update process?

Maybe restarting the process after the update process?

That should work. Both the Objective-C and dyld state are confined to your process.

Oh, one thing to watch out for when updating code: Do not modify a Mach-O image on disk. Rather, write the updated image to a new file and then use that to replace the old file (using

rename
, or your preferred wrapper).

The kernel caches code signature information about a Mach-O image in the file’s vnode. If you rewrite the file, you confuse this horribly. A new file gets a new vnode, and thus avoids this problem.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Sorry to be a necromancer here, but @eskimo, has this behavior changed since Monterey? Reference: https://github.com/java-native-access/jna/issues/1423#issuecomment-1382220523

has this behavior changed since Monterey?

Which behaviour specifically?

As a general rule, you shouldn’t expect dlclose to unload the code because there are many different constructs that cause that not to happen. The ones that spring immediately to mind are Objective-C or Swift runtime registration and C++ ODR. It’s very hard to tell whether you’re using such constructs and so behaviour like this can change in non-obvious ways.

Share and Enjoy

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