Prevent Obj-C dealloc accessing Kotlin counterpart
#KT-41811 Fixed. Also add GC assertion for add ref for reclaimed object.
This commit is contained in:
committed by
SvyatoslavScherbina
parent
e5e1648462
commit
9c7a002eb0
@@ -0,0 +1,22 @@
|
|||||||
|
#import <Foundation/NSObject.h>
|
||||||
|
|
||||||
|
extern BOOL deallocRetainReleaseDeallocated;
|
||||||
|
|
||||||
|
@interface DeallocRetainRelease : NSObject
|
||||||
|
@end;
|
||||||
|
|
||||||
|
@protocol WeakReference
|
||||||
|
@required
|
||||||
|
@property (weak) id referent;
|
||||||
|
@end;
|
||||||
|
|
||||||
|
@interface ObjCWeakReference : NSObject <WeakReference>
|
||||||
|
@property (weak) id referent;
|
||||||
|
@end;
|
||||||
|
|
||||||
|
extern id <WeakReference> weakDeallocLoadWeak;
|
||||||
|
extern BOOL deallocLoadWeakDeallocated;
|
||||||
|
|
||||||
|
@interface DeallocLoadWeak : NSObject
|
||||||
|
-(void)checkWeak;
|
||||||
|
@end;
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import kotlin.native.ref.WeakReference
|
||||||
|
import kotlinx.cinterop.*
|
||||||
|
import kotlin.test.*
|
||||||
|
import objcTests.*
|
||||||
|
|
||||||
|
// Note: these tests rely on GC assertions: without the fix and the assertions it won't actually crash.
|
||||||
|
// GC should fire an assertion if it obtains a reference to Kotlin object that is being (or has been) deallocated.
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testKT41811() {
|
||||||
|
// Attempt to make the state predictable:
|
||||||
|
kotlin.native.internal.GC.collect()
|
||||||
|
|
||||||
|
deallocRetainReleaseDeallocated = false
|
||||||
|
assertFalse(deallocRetainReleaseDeallocated)
|
||||||
|
|
||||||
|
createGarbageDeallocRetainRelease()
|
||||||
|
|
||||||
|
// Runs [DeallocRetainRelease dealloc]:
|
||||||
|
kotlin.native.internal.GC.collect()
|
||||||
|
|
||||||
|
assertTrue(deallocRetainReleaseDeallocated)
|
||||||
|
|
||||||
|
// Might crash due to double-dispose if the dealloc applied addRef/releaseRef to reclaimed Kotlin object:
|
||||||
|
kotlin.native.internal.GC.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createGarbageDeallocRetainRelease() {
|
||||||
|
autoreleasepool {
|
||||||
|
object : DeallocRetainRelease() {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testKT41811LoadWeak() {
|
||||||
|
testKT41811LoadWeak(ObjCWeakReference())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testKT41811LoadKotlinWeak() {
|
||||||
|
val kotlinWeak = object : NSObject(), WeakReferenceProtocol {
|
||||||
|
lateinit var weakReference: WeakReference<Any>
|
||||||
|
|
||||||
|
override fun referent(): Any? {
|
||||||
|
return weakReference.value
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setReferent(referent: Any?) {
|
||||||
|
weakReference = WeakReference(referent!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testKT41811LoadWeak(kotlinWeak)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun testKT41811LoadWeak(weakRef: WeakReferenceProtocol) {
|
||||||
|
// Attempt to make the state predictable:
|
||||||
|
kotlin.native.internal.GC.collect()
|
||||||
|
|
||||||
|
deallocLoadWeakDeallocated = false
|
||||||
|
assertFalse(deallocLoadWeakDeallocated)
|
||||||
|
|
||||||
|
createGarbageDeallocLoadWeak(weakRef)
|
||||||
|
|
||||||
|
// Runs [DeallocLoadWeak dealloc]:
|
||||||
|
kotlin.native.internal.GC.collect()
|
||||||
|
|
||||||
|
assertTrue(deallocLoadWeakDeallocated)
|
||||||
|
|
||||||
|
// Might crash due to double-dispose if the dealloc applied addRef/releaseRef to reclaimed Kotlin object:
|
||||||
|
kotlin.native.internal.GC.collect()
|
||||||
|
|
||||||
|
weakDeallocLoadWeak = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createGarbageDeallocLoadWeak(weakRef: WeakReferenceProtocol) {
|
||||||
|
autoreleasepool {
|
||||||
|
val obj = object : DeallocLoadWeak() {}
|
||||||
|
weakDeallocLoadWeak = weakRef.apply { referent = obj }
|
||||||
|
obj.checkWeak()
|
||||||
|
assertSame(obj, weakDeallocLoadWeak!!.referent)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
#import "kt41811.h"
|
||||||
|
#import "assert.h"
|
||||||
|
|
||||||
|
id retainObject = nil;
|
||||||
|
BOOL deallocRetainReleaseDeallocated = NO;
|
||||||
|
|
||||||
|
@implementation DeallocRetainRelease
|
||||||
|
-(void)dealloc {
|
||||||
|
retainObject = self;
|
||||||
|
assert(retainObject == self);
|
||||||
|
retainObject = nil;
|
||||||
|
|
||||||
|
assert(!deallocRetainReleaseDeallocated);
|
||||||
|
deallocRetainReleaseDeallocated = YES;
|
||||||
|
}
|
||||||
|
@end;
|
||||||
|
|
||||||
|
@implementation ObjCWeakReference
|
||||||
|
@end;
|
||||||
|
|
||||||
|
id <WeakReference> weakDeallocLoadWeak = nil;
|
||||||
|
BOOL deallocLoadWeakDeallocated = NO;
|
||||||
|
|
||||||
|
@implementation DeallocLoadWeak
|
||||||
|
-(void)checkWeak {
|
||||||
|
assert(weakDeallocLoadWeak != nil);
|
||||||
|
assert(weakDeallocLoadWeak.referent == self);
|
||||||
|
}
|
||||||
|
|
||||||
|
-(void)dealloc {
|
||||||
|
assert(weakDeallocLoadWeak != nil);
|
||||||
|
assert(weakDeallocLoadWeak.referent == nil);
|
||||||
|
|
||||||
|
assert(!deallocLoadWeakDeallocated);
|
||||||
|
deallocLoadWeakDeallocated = YES;
|
||||||
|
}
|
||||||
|
@end;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||||
|
* that can be found in the LICENSE file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package deallocretain
|
||||||
|
|
||||||
|
open class DeallocRetainBase
|
||||||
|
|
||||||
|
fun garbageCollect() = kotlin.native.internal.GC.collect()
|
||||||
|
|
||||||
|
fun createWeakReference(value: Any) = kotlin.native.ref.WeakReference(value)
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||||
|
* that can be found in the LICENSE file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Kt
|
||||||
|
|
||||||
|
// Note: these tests rely on GC assertions: without the fix and the assertions it won't actually crash.
|
||||||
|
// GC should fire an assertion if it obtains a reference to Kotlin object that is being (or has been) deallocated.
|
||||||
|
|
||||||
|
private func test1() throws {
|
||||||
|
// Attempt to make the state predictable:
|
||||||
|
DeallocRetainKt.garbageCollect()
|
||||||
|
|
||||||
|
DeallocRetain.deallocated = false
|
||||||
|
try assertFalse(DeallocRetain.deallocated)
|
||||||
|
|
||||||
|
try autoreleasepool {
|
||||||
|
let obj = DeallocRetain()
|
||||||
|
try obj.checkWeak()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runs DeallocRetain.deinit:
|
||||||
|
DeallocRetainKt.garbageCollect()
|
||||||
|
|
||||||
|
try assertTrue(DeallocRetain.deallocated)
|
||||||
|
|
||||||
|
// Might crash due to double-dispose if the dealloc applied addRef/releaseRef to reclaimed Kotlin object:
|
||||||
|
DeallocRetainKt.garbageCollect()
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DeallocRetain : DeallocRetainBase {
|
||||||
|
static var deallocated = false
|
||||||
|
static var retainObject: DeallocRetain? = nil
|
||||||
|
static weak var weakObject: DeallocRetain? = nil
|
||||||
|
static var kotlinWeakRef: KotlinWeakReference<AnyObject>? = nil
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
super.init()
|
||||||
|
DeallocRetain.weakObject = self
|
||||||
|
DeallocRetain.kotlinWeakRef = DeallocRetainKt.createWeakReference(value: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkWeak() throws {
|
||||||
|
try assertSame(actual: DeallocRetain.weakObject, expected: self)
|
||||||
|
try assertSame(actual: DeallocRetain.kotlinWeakRef!.value, expected: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
DeallocRetain.retainObject = self
|
||||||
|
DeallocRetain.retainObject = nil
|
||||||
|
|
||||||
|
try! assertNil(DeallocRetain.weakObject)
|
||||||
|
try! assertNil(DeallocRetain.kotlinWeakRef!.value)
|
||||||
|
|
||||||
|
try! assertFalse(DeallocRetain.deallocated)
|
||||||
|
DeallocRetain.deallocated = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DeallocRetainTests : SimpleTestProvider {
|
||||||
|
override init() {
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
test("Test1", test1)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -318,6 +318,19 @@ __attribute__((swift_name("CoroutinesKt")))
|
|||||||
+ (void)invoke1Block:(id<KtKotlinSuspendFunction1>)block argument:(id _Nullable)argument completionHandler:(void (^)(id _Nullable, NSError * _Nullable))completionHandler __attribute__((swift_name("invoke1(block:argument:completionHandler:)")));
|
+ (void)invoke1Block:(id<KtKotlinSuspendFunction1>)block argument:(id _Nullable)argument completionHandler:(void (^)(id _Nullable, NSError * _Nullable))completionHandler __attribute__((swift_name("invoke1(block:argument:completionHandler:)")));
|
||||||
@end;
|
@end;
|
||||||
|
|
||||||
|
__attribute__((swift_name("DeallocRetainBase")))
|
||||||
|
@interface KtDeallocRetainBase : KtBase
|
||||||
|
- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer));
|
||||||
|
+ (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead")));
|
||||||
|
@end;
|
||||||
|
|
||||||
|
__attribute__((objc_subclassing_restricted))
|
||||||
|
__attribute__((swift_name("DeallocRetainKt")))
|
||||||
|
@interface KtDeallocRetainKt : KtBase
|
||||||
|
+ (void)garbageCollect __attribute__((swift_name("garbageCollect()")));
|
||||||
|
+ (KtKotlinWeakReference<id> *)createWeakReferenceValue:(id)value __attribute__((swift_name("createWeakReference(value:)")));
|
||||||
|
@end;
|
||||||
|
|
||||||
__attribute__((swift_name("FHolder")))
|
__attribute__((swift_name("FHolder")))
|
||||||
@interface KtFHolder : KtBase
|
@interface KtFHolder : KtBase
|
||||||
- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer));
|
- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer));
|
||||||
|
|||||||
@@ -1578,10 +1578,12 @@ inline void addHeapRef(ContainerHeader* container) {
|
|||||||
case CONTAINER_TAG_STACK:
|
case CONTAINER_TAG_STACK:
|
||||||
break;
|
break;
|
||||||
case CONTAINER_TAG_LOCAL:
|
case CONTAINER_TAG_LOCAL:
|
||||||
|
RuntimeAssert(container->refCount() > 0, "add ref for reclaimed object");
|
||||||
incrementRC</* Atomic = */ false>(container);
|
incrementRC</* Atomic = */ false>(container);
|
||||||
break;
|
break;
|
||||||
/* case CONTAINER_TAG_FROZEN: case CONTAINER_TAG_SHARED: */
|
/* case CONTAINER_TAG_FROZEN: case CONTAINER_TAG_SHARED: */
|
||||||
default:
|
default:
|
||||||
|
RuntimeAssert(container->refCount() > 0, "add ref for reclaimed object");
|
||||||
incrementRC</* Atomic = */ true>(container);
|
incrementRC</* Atomic = */ true>(container);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,6 +127,8 @@ void BackRefFromAssociatedObject::addRef() {
|
|||||||
static_assert(errorPolicy != ErrorPolicy::kDefaultValue, "Cannot use default return value here");
|
static_assert(errorPolicy != ErrorPolicy::kDefaultValue, "Cannot use default return value here");
|
||||||
|
|
||||||
if (atomicAdd(&refCount, 1) == 1) {
|
if (atomicAdd(&refCount, 1) == 1) {
|
||||||
|
if (obj_ == nullptr) return; // E.g. after [detach].
|
||||||
|
|
||||||
// There are no references to the associated object itself, so Kotlin object is being passed from Kotlin,
|
// There are no references to the associated object itself, so Kotlin object is being passed from Kotlin,
|
||||||
// and it is owned therefore.
|
// and it is owned therefore.
|
||||||
ensureRefAccessible<errorPolicy>(obj_, context_); // TODO: consider removing explicit verification.
|
ensureRefAccessible<errorPolicy>(obj_, context_); // TODO: consider removing explicit verification.
|
||||||
@@ -144,6 +146,8 @@ template <ErrorPolicy errorPolicy>
|
|||||||
bool BackRefFromAssociatedObject::tryAddRef() {
|
bool BackRefFromAssociatedObject::tryAddRef() {
|
||||||
static_assert(errorPolicy != ErrorPolicy::kDefaultValue, "Cannot use default return value here");
|
static_assert(errorPolicy != ErrorPolicy::kDefaultValue, "Cannot use default return value here");
|
||||||
|
|
||||||
|
if (obj_ == nullptr) return false; // E.g. after [detach].
|
||||||
|
|
||||||
// Suboptimal but simple:
|
// Suboptimal but simple:
|
||||||
ensureRefAccessible<errorPolicy>(obj_, context_);
|
ensureRefAccessible<errorPolicy>(obj_, context_);
|
||||||
|
|
||||||
@@ -165,6 +169,8 @@ template bool BackRefFromAssociatedObject::tryAddRef<ErrorPolicy::kTerminate>();
|
|||||||
void BackRefFromAssociatedObject::releaseRef() {
|
void BackRefFromAssociatedObject::releaseRef() {
|
||||||
ForeignRefContext context = context_;
|
ForeignRefContext context = context_;
|
||||||
if (atomicAdd(&refCount, -1) == 0) {
|
if (atomicAdd(&refCount, -1) == 0) {
|
||||||
|
if (obj_ == nullptr) return; // E.g. after [detach].
|
||||||
|
|
||||||
// Note: by this moment "subsequent" addRef may have already happened and patched context_.
|
// Note: by this moment "subsequent" addRef may have already happened and patched context_.
|
||||||
// So use the value loaded before refCount update:
|
// So use the value loaded before refCount update:
|
||||||
DeinitForeignRef(obj_, context);
|
DeinitForeignRef(obj_, context);
|
||||||
@@ -173,8 +179,15 @@ void BackRefFromAssociatedObject::releaseRef() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void BackRefFromAssociatedObject::detach() {
|
||||||
|
RuntimeAssert(atomicGet(&refCount) == 0, "unexpected refCount")
|
||||||
|
obj_ = nullptr; // Handled in addRef/tryAddRef/releaseRef/ref.
|
||||||
|
}
|
||||||
|
|
||||||
template <ErrorPolicy errorPolicy>
|
template <ErrorPolicy errorPolicy>
|
||||||
ObjHeader* BackRefFromAssociatedObject::ref() const {
|
ObjHeader* BackRefFromAssociatedObject::ref() const {
|
||||||
|
RuntimeAssert(obj_ != nullptr, "no valid Kotlin object found");
|
||||||
|
|
||||||
if (!ensureRefAccessible<errorPolicy>(obj_, context_)) {
|
if (!ensureRefAccessible<errorPolicy>(obj_, context_)) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,12 +54,14 @@ class BackRefFromAssociatedObject {
|
|||||||
|
|
||||||
void releaseRef();
|
void releaseRef();
|
||||||
|
|
||||||
|
void detach();
|
||||||
|
|
||||||
// Error if called from the wrong worker with non-frozen obj_.
|
// Error if called from the wrong worker with non-frozen obj_.
|
||||||
template <ErrorPolicy errorPolicy>
|
template <ErrorPolicy errorPolicy>
|
||||||
ObjHeader* ref() const;
|
ObjHeader* ref() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
ObjHeader* obj_;
|
ObjHeader* obj_; // May be null before [initAndAddRef] or after [detach].
|
||||||
ForeignRefContext context_;
|
ForeignRefContext context_;
|
||||||
volatile int refCount;
|
volatile int refCount;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -118,6 +118,24 @@ void releaseImp(id self, SEL _cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void releaseAsAssociatedObjectImp(id self, SEL _cmd) {
|
void releaseAsAssociatedObjectImp(id self, SEL _cmd) {
|
||||||
|
// This function is called by the GC. It made a decision to reclaim Kotlin object, and runs
|
||||||
|
// deallocation hooks at the moment, including deallocation of the "associated object" ([self])
|
||||||
|
// using the [super release] call below.
|
||||||
|
|
||||||
|
// The deallocation involves running [self dealloc] which can contain arbitrary code.
|
||||||
|
// In particular, this code can retain and release [self]. Obj-C and Swift runtimes handle this
|
||||||
|
// gracefully (unless the object gets accessed after the deallocation of course), but Kotlin doesn't.
|
||||||
|
// For example, this happens in https://youtrack.jetbrains.com/issue/KT-41811, provoked by
|
||||||
|
// UIViewController.dealloc (which retains-releases self._view._viewDelegate == self) and UIView.dealloc.
|
||||||
|
// Generally retaining and releasing Kotlin object that is being deallocated would lead to
|
||||||
|
// use-after-dispose and double-dispose problems (with unpredictable consequences) or to an assertion failure.
|
||||||
|
// To workaround this, detach the back ref from the Kotlin object:
|
||||||
|
getBackRef(self)->detach();
|
||||||
|
// So retain/release/etc. on [self] won't affect the Kotlin object, and an attempt to get
|
||||||
|
// the reference to it (e.g. when calling Kotlin method on [self]) would crash.
|
||||||
|
// The latter is generally ok, because by the time superclass dealloc gets launched, subclass state
|
||||||
|
// should already be deinitialized, and Kotlin methods operate on the subclass.
|
||||||
|
|
||||||
// [super release]
|
// [super release]
|
||||||
Class clazz = object_getClass(self);
|
Class clazz = object_getClass(self);
|
||||||
struct objc_super s = {self, clazz};
|
struct objc_super s = {self, clazz};
|
||||||
|
|||||||
@@ -133,6 +133,22 @@ static void injectToRuntime();
|
|||||||
}
|
}
|
||||||
|
|
||||||
-(void)releaseAsAssociatedObject {
|
-(void)releaseAsAssociatedObject {
|
||||||
|
// This function is called by the GC. It made a decision to reclaim Kotlin object, and runs
|
||||||
|
// deallocation hooks at the moment, including deallocation of the "associated object" ([self])
|
||||||
|
// using the [super release] call below.
|
||||||
|
|
||||||
|
// The deallocation involves running [self dealloc] which can contain arbitrary code.
|
||||||
|
// In particular, this code can retain and release [self]. Obj-C and Swift runtimes handle this
|
||||||
|
// gracefully (unless the object gets accessed after the deallocation of course), but Kotlin doesn't.
|
||||||
|
// Generally retaining and releasing Kotlin object that is being deallocated would lead to
|
||||||
|
// use-after-dispose and double-dispose problems (with unpredictable consequences) or to an assertion failure.
|
||||||
|
// To workaround this, detach the back ref from the Kotlin object:
|
||||||
|
refHolder.detach();
|
||||||
|
// So retain/release/etc. on [self] won't affect the Kotlin object, and an attempt to get
|
||||||
|
// the reference to it (e.g. when calling Kotlin method on [self]) would crash.
|
||||||
|
// The latter is generally ok because can be triggered only by user-defined Swift/Obj-C
|
||||||
|
// subclasses of Kotlin classes.
|
||||||
|
|
||||||
[super release];
|
[super release];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user