Last active
January 10, 2018 15:20
Star
You must be signed in to star a gist
Objective-C bundle unload causing crash due to invalid protocol reference
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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 && \ | |
OBJC_PRINT_CLASS_SETUP=YES OBJC_PRINT_PROTOCOL_SETUP=YES OBJC_PRINT_IMAGES=YES DYLD_PRINT_LIBRARIES=1 ./host *.dylib | |
This will crash with the following backtrace: | |
* thread #1, queue = 'com.apple.main-thread', 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; | |
} | |
#else | |
#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]; | |
} | |
#endif | |
#endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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; | |
#else | |
typedef mach_header mach_header_t; | |
#endif | |
@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"); | |
return; | |
} | |
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 " \ | |
"OBJC_PRINT_IMAGES=YES OBJC_PRINT_PROTOCOL_SETUP=YES to debug.\n"); | |
} | |
} | |
} | |
@end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment