robotspacer
robotspacer

Reputation: 2762

How can I prevent this NSPropertyListSerialization crash?

I have an app for Mac and iOS that uses .plist files to sync data between devices using iCloud. Every once in a while I get an email from someone that's getting repeated crashes on a specific device. (Other devices are fine.) In every case the app crashed while trying to read in a file from iCloud, using NSPropertyListSerialization to convert the data to a dictionary. In every case the problem is solved by having the user delete the iCloud data for the app. The same files get synced back in, and everything works fine again.

The specific example I'll use here is from the Mac version, but I've had almost identical crashes on iOS. From the crash report:

Exception Type:  EXC_BAD_ACCESS (SIGBUS)
Exception Codes: 0x000000000000000a, 0x0000000101ee2000

VM Regions Near 0x101ee2000:
    VM_ALLOCATE            0000000101ee1000-0000000101ee2000 [    4K] rw-/rwx SM=PRV  
--> mapped file            0000000101ee2000-0000000101ee3000 [    4K] r--/rwx SM=COW  /Users/USER/Library/Mobile Documents/PB4R74AA4J~com~junecloud~Notefile/*/*.notefile
    shared memory          0000000101ee3000-0000000101ee4000 [    4K] rw-/rw- SM=SHM  

And then:

Thread 7 Crashed:
0   libsystem_c.dylib               0x0000000105157013 bcmp + 19
1   com.apple.CoreFoundation        0x0000000101bbf4b0 __CFBinaryPlistGetTopLevelInfo + 80
2   com.apple.CoreFoundation        0x0000000101bbf36d __CFTryParseBinaryPlist + 93
3   com.apple.CoreFoundation        0x0000000101bbede2 _CFPropertyListCreateWithData + 146
4   com.apple.CoreFoundation        0x0000000101bcbdb0 CFPropertyListCreateWithData + 112
5   com.apple.Foundation            0x0000000101568b89 +[NSPropertyListSerialization propertyListWithData:options:format:error:] + 94
6   com.junecloud.Notefile-Helper   0x000000010107ad06 -[JUNNoteDocument readFromData:ofType:error:] + 64
7   com.apple.AppKit                0x000000010268d507 -[NSDocument readFromURL:ofType:error:] + 546
8   com.apple.AppKit                0x000000010228e3c8 -[NSDocument _initWithContentsOfURL:ofType:error:] + 135
9   com.apple.AppKit                0x000000010228e074 -[NSDocument initWithContentsOfURL:ofType:error:] + 262
10  com.junecloud.Notefile-Helper   0x000000010107cad8 -[JUNSyncDocument initWithFileURL:] + 213
11  com.junecloud.Notefile-Helper   0x0000000101079ec7 -[NotefileAppDelegate documentForURL:] + 68
12  com.junecloud.Notefile-Helper   0x00000001010825cb -[JUNSyncManager documentForURL:] + 76
13  com.junecloud.Notefile-Helper   0x000000010107d43c -[JUNSyncInOperation main] + 1340
14  com.apple.Foundation            0x0000000101563cd2 __NSThread__main__ + 1345
15  libsystem_c.dylib               0x00000001051697a2 _pthread_start + 327
16  libsystem_c.dylib               0x00000001051561e1 thread_start + 13

Thread 7 crashed with X86 Thread State (64-bit):
  rax: 0x0000000000000062  rbx: 0x000000000000007b  rcx: 0x000000010c2be868  rdx: 0x0000000000000007
  rdi: 0x0000000101d1e151  rsi: 0x0000000101ee2000  rbp: 0x000000010c2be810  rsp: 0x000000010c2be7b8
   r8: 0x000000010c2be870   r9: 0x0000000000000000  r10: 0x00007ff841c28900  r11: 0x00007ff841c186d8
  r12: 0x000000010c2be870  r13: 0x0000000101ee2000  r14: 0x000000010c2beb00  r15: 0x000000010c2be980
  rip: 0x0000000105157013  rfl: 0x0000000000010202  cr2: 0x0000000101ee2000
Logical CPU: 2

Here's the relevant bit of code where it's crashing:

- (BOOL)readFromData:(NSData *)data ofType:(NSString *)typeName error:(NSError **)outError {
    BOOL success = NO;
    NSDictionary *dictionary = nil;

    NSError *error = nil;
    id plist = [NSPropertyListSerialization propertyListWithData:data
        options:NSPropertyListImmutable format:NULL error:&error];
    if (plist && [plist isKindOfClass:[NSDictionary class]]) {
        dictionary = plist;
    } else {
        NSLog(@"Error opening document: %@ %@",error,[error userInfo]);
        if (outError != NULL) *outError = error;
    }

    // Then it does some things with the dictionary if it's not nil

    return success;
}

Am I correct in thinking that NSPropertyListSerialization is just choking on some corrupt data, or is the problem more likely in my own code? If the problem is with NSPropertyListSerialization, is there anything I can do to prevent the app from crashing, and deal with the problem more appropriately?

If the problem is likely to be in my own code, what could I do to track down the cause? This would be much easier if I could duplicate the problem myself, but I've never seen this crash on my own devices, and obviously I can't expect a user to give me access to their iCloud account.

Update As requested, here's a bit of JUNSyncDocument. On iOS this is a UIDocument subclass, on Mac it's an NSDocument subclass.

- (id)initWithFileURL:(NSURL *)url {
    #if TARGET_OS_IPHONE

    self = [super initWithFileURL:url];
    return self;

    #else

    NSError *error = nil;
    if ([[NSFileManager defaultManager] fileExistsAtPath:[url path]]) {
        if ((self = [super initWithContentsOfURL:url ofType:[self documentType] error:&error])) {
            self.fileURL = url;
            self.hasUndoManager = NO;
        } else NSLog(@"Error initializing existing document: %@ %@",url,error); 
    } else {
        if ((self = [super initWithType:[self documentType] error:&error])) {
            self.fileURL = url;
            self.hasUndoManager = NO;
        } else NSLog(@"Error initializing new document: %@",error);
    }
    return self;

    #endif
}

This seems messy, and I wouldn't be surprised if I'm doing something stupid here. It works fine in most cases though. That gets called from NotefileAppDelegate:

- (JUNSyncDocument *)documentForURL:(NSURL *)url {
    JUNNoteDocument *document = [[JUNNoteDocument alloc] initWithFileURL:url];
    return document;
}

Which in turn is called by JUNSyncManager:

- (JUNSyncDocument *)documentForURL:(NSURL *)url {
    return [self.delegate documentForURL:url];
}

Which is called by JUNSyncInOperation:

JUNSyncDocument *document = [self.delegate documentForURL:self.url];

I just realized I can get rid of that ridiculous delegate chain by using NSClassFromString, but I don't know if that will affect the problem or not.

Upvotes: 3

Views: 1092

Answers (2)

rsebbe
rsebbe

Reputation: 334

I have the exact same problem, (UIDocument & NSDocument subclasses, crash when accessing the plist data), although I'm using NSFileWrapper instead.

The mapped file path looks suspicious in your report (unless it's been anonymized by the OS), doesn't appear to be an actual file, but the query string. Isn't it that the NSMetadataQuery is returning incorrect results?

I have no solution so far, still searching.

update

I could debug this problem with a customer, generating custom builds with logging information. Here are my observations:

  • the crashes happen with different APIs, like NSPropertyListSerialization, CGImageSourceCreateWithURL, and possibly many other.
  • the accessed file is stored in the iCloud container

It has the following status:

fileExists = YES;
isUbiquitous = YES;
hasNonZeroSize = YES;
isDownloaded = NO;

So it appears as a regular file to most APIs, yet it is not available. When accessed, it makes the app crash. - the accessed file can be within a file package that has the isDownloaded = YES, although the accessed file itself (within the file package) has isDownloaded = NO.

Work around: check the isDownloaded property for the file prior to accessing its contents. This property is checked using:

-[NSURL getResourceValue:&val forKey:NSURLUbiquitousItemIsDownloadedKey error:&error]

If like me you're using NSFileWrapper to read the UIDocument contents, then you need to have this check in:

-[UIDocument readFromURL:(NSURL *)url error:(NSError *__autoreleasing *)outError]

because the NSFileWrapper won't give you access to the file package NSURL that's needed.

I suspect a problem with the atomicity of iCloud when the document is created. It looks like the file package and its contents are not in sync for their downloaded status, just like if the document directory is made first, and its contents copied to it as 2 distinct operations.

Upvotes: 1

Ken Aspeslagh
Ken Aspeslagh

Reputation: 11594

Looking at the source of __CFTryParseBinaryPlist and __CFBinaryPlistGetTopLevelInfo, which are open source.

It looks like the memcmp (bcmp) that is crashing is at the very beginning where it checks first few bytes of the data for the binary plist header. It wouldn't get that far if CFDataGetLength was <= 8 bytes, so it's not a buffer underflow. It's possible CFDataGetBytePtr returned nil, but I don't see how that could happen if the length was > 8. Most likely, the data pointer has gone invalid.

I could tell more you posted the register contents from the crash report. Also, post the code of how it's creating the data (-[JUNSyncDocument initWithFileURL:] and -[NotefileAppDelegate documentForURL:].)

Upvotes: 3

Related Questions