Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
Objective-C bundle unload causing crash due to invalid protocol reference
Build and run using:
clang main.m -DHOST -framework Foundation -o host && \
clang main.m -DPLUGIN_A -bundle -framework Foundation -o plugin_a.dylib && \
clang main.m -DPLUGIN_B -bundle -framework Foundation -o plugin_b.dylib && \
This will crash with the following backtrace:
* thread #1, queue = '', stop reason = EXC_BAD_ACCESS (code=2, address=0x1000dd6c8)
* frame #0: 0x00007fff686deefc libobjc.A.dylib`protocol_t::demangledName() + 76
frame #1: 0x00007fff686d421d libobjc.A.dylib`_read_images + 2065
frame #2: 0x00007fff686d2a9e libobjc.A.dylib`map_images_nolock + 1146
frame #3: 0x00007fff686e54e1 libobjc.A.dylib`map_images + 43
frame #4: 0x0000000100007c65 dyld`dyld::notifyBatchPartial(dyld_image_states, bool, char const* (*)(dyld_image_states, unsigned int, dyld_image_info const*), bool, bool) + 1071
frame #5: 0x0000000100013bb4 dyld`ImageLoader::link(ImageLoader::LinkContext const&, bool, bool, bool, ImageLoader::RPathChain const&, char const*) + 260
frame #6: 0x0000000100007f6a dyld`dyld::link(ImageLoader*, bool, bool, ImageLoader::RPathChain const&, unsigned int) + 161
frame #7: 0x0000000100010fa0 dyld`dlopen + 429
frame #8: 0x00007fff692d3e86 libdyld.dylib`dlopen + 86
frame #9: 0x0000000100000f5f host`main + 63
frame #10: 0x00007fff692d2115 libdyld.dylib`start + 1
frame #11: 0x00007fff692d2115 libdyld.dylib`start + 1
Reproducible on macOS 10.13 and 10.12.
OBJC_PRINT_PROTOCOL_SETUP=YES is the trigger of this bug, but can be reproduced
without it too, eg by calling protocol_copyMethodDescriptionList(@protocol(...))
#ifdef HOST
#include <dlfcn.h>
int main(int argc, char *argv[])
for (int i = 1; i < argc; ++i)
dlclose(dlopen(argv[i], RTLD_LOCAL));
return 0;
#import <Foundation/Foundation.h>
@interface MyProtocolSubclass : NSObject<NSURLDownloadDelegate> @end
@implementation MyProtocolSubclass @end
#ifdef PLUGIN_B
// This must only run in plugin B, otherwise the runtime will cache the
// protocol while loading plugin A, where it's still valid.
#import <objc/runtime.h>
__attribute((constructor)) void init_plugin_b()
[MyProtocolSubclass class];
#import <Foundation/Foundation.h>
#include <objc/runtime.h>
#include <mach-o/dyld.h>
#include <mach-o/getsect.h>
#include <dlfcn.h>
#ifdef __LP64__
typedef mach_header_64 mach_header_t;
typedef mach_header mach_header_t;
@interface DanglingProtocolDetector : NSObject @end
@implementation DanglingProtocolDetector
+ (void)load
Dl_info imageInfo;
if (!dladdr((void*)self, &imageInfo) || !imageInfo.dli_fbase) {
fprintf(stderr, "Failed to resolve Mach-O header\n");
const mach_header_t *header = reinterpret_cast<const mach_header_t *>(imageInfo.dli_fbase);
unsigned long bytes = 0;
Protocol **protocols = reinterpret_cast<Protocol**>(getsectiondata(header, SEG_DATA, "__objc_protolist", &bytes));
size_t numProtocols = bytes / sizeof(Protocol*);
for (sizt_t i = 0; i < numProtocols; ++i) {
// Map from the protocol definition in the image, to the protocol known
// to the Objective-C runtime, via the name, to see if they are the same.
Protocol *imageProtocol = protocols[i];
const char *protocolName = protocol_getName(imageProtocol);
Protocol *runtimeProtocol = objc_getProtocol(protocolName);
if (runtimeProtocol == imageProtocol)
continue; // All good
Dl_info protocolInfo;
// Check if the runtime reference has been invalidated by unloading the image. We use
// the fact that dladdr will resolve the dangling protocol to the current image if it
// has been invalidated, which we know is not the case due to the check above.
if (!dladdr(runtimeProtocol, &protocolInfo) || protocolInfo.dli_fbase == imageInfo.dli_fbase) {
fprintf(stderr, "Protocol %s was initialized in image at address %p, " \
"but has since been unmapped!\n", protocolName, runtimeProtocol);
fprintf(stderr, "This will likely result in a crash. Run with " \
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment