objective-ccore-datansvaluensvaluetransformernssecurecoding

NSSecureCoding crash in Core Data


I am getting crash reports from users when fetching from my Core Data entity with a fetch request. This is what the crash looks like:

SIGABRT: Unhandled error (NSCocoaErrorDomain, 4864) occurred during faulting and was thrown: Error Domain=NSCocoaErrorDomain Code=4864 "The data couldn’t be read because it isn’t in the correct format." UserInfo={NSUnderlyingError=0x2822582d0 {Error Domain=NSCocoaErrorDomain Code=4864 "value for key 'NS.objects' was of unexpected class 'NSTextAlternatives (0x1ffa90890) [/System/Library/PrivateFrameworks/UIFoundation.framework]'. Allowed classes are '{( "NSTextAttachment (0x1ff3bc730) [/System/Library/PrivateFrameworks/UIFoundation.framework]", "NSNumber (0x1ff35d8c8) [/System/Library/Frameworks/Foundation.framework]", "NSDictionary (0x1ff352418) [/System/Library/Frameworks/CoreFoundation.framework]", "NSFont (0x1ff3bc3e8) [/System/Library/PrivateFrameworks/UIFoundation.framework]", "NSGlyphInfo (0x1ffa906d8) [/System/Library/PrivateFrameworks/UIFoundation.framework]", "NSArray (0x1ff352238) [/System/Library/Frameworks/CoreFoundation.framework]", "NSParagraphStyle (0x1ff3bbec0) [/System/Library/PrivateFrameworks/UIFoundation.framework]", "NSUUID (0x1ff35f290) [/System/Library/Frameworks/Foundation.framework]", "NSDate (0x1ff3522b0) [/System/Library/Frameworks/CoreFoundation.framework]", "NSColor (0x1ff3a67a8) [/System/Library/PrivateFrameworks/UIKitCore.framework]", "NSNull (0x1ff3527b0) [/System/Library/Frameworks/CoreFoundation.framework]", "NSData (0x1ff3519c8) [/System/Library/Frameworks/CoreFoundation.framework]", "UIColor (0x1ff3a6a78) [/System/Library/PrivateFrameworks/UIKitCore.framework]", "NSAttributedString (0x1ff359c00) [/System/Library/Frameworks/Foundation.framework]", "NSURL (0x1ff352e18) [/System/Library/Frameworks/CoreFoundation.framework]", "NSSet (0x1ff352878) [/System/Library/Frameworks/CoreFoundation.framework]", "NSValue (0x1ff35d940) [/System/Library/Frameworks/Foundation.framework]", "UIFont (0x1ff3bc780) [/System/Library/PrivateFrameworks/UIFoundation.framework]", "NSString (0x1ff35d170) [/System/Library/Frameworks/Foundation.framework]" )}'." UserInfo={NSDebugDescription=value for key 'NS.objects' was of unexpected class 'NSTextAlternatives (0x1ffa90890) [/System/Library/PrivateFrameworks/UIFoundation.framework]'. Allowed classes are '{( "NSTextAttachment (0x1ff3bc730) [/System/Library/PrivateFrameworks/UIFoundation.framework]", "NSNumber (0x1ff35d8c8) [/System/Library/Frameworks/Foundation.framework]", "NSDictionary (0x1ff352418) [/System/Library/Frameworks/CoreFoundation.framework]", "NSFont (0x1ff3bc3e8) [/System/Library/PrivateFrameworks/UIFoundation.framework]", "NSGlyphInfo (0x1ffa906d8) [/System/Library/PrivateFrameworks/UIFoundation.framework]", "NSArray (0x1ff352238) [/System/Library/Frameworks/CoreFoundation.framework]", "NSParagraphStyle (0x1ff3bbec0) [/System/Library/PrivateFrameworks/UIFoundation.framework]", "NSUUID (0x1ff35f290) [/System/Library/Frameworks/Foundation.framework]", "NSDate (0x1ff3522b0) [/System/Library/Frameworks/CoreFoundation.framework]", "NSColor (0x1ff3a67a8) [/System/Library/PrivateFrameworks/UIKitCore.framework]", "NSNull (0x1ff3527b0) [/System/Library/Frameworks/CoreFoundation.framework]", "NSData (0x1ff3519c8) [/System/Library/Frameworks/CoreFoundation.framework]", "UIColor (0x1ff3a6a78) [/System/Library/PrivateFrameworks/UIKitCore.framework]", "NSAttributedString (0x1ff359c00) [/System/Library/Frameworks/Foundation.framework]", "NSURL (0x1ff352e18) [/System/Library/Frameworks/CoreFoundation.framework]", "NSSet (0x1ff352878) [/System/Library/Frameworks/CoreFoundation.framework]", "NSValue (0x1ff35d940) [/System/Library/Frameworks/Foundation.framework]", "UIFont (0x1ff3bc780) [/System/Library/PrivateFrameworks/UIFoundation.framework]", "NSString (0x1ff35d170) [/System/Library/Frameworks/Foundation.framework]" )}'.}}}CrashVersion 5.6.8 (5.6.9)1 user1 report

