Incorrect behaviour of task_info() syscall after an unrelated dlclose() call

For some reason, after invoking an unrelated dlclose() call to unload any .dylib that had previously been loaded via dlopen(..., RTLD_NOW), the subsequent call to task_info(mach_task_self(), TASK_DYLD_INFO, ...) syscall returns unexpected structure in dyld_uuid_info image_infos->uuidArray, that, while it seems to represent an array of struct dyld_uuid_info elements, there is only 1 such element (dyld_all_image_infos *infos->uuidArrayCount == 1) and the app crashes when trying to access dyld_uuid_info image->imageLoadAddress->magic, as image->imageLoadAddress doesn't seem to represent a valid struct mach_header structure address (although it looks like a normal pointer within the process address space. What does it point to?).

This reproduces on macOS 15.4.1 (24E263)

Could you please confirm that this is a bug in the specified OS build, or point to incorrect usage of the task_info() API?

Attaching the C++ file that reproduces the issue to this email message It needs to be built on macOS 15.4.1 (24E263) via Xcode or just a command line clang++ compiler. It may crash or return garbage, depending on memory layout, but on this macOS build it doesn’t return a correct feedfacf magic number for the struct mach_header structure.

Thank you

Feedback Assistant reference: FB18431345

//On `macOS 15.4.1 (24E263)` create a C++ application (for example, in Xcode), with the following contents. Note, that this application should crash on this macOS build. It will not crash, however, if you either:
//1. Comment out `dlclose()` call
//2. Change the order of the `performDlOpenDlClose()` and `performTaskInfoSyscall()` functions calls (first performTaskInfoSyscall() then performDlOpenDlClose()).



#include <iostream>
#include <dlfcn.h>
#include <mach/mach.h>
#include <mach-o/dyld_images.h>
#include <mach-o/loader.h>

void performDlOpenDlClose() {
    printf("dlopen/dlclose function\n");
    printf("Note: please adjust the path below to any real dylib on your system, if the path below doesn't exist!\n");
    std::string path = "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/libswiftDemangle.dylib";

	printf("Dylib to open: %s\n", path.c_str());
	void* handle = ::dlopen(path.c_str(), RTLD_NOW);
	if(handle) {
	  ::dlclose(handle);
	} else {
	  printf("Error: %s\n", dlerror());
	}
}

void performTaskInfoSyscall() {
    printf("Making a task_info() syscall\n");
    printf("\033[34mSource File: %s\033[0m\n", __FILE__);
    
    task_t task = mach_task_self();
    struct task_dyld_info dyld_info;
    mach_msg_type_number_t size = TASK_DYLD_INFO_COUNT;
    
    kern_return_t kr = task_info(task, TASK_DYLD_INFO, (task_info_t)&dyld_info, &size);
    if (kr != KERN_SUCCESS) {
        fprintf(stderr, "task_info failed: %s\n", mach_error_string(kr));
    }
    
    
    const struct dyld_all_image_infos* infos =
        (const struct dyld_all_image_infos*)dyld_info.all_image_info_addr;

    printf("version: %d, infos->infoArrayCount: %d\n", infos->version, infos->infoArrayCount);
    for(uint32_t i=0; i<infos->infoArrayCount; i++) {
        dyld_image_info image = infos->infoArray[i];
        const struct mach_header* header = image.imageLoadAddress;
        printf("%d ", i);
        printf("%p ", (void*)image.imageLoadAddress);
        printf("(%x) ", header->magic);
        printf("%s\n", image.imageFilePath);
        fflush(stdout);
    }
    
    printf("\n\n");
    
    printf("infos->uuidArrayCount: %lu\n", infos->uuidArrayCount);
    for(uint32_t i=0; i<infos->uuidArrayCount; i++) {
        dyld_uuid_info image = infos->uuidArray[i];
        const struct mach_header* header = image.imageLoadAddress;
        printf("%d ", i);
        printf("%p ", (void*)image.imageLoadAddress);
        printf("(%x)\n", header->magic);
        fflush(stdout);
    }
    printf("task_info() syscall result processing is completed\n\n");
}

int main(int argc, const char * argv[]) {    
    performDlOpenDlClose();
    performTaskInfoSyscall();

    return 0;
}
Answered by DTS Engineer in 846726022
Feedback Assistant reference: FB18431345

That’s the best path forward for this.

Honestly, a bug like this isn’t super surprising. dlclose is not a well-trodden path on Apple platforms because the vast majority of libraries can’t be successfully unloaded [1].

Share and Enjoy

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

[1] Neither the Swift nor the Objective-C runtimes allow you to unload code. Even with C++ that’s tricky because of the ODR.

Feedback Assistant reference: FB18431345

That’s the best path forward for this.

Honestly, a bug like this isn’t super surprising. dlclose is not a well-trodden path on Apple platforms because the vast majority of libraries can’t be successfully unloaded [1].

Share and Enjoy

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

[1] Neither the Swift nor the Objective-C runtimes allow you to unload code. Even with C++ that’s tricky because of the ODR.

Thanks Quinn,

Currently the issue doesn't seem to reproduce on latest macOS (15.5), it seems to be specific to 15.4.1.

However, even though it seems to be fixed in newer builds, from what I understand, for future-proofing it is generally not recommended to use dlclose() on libraries like libswiftDemangle.dylib even though I had manually dlopen()'ed previously, and keep such libraries loaded until the application exits?

Accepted Answer

However, even though it seems to be fixed in newer builds, from what I understand, for future-proofing it is generally not recommended to use dlclose()

Correct. As Quinn noted, the ObjC and Swift runtimes don't actually support unloading, so unless you wrote the library and/or understand its full implementation, the behavior is basically undefined. Lots of libraries will unload without crashing, but in most cases that is an implementation "accident", not any specific design choice.

on libraries like libswiftDemangle.dylib even though I had manually dlopen()'ed previously,

How it was opened doesn't matter. At a high level, the problem is that loading these libraries modifies your app’s internal state but unloading doesn't undo those changes.

That leads to here:

and keep such libraries loaded until the application exits?

Depends on what you're actually doing. I don't know what you're actually trying to do, but the main reason apps want to load/unload libraries dynamically is that whatever they're doing is an uncommon operation and they don't want to be wasting resources on a library they're not actually using.

Unfortunately, that also means that leaving it loaded isn't ideal- partly because of the resource issue but particularly because it opens the door to new interesting bugs (it only fails when x and y have been loaded and then you...). If that's the situation here, then I'd probably use something like a helper tool or XPCService to shift the work out of your process, avoiding all these issues.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Thanks a lot Kevin and Quinn,

Greatly appreciate your detailed responses!

Incorrect behaviour of task_info() syscall after an unrelated dlclose() call
 
 
Q