Skip to content

Instantly share code, notes, and snippets.

@bdash
Created April 10, 2026 15:59
Show Gist options
  • Select an option

  • Save bdash/fefbbd7a695ee559785a2cd0c90a5d31 to your computer and use it in GitHub Desktop.

Select an option

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
// 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