NSManagedObjectContext crash if autoreleased while deallocating

Originator:me
Number:rdar://10713687 Date Originated:18-Jan-2012 01:23 AM
Status:Open Resolved:
Product:Mac OS X Product Version:10.7.3 (11D42)
Classification:Crash/Hang/Data Loss Reproducible:Always
 
In the attached code, a NSManagedObject subclass overrides -willTurnIntoFault and does a retain-autorelease on its managed object context. (Both ARC and non-ARC code are included; both trigger the same issue.)

When the managed object context is released (with objects of this class registered), it enters the autorelease pool as a zombie, and is quickly sent -release, which crashes.

Things that make this a bigger deal:
1. With ARC, it's hard to control retain/autorelease behavior.
2. With nested contexts, short-lived contexts will be more common.

In this case, calling -refreshObject:mergeChanges: on every registered object (#if'd out in the attached code) appears to force correct ordering.

This general pattern -- a master object is deallocating; children do a retain-autorelease, then master object gets a zombie release -- has come up other times too. (Notably when I had a rule editor's rows bound to a view controller; the binding was established in -awakeFromNib, but calling -unbind: in the view controller's -dealloc led to this same situation.)

Comments

main-arc.m

//
//  main.m
//  AutoreleaseMOC
//
//  Created by Jonathon Mah on 2012-01-18.
//  Copyright (c) 2012 Delicious Monster Software. All rights reserved.
//

#import <Foundation/Foundation.h>
#import <Foundation/NSDebug.h>
#import <CoreData/CoreData.h>


@interface WonderEntity : NSManagedObject
@property (nonatomic, copy) NSString *name;
@end

@implementation WonderEntity
@dynamic name;

- (void)willTurnIntoFault;
{
    [super willTurnIntoFault];

    // Touching the managed object context, such that it goes into the autorelease pool here, is DOOM!
    __autoreleasing NSManagedObjectContext *context = self.managedObjectContext;
    NSLog(@"%s (%@) with context %p", __func__, self.name, context);
}
@end


int main(int argc, const char * argv[])
{
    NSZombieEnabled = YES;
    @autoreleasepool {
        // Only to avoid having to bundle a model in the binary
        NSManagedObjectModel *mom = [[NSManagedObjectModel alloc] init];
        {
            NSEntityDescription *wonderEntity = [[NSEntityDescription alloc] init];
            wonderEntity.name = @"WonderEntity";
            wonderEntity.managedObjectClassName = NSStringFromClass([WonderEntity class]);

            NSAttributeDescription *nameAttr = [[NSAttributeDescription alloc] init];
            nameAttr.name = @"name"; nameAttr.attributeType = NSStringAttributeType;
            wonderEntity.properties = [NSArray arrayWithObject:nameAttr];
            mom.entities = [NSArray arrayWithObject:wonderEntity];
        }
        NSLog(@"Made model:\n%@", mom);

        NSPersistentStoreCoordinator *coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:mom];
        if (![coordinator addPersistentStoreWithType:NSInMemoryStoreType configuration:nil URL:nil options:nil error:NULL])
            exit(EXIT_FAILURE);

        NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
        moc.persistentStoreCoordinator = coordinator;
        //moc.retainsRegisteredObjects = YES; // Makes no difference

        NSLog(@"Inserting some managed objects");
        for (NSString *name in [NSSet setWithObjects:@"John", @"Paul", @"Ringo", @"George", nil]) {
            WonderEntity *beatle = [NSEntityDescription insertNewObjectForEntityForName:@"WonderEntity" inManagedObjectContext:moc];
            beatle.name = name;
        }
        [moc save:NULL];

#if 0
        // Explicitly faulting out each object before releasing the context is a workaround
        NSLog(@"Faulting each registered object");
        for (NSManagedObject *someObject in moc.registeredObjects)
            [moc refreshObject:someObject mergeChanges:NO];
#endif

        NSLog(@"Releasing context");
        moc = nil;

        NSLog(@"Popping autorelease pool");
    }
    fprintf(stderr, "Clean exit, phew\n");
    return 0;
}

Please note: Reports posted here will not necessarily be seen by Apple. All problems should be submitted at bugreport.apple.com before they are posted here. Please only post information for Radars that you have filed yourself, and please do not include Apple confidential information in your posts. Thank you!