Skip to content

Commit b3713cb

Browse files
committed
fix(bridge): Remove ObjCWrapperObject from cache after dealloc
When `dealloc` is implemented in JS we were caching a wrapper to the already deallocated instance. This doesn't do any harm until the same memory address is reused for another Objective-C instance. When this happens the new object will be returned to the JS world via the cached wrapper, which however won't increment its retention count, leading to its premature deallocation and a crash whenever it's attempted to be used afterwards.
1 parent 11edad6 commit b3713cb

3 files changed

Lines changed: 20 additions & 1 deletion

File tree

src/NativeScript/ObjC/ObjCMethodCallback.mm

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#include "Metadata.h"
1414
#include "ObjCConstructorNative.h"
1515
#include "ObjCTypes.h"
16+
#include "ObjCWrapperObject.h"
1617
#include "ReferenceTypeInstance.h"
1718
#include "TypeFactory.h"
1819

@@ -151,8 +152,8 @@ static bool checkErrorOutParameter(ExecState* execState, const WTF::Vector<Stron
151152
ExecState* execState = methodCallback->_globalExecState;
152153

153154
id target = *static_cast<id*>(argValues[0]);
154-
#ifdef DEBUG_OBJC_INVOCATION
155155
SEL selector = *static_cast<SEL*>(argValues[1]);
156+
#ifdef DEBUG_OBJC_INVOCATION
156157
bool isInstance = !class_isMetaClass(object_getClass(target));
157158
NSLog(@"< %@[%@ %@]", isInstance ? @"-" : @"+", NSStringFromClass(object_getClass(target)), NSStringFromSelector(selector));
158159
#endif
@@ -172,6 +173,18 @@ static bool checkErrorOutParameter(ExecState* execState, const WTF::Vector<Stron
172173
JSValue thisValue = toValue(execState, target);
173174
methodCallback->callFunction(thisValue, arguments, retValue);
174175

176+
if (sel_isEqual(selector, @selector(dealloc))) {
177+
// When `dealloc` is implemented in JS we were caching a wrapper to the
178+
// already deallocated instance. This doesn't do any harm until the same
179+
// memory address is reused for another Objective-C instance. When this happens
180+
// the new object will be returned to the JS world via the cached wrapper,
181+
// which however won't increment its retention count, leading to its premature
182+
// deallocation and a crash whenever it's attempted to be used afterwards.
183+
if (auto wrapperObject = jsCast<ObjCWrapperObject*>(thisValue)) {
184+
wrapperObject->removeFromCache();
185+
}
186+
}
187+
175188
if (methodCallback->_hasErrorOutParameter) {
176189
// size_t methodCallbackLength = jsDynamicCast<JSObject*>(vm, methodCallback->function())->get(execState, vm.propertyNames->length).toUInt32(execState);
177190
if (methodCallbackLength == methodCallback->parametersCount() - 1) {

src/NativeScript/ObjC/ObjCWrapperObject.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ class ObjCWrapperObject : public JSC::JSDestructibleObject {
3535

3636
void setWrappedObject(id wrappedObject);
3737

38+
void removeFromCache();
39+
3840
static WTF::String className(const JSObject* object, JSC::VM&);
3941

4042
~ObjCWrapperObject();

src/NativeScript/ObjC/ObjCWrapperObject.mm

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@
4848
return Base::putByIndex(cell, execState, propertyName, value, shouldThrow);
4949
}
5050

51+
void ObjCWrapperObject::removeFromCache() {
52+
this->_objectMap->remove(this->_wrappedObject.get());
53+
}
54+
5155
void ObjCWrapperObject::setWrappedObject(id wrappedObject) {
5256
if (this->_wrappedObject) {
5357
this->_objectMap->remove(this->_wrappedObject.get());

0 commit comments

Comments
 (0)