Created
April 10, 2026 15:59
-
-
Save bdash/fefbbd7a695ee559785a2cd0c90a5d31 to your computer and use it in GitHub Desktop.
Demonstrates the bug within FPRObjectSwizzler that shows it deallocating a class while it is still in use, causing a crash when weak references to an instance are accessed
This file contains hidden or 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
| // clang -framework Foundation -fobjc-arc -O2 -g -o objc-weak-reference-test objc-weak-reference-test.m | |
| // | |
| // Attempts to reproduce a crash in objc_loadWeakRetained caused by | |
| // objc_disposeClassPair being called while the instance is still | |
| // mid-destruction (weak references not yet cleared). | |
| // | |
| // The bug: an associated object holds a __weak back-reference to the | |
| // object it's attached to. During dealloc, the associated object's | |
| // -dealloc loads the weak ref, gets nil (because the object is | |
| // deallocating and rootTryRetain fails), concludes the object is gone, | |
| // and disposes the dynamically-created class pair. But the object is | |
| // still in objc_destructInstance — its isa now points to a freed class. | |
| // A concurrent thread loading a weak reference to the same object will | |
| // read the isa and crash. | |
| #import <Foundation/Foundation.h> | |
| #import <objc/runtime.h> | |
| #import <pthread.h> | |
| #import <stdatomic.h> | |
| static const void *kSwizzlerKey = &kSwizzlerKey; | |
| static atomic_bool g_stop = false; | |
| // --------------------------------------------------------------------------- | |
| // Swizzler — mimics FPRObjectSwizzler | |
| // Attached as an associated object. Holds a weak ref back to the target. | |
| // In -dealloc, if the weak ref is nil, disposes the generated class. | |
| // --------------------------------------------------------------------------- | |
| @interface Swizzler : NSObject { | |
| __weak id _swizzledObject; | |
| Class _generatedClass; | |
| } | |
| - (instancetype)initWithObject:(id)obj generatedClass:(Class)cls; | |
| @end | |
| @implementation Swizzler | |
| - (instancetype)initWithObject:(id)obj generatedClass:(Class)cls { | |
| self = [super init]; | |
| if (self) { | |
| _swizzledObject = obj; | |
| _generatedClass = cls; | |
| } | |
| return self; | |
| } | |
| - (void)dealloc { | |
| id obj = _swizzledObject; | |
| if (!obj) { | |
| // The swizzled object is "gone" (actually mid-dealloc). | |
| // Dispose the class pair — this is the bug. | |
| if (_generatedClass) { | |
| objc_disposeClassPair(_generatedClass); | |
| } | |
| } | |
| } | |
| @end | |
| // --------------------------------------------------------------------------- | |
| // Weak reference hammering thread | |
| // --------------------------------------------------------------------------- | |
| static void *hammerWeakRef(void *ctx) { | |
| __weak id *weakPtr = (__weak id *)ctx; | |
| while (!atomic_load(&g_stop)) { | |
| // This is what NSConcreteMapTable does during -grow / reclaim. | |
| // If the class pair has been disposed but weak refs aren't cleared | |
| // yet, objc_loadWeakRetained will follow a dangling isa. | |
| @autoreleasepool { | |
| id obj = *weakPtr; | |
| (void)obj; | |
| } | |
| } | |
| return NULL; | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Main loop — repeatedly create swizzled objects and tear them down | |
| // --------------------------------------------------------------------------- | |
| int main(int argc, const char *argv[]) { | |
| @autoreleasepool { | |
| fprintf(stderr, "Testing... (will crash on success)\n"); | |
| static int classCounter = 0; | |
| for (int i = 0; i < 10000; i++) { | |
| @autoreleasepool { | |
| // 1. Create a dynamic subclass | |
| char name[64]; | |
| snprintf(name, sizeof(name), "DynClass_%d", ++classCounter); | |
| Class baseClass = [NSObject class]; | |
| Class dynClass = objc_allocateClassPair(baseClass, name, 0); | |
| if (!dynClass) continue; | |
| objc_registerClassPair(dynClass); | |
| // 2. Create an instance of the dynamic class | |
| id instance = [[dynClass alloc] init]; | |
| // 3. Set up the weak reference that the hammer thread will load | |
| __weak id weakInstance = instance; | |
| // 4. Attach a Swizzler as an associated object (like Firebase does) | |
| Swizzler *swizzler = [[Swizzler alloc] initWithObject:instance | |
| generatedClass:dynClass]; | |
| objc_setAssociatedObject(instance, kSwizzlerKey, swizzler, | |
| OBJC_ASSOCIATION_RETAIN_NONATOMIC); | |
| swizzler = nil; | |
| // 5. Start the thread | |
| pthread_t thread; | |
| pthread_create(&thread, NULL, hammerWeakRef, &weakInstance); | |
| // 6. Give the thread a moment to start | |
| usleep(100); | |
| // 7. Release the instance — triggers dealloc → associated object | |
| // removal → Swizzler dealloc → disposeClassPair while | |
| // the thread is repeatedly loading the weak ref. | |
| instance = nil; | |
| // 8. Clean up | |
| atomic_store(&g_stop, true); | |
| pthread_join(thread, NULL); | |
| atomic_store(&g_stop, false); | |
| } | |
| } | |
| fprintf(stderr, "Finished without crashing (couldn't reproduce)\n"); | |
| } | |
| return 0; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment