I've got back to working on isolated/async deinit, and after rebasing I've got new warnings in some of my unit tests, which seem to originate from this commit [Concurrency] Don't allow subclasses of nonisolated classes to add gl… · apple/swift@c9bfe8c · GitHub by @hborla.
Adding global actor isolation via subclassing admits data races because
actor-isolated types are Sendable while nonisolated classes are not (unless
otherwise annotated), so this allowed bypassing Sendable checking.
I can think of the following scenario of how checking could have been bypassed:
class A {
var x: Int = 0
}
@FirstActor class B: A {
var y: Int = 0
}
@FirstActor func f() async {
let b = B()
b.y += 1 // ok
b.x += 1 // ok, accessing A<@FirstActor>.x in the context of @FirstActor
let a: A = b // ok, casting to A<@FirstActor>
a.x += 1 // ok, accessing A<@FirstActor>.x in the context of @FirstActor
await g(b)
}
@SecondActor func g(_ b: B) {
b.y += 1 // error
b.x += 1 // ok, but should be an error - accessing A<@FirstActor>.x in the context of @SecondActor
let a: A = b // ok, but should be an error - B inherits from A<@FirstActor>, but casting to A<@SecondActor>
a.x += 1 // ok
}
Banning adding isolation in the subclasses solves the issue, but I think it is unnecessarily restrictive. I think it can be loosened.
The way I understand non-sendable classes, is that they are generic over isolation context. When type A
is used inside actor-isolated context, it behaves as if it is isolated to that actor. When type A
is used inside nonisolated async function - it behaves as if it is isolated to the current task. And that is what makes type A
non-sendable - you cannot assign A<@FirstActor>
to A<@SecondActor>
.
But when isolated subclass inherits from non-sendable superclass, it actually inherits from the specialisations of the superclass. When accessing members of A
through instance of B
, they should be treated as isolated to B
's isolation, even though A
is not isolated. And in the context of the @SecondActor func g()
type A
means A<@SecondActor>
, which is not the base class of B
. So upcasting from B
to A
should fail.
On the other hand, there seems to be still uncovered hole with upcasting to AnyObject
. For some reason the following code does not produce errors:
@FirstActor
func foo() async {
let x: AnyObject = A() // A<@FirstActor>
await bar(x)
}
@SecondActor
func bar(_ obj: AnyObject) {
let y: A = obj as! A // A<@SecondActor>
print(y)
}