The line it crashes at is when NSFetchedResultsController is calling performFetch:

7 CoreData 0x00000001b48c1a94 __43-[NSFetchedResultsController performFetch:]_block_invoke + 572 8 CoreData 0x00000001b487c01c developerSubmittedBlockToNSManagedObjectContextPerform + 152 9
CoreData 0x00000001b474eda4 -[NSManagedObjectContext performBlockAndWait:] + 204 10 CoreData 0x00000001b475cae4 -[NSFetchedResultsController _recursivePerformBlockAndWait:withContext:] + 144 11 CoreData 0x00000001b475cc1c -[NSFetchedResultsController performFetch:] + 220

This is almost certainly happening because I recently changed a Transformable attribute which didn't have any transformer set (so essentially using NSKeyedUnarchiveFromDataTransformerName) to using a subclass of NSSecureUnarchiveFromDataTransformer. The attribute is storing an NSAttributedString that comes from a UITextView in an iOS app. And I also save the NSRange for the 'bold' and 'italics' items as an NSValue.

I added the following transformer:

@objc(AttributedStringDictionaryTransformer)
final class AttributedStringDictionaryTransformer: NSSecureUnarchiveFromDataTransformer {

    override static var allowedTopLevelClasses: [AnyClass] {
        return super.allowedTopLevelClasses + [NSValue.self]
    }
}

This 'transformer' is used in the Transformable attributed in the Core Data model. It adds the NSValue class to the allowed list of classes.

When it says "value for key 'NS.objects' was of unexpected class 'NSTextAlternatives " ... I'm not sure what NSTextAlternatives is. I have never (explicitly) used this in my code. Any ideas where it might be coming from?

EDIT: On further investigation, it turns out NSAttributedString can tack on all sorts of objects, like objects for NSTextAlternatives and _UITextInputDictationResultMetadata, which might be present if the user uses dictation on the UITextView. So the NSSecureUnarchiveFromDataTransformer, which technically supports NSAttributedString, doesn't actually support it properly. These classes are coming from private frameworks, so not sure how I can add them to my custom transformer. Not sure there is a work-around other than filing a bug. If there is, please let me know.


Solution

  • Indeed, NSAttributedString can be a bit of a grab-bag of text-system-related attributes: various operations in text views can cause the text system (CoreText, UIKit, etc.) to annotate ranges with bits of metadata to improve the iOS experience, but it does mean that any given NSAttributedString can end up with bits of baggage you might not be aware of. (Conceptually, NSAttributedString is like an NSString with an associated NSDictionary which can contain anything, if you squint just right.) Regular usage of text views can cause this, and when you factor in pasting text from other attributed string sources, too, you can end up with many attribute values you don't expect. This is the text system working as expected.

    Unfortunately, this design butts heads with NSSecureCoding, which requires you to list out the types that you expect to decode from an archive before decoding it. You're correctly whitelisting NSAttributedString and NSValue objects, and NSAttributedString itself whitelists several types that it is aware of (and NSSecureUnarchiveFromDataTransformer also allows some known Foundation types), but the issue is that the string ultimately contains types that neither you nor NSAttributedString know about, and without whitelisting those, decoding will be prevented.

    Since these types are indeed private, you're going to have a rough time trying to find all possible private types the text system can throw at you. The easiest way forward would be on the encoding side: before you encode an NSAttributedString, iterate over all of its attribute ranges and strip out any attributes whose types don't meet your whitelisting criteria. Using the same criteria at encode time that you use at decode time will allow you to decode successfully.