6

I've got a released app using Realm and there are some crash logs showing that there is sometimes a failure to create the realm with a configuration resulting in a EXC_BREAKPOINT (SIGTRAP) crash. (there's 9 crash files for a few hundred app installations so its not something which is happening frequently)

@objc class Database : NSObject
{    
    let configuration = Realm.Configuration(encryptionKey: Database.getKey() as Data)
    var transactionRealm:Realm? = nil

    override init()
    {
        let realm = try! Realm(configuration: configuration) // Crash here
        <snip>
    }

    // This getKey() method is taken from the Realm website
    class func getKey() -> NSData {
        let keychainIdentifier = "Realm.EncryptionKey.AppKey"
        let keychainIdentifierData = keychainIdentifier.data(using: String.Encoding.utf8, allowLossyConversion: false)!

        // First check in the keychain for an existing key
        var query: [NSString: AnyObject] = [
            kSecClass: kSecClassKey,
            kSecAttrApplicationTag: keychainIdentifierData as AnyObject,
            kSecAttrKeySizeInBits: 512 as AnyObject,
            kSecReturnData: true as AnyObject
        ]

        // To avoid Swift optimization bug, should use withUnsafeMutablePointer() function to retrieve the keychain item
        // See also: http://stackoverflow.com/questions/24145838/querying-ios-keychain-using-swift/27721328#27721328
        var dataTypeRef: AnyObject?
        var status = withUnsafeMutablePointer(to: &dataTypeRef) { SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0)) }
        if status == errSecSuccess {
            return dataTypeRef as! NSData
        }

        // No pre-existing key from this application, so generate a new one
        let keyData = NSMutableData(length: 64)!
        let result = SecRandomCopyBytes(kSecRandomDefault, 64, keyData.mutableBytes.bindMemory(to: UInt8.self, capacity: 64))
        assert(result == 0, "REALM - Failed to get random bytes")

        // Store the key in the keychain
        query = [
            kSecClass: kSecClassKey,
            kSecAttrApplicationTag: keychainIdentifierData as AnyObject,
            kSecAttrKeySizeInBits: 512 as AnyObject,
            kSecValueData: keyData,
            kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlock
        ]

        status = SecItemAdd(query as CFDictionary, nil)
        assert(status == errSecSuccess, "REALM - Failed to insert the new key in the keychain")

        return keyData
    }

Here's the relevant portion of the crash file:

Exception Type:  EXC_BREAKPOINT (SIGTRAP)
Exception Codes: 0x0000000000000001, 0x0000000103c0f30c
Termination Signal: Trace/BPT trap: 5
Termination Reason: Namespace SIGNAL, Code 0x5
Terminating Process: exc handler [0]
Triggered by Thread:  0

Thread 0 name:
Thread 0 Crashed:
0   libswiftCore.dylib              0x0000000103c0f30c 0x103ab4000 + 1422092
1   libswiftCore.dylib              0x0000000103c0f30c 0x103ab4000 + 1422092
2   libswiftCore.dylib              0x0000000103b13d2c 0x103ab4000 + 392492
3   libswiftCore.dylib              0x0000000103b13bf4 0x103ab4000 + 392180
4   My app                          0x000000010334ff14 _TFC14Caller_Name_ID8DatabasecfT_S0_ + 1648 (Database.swift:21)
5   My app                          0x0000000103330624 -[Model createDatabase] + 200 (Model.m:271)

Line 21 of the Database.swift file is the one indicated in the code above in the Init() method.

If the reason for the Realm crash is because getKey() fails, then why would that be failing? If thats not the cause, that what are other reasons for the failure? And is there anything the code can do (such as retry to create the Realm object) if there is a failure?

Gruntcakes
  • 37,738
  • 44
  • 184
  • 378

1 Answers1

0

The init method

I'm going to get this out of the way first: It's unlikely that this has anything to do with your crash in this instance, but when you override any init method, you should always call super's version of that init method unless there is no super class or no available superclass implementation to call. In Objective-C you assign the results of [super init] to self, however, this pattern was not adopted by swift and it is unclear, from Apple's documentation, what happens when you call super.init() from a direct NSObject subclass in Swift. I'm running out of steam at this hour, thought I did spend some time looking at the apple/swift GitHub repository Apple/Swift Github among other place and was unable to find any really satisfying information about calling NSObject init from a Swift subclass. I would definitely encourage you to continue the search if you really want to know more and not just take my word for it. Until then, it is highly unlikely that it will cause issues if you call NSObject's init() from a Swift subclass and it just might save your butt at some point too! ;)

The crash

If the reason for the Realm crash is because getKey() fails, then why would that be failing?

I'm not sure why getKey() might be failing, but it's unlikely this is the cause of your crash. I believe default values for properties are available by the time code inside init() is called in which case your app would crash before it reached that call inside init(). I will do a little more research on this tomorrow.

If thats not the cause, that what are other reasons for the failure?

I've looked at the Realm API you are using there and it's unclear why the call to Realm(configuration:) is failing although clearly that's expected to be possible since the call can throw. However, the documentation doesn't state the conditions under which it will throw.

And is there anything the code can do (such as retry to create the Realm object) if there is a failure?

Yes! Fortunately, the "try" mechanism has other variation than just "try!". Actually, in this case, the reason the app appears to be crashing is that you are using try! which in production code should only be used in situations where you know the call is extremely unlikely to ever fail such as retrieving a resource your app ships with from inside it's app package. Per Apple's documentation:

Sometimes you know a throwing function or method won’t, in fact, throw an error at runtime. On those occasions, you can write try! before the expression to disable error propagation and wrap the call in a runtime assertion that no error will be thrown. If an error actually is thrown, you’ll get a runtime error. Swift Error Handling Documentation

When it comes to impacting the user experience, I always like to err on the side of caution so I would be highly surprised to find even one occurrence of try! in any of my code (even Swift playground toys and experiments).

In order to fail gracefully and retry or take another course of action your code either needs to use try? which will convert the thrown error to an optional as in:

if let realm = try? Realm(configuration: configuration) {
    // Do something with realm and ignore the error
}

Or you can use a full try-catch mechanism like so:

let realm: Realm? = nil
do {
   realm = try Realm(configuration: configuration)
   // do something with realm
} catch let e {
   realm = nil
   // do something with the error
   Swift.print(e)
}

Or alternatively:

do {
    let realm = try Realm(configuration: configuration)
    // do something with realm
} catch let e {
    // do something with the error
    Swift.print(e)
}  


So this may not be your golden ticket to never having this Realm call fail, but I hope it provides some help towards making your code a little more robust. I apologize for any errors, it's getting late for me here. Good luck and cheers! :)

Pat Garner
  • 16
  • 4