SecItemCopyMatching still leak on osx under ARC

2019-04-02 14:44发布

问题:

i found memory leak on SecItemCopyMatching. After investigate on SF i was found solution:

__block NSString *certificateName = nil;
SecKeychainRef keychain;
SecKeychainCopyDefault(&keychain);
NSMutableDictionary *attributeQuery = [NSMutableDictionary dictionary];
[attributeQuery setObject: (id) kSecClassIdentity forKey:(__bridge_transfer id) kSecClass];
[attributeQuery setObject: (id) kCFBooleanTrue forKey:(__bridge_transfer id) kSecReturnRef];
[attributeQuery setObject: (id) kSecMatchLimitAll forKey:(__bridge_transfer id) kSecMatchLimit];
CFTypeRef attrResult = NULL;
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef) attributeQuery,(CFTypeRef *) &attrResult);<------- here is a leak according Instruments


if (status != errSecItemNotFound) {
    NSArray *attributeResult = (__bridge_transfer NSArray *)attrResult;
    [attributeResult enumerateObjectsUsingBlock:^(id identityFromArray, NSUInteger idx, BOOL *stop) {
        OSStatus status;
        SecCertificateRef cert = NULL;
        status = SecIdentityCopyCertificate((__bridge SecIdentityRef)identityFromArray, &cert);
        if (!status)
        {

or another solution:

NSMutableDictionary *attributeQuery = [NSMutableDictionary dictionary];
[attributeQuery setObject: (id) kSecClassIdentity forKey:(__bridge_transfer id) kSecClass];
[attributeQuery setObject: (id) kCFBooleanTrue forKey:(__bridge_transfer id) kSecReturnRef];
[attributeQuery setObject: (id) kSecMatchLimitAll forKey:(__bridge_transfer id) kSecMatchLimit];
CFTypeRef attrResult = NULL;
CFDictionaryRef cfquery = (__bridge_retained CFDictionaryRef)attributeQuery;
OSStatus status = SecItemCopyMatching(cfquery,(CFTypeRef *) &attrResult);<------- here is a leak according Instruments


if (status != errSecItemNotFound) {
    NSArray *attributeResult = (__bridge_transfer NSArray *)attrResult;
    [attributeResult enumerateObjectsUsingBlock:^(id identityFromArray, NSUInteger idx, BOOL *stop) {
        OSStatus status;
        SecCertificateRef cert = NULL;
        status = SecIdentityCopyCertificate((__bridge SecIdentityRef)identityFromArray, &cert);
        if (!status)
        {
            char *nameBuf = NULL;
            CFStringRef nameRef = NULL;
            OSStatus statusNew = SecCertificateInferLabel(cert, &nameRef);
            .....
CFRelease(cfquery)

But both of them still do leak for me.

Any another ideas

回答1:

  • You receive the keychain object from a CF-style function with Copy in its name. Therefore it has a +1 reference count, and you have the responsibility of explicitly releasing it when you're done using it. It is never released by your sample code, so it's leaking. The keychain object is never used in the code you posted, thus it can be eliminated entirely.
  • In the first solution, you pass attributeQuery (a local variable) with a simple __bridge cast, which is not a good a idea; ARC may release it prematurely from under you. You should use __bridge_retained (or CFBridgingRetain) to convert it into CF country with a +1 retain count (and explicitly release it later).
  • In the second solution, you use __bridge_retained, but you do not release the result, which explains the leak.
  • The return value of SecItemCopyMatching is zero if the call was successful. You should not compare only against errSecItemNotFound; there can be any number of other reasons for a failed query.

Updated code:

NSMutableDictionary *attributeQuery = [NSMutableDictionary dictionary];
[attributeQuery setObject:(id)kSecClassIdentity forKey:(__bridge id)kSecClass];
[attributeQuery setObject:(id)kCFBooleanTrue forKey:(__bridge id)kSecReturnRef];
[attributeQuery setObject:(id)kSecMatchLimitAll forKey:(__bridge id)kSecMatchLimit];
CFTypeRef attrResult = NULL;
CFDictionaryRef cfquery = (CFDictionaryRef)CFBridgingRetain(attributeQuery);
OSStatus status = SecItemCopyMatching(cfquery, &cfresult);
CFRelease(cfquery);

if (status == errSecSuccess) {
    NSArray *attributeResult = CFBridgingRelease(cfresult);
    [attributeResult enumerateObjectsUsingBlock:^(id value, NSUInteger idx, BOOL *stop) {
        OSStatus status;
        SecCertificateRef cert = NULL;
        SecIdentityRef identity = CFBridgingRetain(value);
        status = SecIdentityCopyCertificate(identity, &cert);
        CFRelease(identity);
        if (!status)
        {
           ...
           CFRelease(cert);
        }];
 }

I find the Core Foundation/Cocoa bridging casts a little hard to read, so I personally find it cleaner to skip the Cocoa level and create the query dictionary directly on the CF level like this:

CFMutableDictionaryRef cfquery = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
CFDictionarySetValue(cfquery, kSecClass, kSecClassIdentity);
CFDictionarySetValue(cfquery, kSecReturnRef, kCFBoolenTrue);
CFDictionarySetValue(cfquery, kSecMatchLimit, kSecMatchLimitAll);

CFArrayRef cfidentities = NULL;
OSStatus status = SecItemCopyMatching((CFDictionaryRef)cfquery, (CFTypeRef *)&cfidentities);
CFRelease(cfquery);

if (status == errSecSuccess) {
    NSArray *identities = CFBridgingRelease(cfidentities);
    for (id value in identities) {
        SecCertificateRef cfcertificate;
        SecIdentityRef cfidentity = (SecIdentityRef)CFBridgingRetain(value);
        status = SecIdentityCopyCertificate(cfidentity, &cfcertificate);
        if (status == errSecSuccess) {
            // ...
            CFRelease(cfcertificate);
        }
    }
 }