Memory leak in latest SQLCipher library


#1

I am experiencing a memory leak issue when running the app on an arm64 device (tested on iPhone 6 and iPad mini 2) when running from Xcode in debug mode. I am using the latest Xcode 7.0.1 and both devices have latest iOS 9.0.2.

It is worth mentioning that :

  • this memory leak only happens when running the app from Xcode in debug mode. When running the app directly on the device, there is no memory leak.
  • the memory leak only appears on arm64 devices (I have tested on iPad 2 and iPad 3 and there was no memory leak)
  • the memory leak does not happen when running SqlCipher without encryption (when not setting the encryption key)
  • the memory leak does not happen when using a previously compiled version of the SQLCipher library (which I had compiled from the open source repo about 2 years ago)
  • I have double checked and zombie pointers are not enabled

Here are the relevant bits of code from my very simple test app, which just repeatedly inserts a thousand records before waiting one second (BTW, I know that it would be much more efficient to include this inserts inside a transaction, but this is not the point)

AppDelegate.m

- (NSURL *)databaseURL {
    NSURL *directoryURL = [[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask][0];
    return [directoryURL URLByAppendingPathComponent:@"secure.db"];
}

//Repeatedly inserts 1000 rows in the table and waits for 1 second
- (void) run {
    dispatch_async(dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL), ^{
        sqlite3 *db;
        
        if (sqlite3_open([[self.databaseURL path] UTF8String], &db) != SQLITE_OK) {
            return NSLog(@"Could not open database");
        }
        
        const char* key = [@"StrongPassword" UTF8String];
        sqlite3_key(db, key, (int)strlen(key));
        if (sqlite3_exec(db, (const char*) "SELECT count(*) FROM sqlite_master;", NULL, NULL, NULL) != SQLITE_OK) {
            return NSLog(@"Incorrect password!");
        }
        
        if (sqlite3_exec(db, (const char*) "CREATE TABLE IF NOT EXISTS cards (cardID INTEGER PRIMARY KEY NOT NULL);", NULL, NULL, NULL) != SQLITE_OK) {
            return NSLog(@"Could not create table");
        }
        
        for (long i = 0; i < 1000; i++) {
            NSLog(@"%ld", i);
            NSString* sql = [NSString stringWithFormat:@"INSERT OR REPLACE INTO cards (cardID) VALUES (%ld);", i];
            if (sqlite3_exec(db, sql.UTF8String, NULL, NULL, NULL) != SQLITE_OK) {
                return NSLog(@"Could not insert card");
            }
        }
        
        sqlite3_close(db);
        
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [self run];
        });
    });
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [self run];
    return YES;
}

ViewController.m

- (double) memoryUsage {
    struct task_basic_info info;
    mach_msg_type_number_t size = sizeof(info);
    kern_return_t kerr = task_info(mach_task_self(), TASK_BASIC_INFO, (task_info_t)&info, &size);
    if( kerr == KERN_SUCCESS ) {
        return (double)info.resident_size/1024/1024;
    } else {
        return 0;
    }
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    label = [[UILabel alloc] initWithFrame:CGRectMake(20, 100, self.view.bounds.size.width - 40, 40)];
    label.autoresizingMask = UIViewAutoresizingFlexibleWidth;
    label.textAlignment = NSTextAlignmentCenter;
    [self.view addSubview:label];
    
    timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerDidFire) userInfo:nil repeats:YES];
}

- (void) timerDidFire {
    label.text = [NSString stringWithFormat:@"%.1f MB", [self memoryUsage]];
}

#2

Hello @quentinadam,

Thanks for reporting this issue. We were able to independently reproduce identical behavior to what you described here.

After much investigation, we have narrowed down the source of this problem to SecRandomCopyBytes, which is used in the CommonCrypto provider to provide randomness for the database salt, and per-page initialization vectors.

You can observe the “leaky” behavior even in a standalone application that does not call SQLCipher, i.e.

  dispatch_async(dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL), ^{
       uint8_t buffer[8];
       for (long i = 0; i < 1000; i++) {
           SecRandomCopyBytes(kSecRandomDefault, 8, buffer);
       }
       dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
           [self run];
       });
   });

We have not yet determined exactly why this is happening yet, but it is definitely related to Security.framework. As you’ve noted, it only happens when running on arm64 devices under the debugger.


#3

I had the same problem on development iOS devices with arm64 processor. I was not able to debug my app because it generates huge database during first launch and crashes with memory shortage warning.
I have fixed that by tweaking sqlcipher/src/crypto_cc.c file.

I have rewritten sqlcipher_cc_random function this way:

static int sqlcipher_cc_random (void *ctx, void *buffer, int length) {
    arc4random_buf(buffer, length);
    return SQLITE_OK;    
}

Now sqlcipher works great without memory leaks.


#4

After testing this under the latest OS, I’m no longer seeing unconstrained growth. Memory usage does grow to a point, but then levels off. Are you seeing the same thing?