kiwi-bdd / Kiwi

Simple BDD for iOS
BSD 3-Clause "New" or "Revised" License
4.14k stars 512 forks source link

fix: avoid crash when clear all stubed object #722

Open cchicken opened 4 years ago

cchicken commented 4 years ago

there is a crash when some stubed object has been release during one testcase

crash stack:

1. KWInterceptedObjectKey()
2. KWObjectClassRestored()
3. KWClearAllObjectStubs()
...

reason

  1. when stubedObject has been release, key() at line 3 in KWClearAllObjectStubs() will return nil

    void KWClearAllObjectStubs(void) {
    for (KWInterceptedObjectBlock key in KWObjectStubs) {
        id stubbedObject = key();
        if (KWObjectClassRestored(stubbedObject)) {
            continue;
        }
        KWRestoreOriginalClass(stubbedObject);
    }
    [KWObjectStubs removeAllObjects];
    }
  2. KWObjectClassRestored() invoke KWInterceptedObjectKey() with nil

    BOOL KWObjectClassRestored(id anObject) {
    return [KWRestoredObjects containsObject:KWInterceptedObjectKey(anObject)];
    }
  3. objc_setAssociatedObject() will crash when anObject is nil, and key is not nil

    KWInterceptedObjectBlock KWInterceptedObjectKey(id anObject) {
    KWInterceptedObjectBlock key = objc_getAssociatedObject(anObject, kKWInterceptedObjectKey);
    if (key == nil) {
        __weak id weakobj = anObject;
        key = ^{ return weakobj; };
        objc_setAssociatedObject(anObject, kKWInterceptedObjectKey, [key copy], OBJC_ASSOCIATION_COPY);
    }
    return key;
    }

according to https://opensource.apple.com/source/objc4/objc4-781/runtime/objc-references.mm

void
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{
    // This code used to work when nil was passed for object and key. Some code
    // probably relies on that to not crash. Check and handle it explicitly.
    // rdar://problem/44094390
    if (!object && !value) return;

    if (object->getIsa()->forbidsAssociatedObjects())
        _objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object));
    ...

I guess if (!object && !value) return; should be if (!object) return;

solution

check anObject is non nil before invoke objc_setAssociatedObject()