Fix reentrancy bugs in Subscribers.Assign

This commit is contained in:
Sergej Jaskiewicz
2021-07-08 15:35:34 +03:00
committed by Sergej Jaskiewicz
parent adcee8c14d
commit 925bee4af9
3 changed files with 54 additions and 2 deletions
@@ -112,8 +112,17 @@ extension Subscribers {
lock.assertOwner()
#endif
status = .terminal
object = nil
lock.unlock()
// We MUST release the object AFTER unlocking the lock,
// since releasing it may trigger execution of arbitrary code,
// for example, if the object has a deinit.
// When the object deallocates, its deinit is called, and holding
// the lock at that moment can lead to deadlocks.
withExtendedLifetime(object) {
object = nil
lock.unlock()
}
}
}
}
@@ -35,3 +35,10 @@ final class AutomaticallyFinish<Output, Failure: Error> {
receiveValue: receiveValue)
}
}
extension AutomaticallyFinish where Failure == Never {
func assign<Root>(to keyPath: ReferenceWritableKeyPath<Root, Output>,
on object: Root) -> AnyCancellable {
return publisher.assign(to: keyPath, on: object)
}
}
@@ -137,4 +137,40 @@ final class AssignTests: XCTestCase {
publisher.send(100)
XCTAssertEqual(object.value, 42)
}
func testReceiveCompletionWhileCancelling() {
let cancellable: AnyCancellable
do {
let object = ObjectToModify()
cancellable = object.autofinish.assign(to: \.value, on: object)
}
// autofinish is deallocated here, a completion is sent to the sink
cancellable.cancel()
}
func testReceiveCompletionWhileCompleting() {
let cancellable: AnyCancellable
let finish: () -> Void
do {
let object = ObjectToModify()
cancellable = object.autofinish.assign(to: \.value, on: object)
let underlyingPublisher = object.autofinish.publisher
finish = { underlyingPublisher.send(completion: .finished) }
}
finish() // autofinish is deallocated here, a completion is sent to the sink
cancellable.cancel()
}
}
final class ObjectToModify {
let autofinish = AutomaticallyFinish<Int, Never>()
var value = 0
}