Skip to content

[Distributed] Enforce strict order of synthesized id and actorSystem properties; avoid wrong offset crashes #58745

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/AST/DistributedDecl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,7 @@ bool AbstractFunctionDecl::isDistributedActorSystemRemoteCall(bool isVoidReturn)
if (!invocationParam->isInOut()) {
return false;
}

// --- Check parameter: throwing: Err.Type
auto thrownTypeParam = params->get(3);
if (thrownTypeParam->getArgumentName() != C.Id_throwing) {
Expand Down
14 changes: 11 additions & 3 deletions lib/Sema/CodeSynthesisDistributedActor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,17 @@ static VarDecl *addImplicitDistributedActorIDProperty(
propDecl->getAttrs().add(
new (C) CompilerInitializedAttr(/*IsImplicit=*/true));

nominal->addMember(propDecl);
nominal->addMember(pbDecl);

// IMPORTANT: The `id` MUST be the first field of any distributed actor,
// because when we allocate remote proxy instances, we don't allocate memory
// for anything except the first two fields: id and actorSystem, so they
// MUST be those fields.
//
// Their specific order also matters, because it is enforced this way in IRGen
// and how we emit them in AST MUST match what IRGen expects or cross-module
// things could be using wrong offsets and manifest as reading trash memory on
// id or system accesses.
nominal->addMember(propDecl, /*hint=*/nullptr, /*insertAtHead=*/true);
nominal->addMember(pbDecl, /*hint=*/nullptr, /*insertAtHead=*/true);
return propDecl;
}

Expand Down
47 changes: 15 additions & 32 deletions lib/Sema/DerivedConformanceDistributedActor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -426,32 +426,6 @@ static FuncDecl *deriveDistributedActorSystem_invokeHandlerOnReturn(
/******************************* PROPERTIES ***********************************/
/******************************************************************************/

// TODO(distributed): make use of this after all, but FORCE it?
static ValueDecl *deriveDistributedActor_id(DerivedConformance &derived) {
assert(derived.Nominal->isDistributedActor());
auto &C = derived.Context;

// ```
// nonisolated
// let id: Self.ID // Self.ActorSystem.ActorID
// ```
auto propertyType = getDistributedActorIDType(derived.Nominal);

VarDecl *propDecl;
PatternBindingDecl *pbDecl;
std::tie(propDecl, pbDecl) = derived.declareDerivedProperty(
DerivedConformance::SynthesizedIntroducer::Let, C.Id_id, propertyType,
propertyType,
/*isStatic=*/false, /*isFinal=*/true);

// mark as nonisolated, allowing access to it from everywhere
propDecl->getAttrs().add(
new (C) NonisolatedAttr(/*IsImplicit=*/true));

derived.addMembersToConformanceContext({ propDecl, pbDecl });
return propDecl;
}

static ValueDecl *deriveDistributedActor_actorSystem(
DerivedConformance &derived) {
auto &C = derived.Context;
Expand All @@ -460,8 +434,7 @@ static ValueDecl *deriveDistributedActor_actorSystem(
assert(classDecl && derived.Nominal->isDistributedActor());

// ```
// nonisolated
// let actorSystem: ActorSystem
// nonisolated let actorSystem: ActorSystem
// ```
// (no need for @actorIndependent because it is an immutable let)
auto propertyType = getDistributedActorSystemType(classDecl);
Expand All @@ -477,7 +450,14 @@ static ValueDecl *deriveDistributedActor_actorSystem(
propDecl->getAttrs().add(
new (C) NonisolatedAttr(/*IsImplicit=*/true));

derived.addMembersToConformanceContext({ propDecl, pbDecl });
// IMPORTANT: `id` MUST be the first field of a distributed actor, and
// `actorSystem` MUST be the second field, because for a remote instance
// we don't allocate memory after those two fields, so their order is very
// important. The `hint` below makes sure the system is inserted right after.
auto id = derived.Nominal->getDistributedActorIDProperty();
derived.addMemberToConformanceContext(pbDecl, /*hint=*/id);
derived.addMemberToConformanceContext(propDecl, /*hint=*/id);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ensures the system is always AFTER the ID


return propDecl;
}

Expand Down Expand Up @@ -571,11 +551,14 @@ deriveDistributedActorType_SerializationRequirement(

ValueDecl *DerivedConformance::deriveDistributedActor(ValueDecl *requirement) {
if (auto var = dyn_cast<VarDecl>(requirement)) {
if (var->getName() == Context.Id_id)
return deriveDistributedActor_id(*this);

if (var->getName() == Context.Id_actorSystem)
return deriveDistributedActor_actorSystem(*this);

if (var->getName() == Context.Id_id)
llvm_unreachable("DistributedActor.id MUST be synthesized earlier, "
"because it is forced by the Identifiable conformance. "
"If we attempted to do synthesis here, the earlier phase "
"failed and something is wrong: please report a bug.");
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We had to move away from its synthesis here a while back so this was dead code;

Actually I should just delete the hangling here I guess.

}

if (auto func = dyn_cast<FuncDecl>(requirement)) {
Expand Down
16 changes: 12 additions & 4 deletions lib/Sema/DerivedConformances.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,18 @@ void DerivedConformance::addMembersToConformanceContext(
IDC->addMember(child);
}

void DerivedConformance::addMemberToConformanceContext(
Decl *member, Decl *hint) {
auto IDC = cast<IterableDeclContext>(ConformanceDecl);
IDC->addMember(member, hint, /*insertAtHead=*/false);
}

void DerivedConformance::addMemberToConformanceContext(
Decl *member, bool insertAtHead) {
auto IDC = cast<IterableDeclContext>(ConformanceDecl);
IDC->addMember(member, /*hint=*/nullptr, insertAtHead);
}

Type DerivedConformance::getProtocolType() const {
return Protocol->getDeclaredInterfaceType();
}
Expand Down Expand Up @@ -325,10 +337,6 @@ ValueDecl *DerivedConformance::getDerivableRequirement(NominalTypeDecl *nominal,
if (name.isSimpleName(ctx.Id_unownedExecutor))
return getRequirement(KnownProtocolKind::Actor);

// DistributedActor.id
if(name.isSimpleName(ctx.Id_id))
return getRequirement(KnownProtocolKind::DistributedActor);

// DistributedActor.actorSystem
if(name.isSimpleName(ctx.Id_actorSystem))
return getRequirement(KnownProtocolKind::DistributedActor);
Expand Down
4 changes: 4 additions & 0 deletions lib/Sema/DerivedConformances.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ class DerivedConformance {

/// Add \c children as members of the context that declares the conformance.
void addMembersToConformanceContext(ArrayRef<Decl *> children);
/// Add \c member right after the \c hint member which may be the tail
void addMemberToConformanceContext(Decl *member, Decl* hint);
/// Add \c member in front of any other existing members
void addMemberToConformanceContext(Decl *member, bool insertAtHead);

/// Get the declared type of the protocol that this is conformance is for.
Type getProtocolType() const;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
//===----------------------------------------------------------------------===//

import Swift
import _Concurrency

#if canImport(Darwin)
import Darwin
Expand All @@ -27,33 +26,25 @@ import WinSDK
/// for learning about `distributed actor` isolation, as well as early
/// prototyping stages of development where a real system is not necessary yet.
@available(SwiftStdlib 5.7, *)
public struct LocalTestingDistributedActorSystem: DistributedActorSystem, @unchecked Sendable {
public final class LocalTestingDistributedActorSystem: DistributedActorSystem, @unchecked Sendable {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This UNDOES the workaround (which actually did not work after all).

This is NOT a change in the API of the system

public typealias ActorID = LocalTestingActorAddress
public typealias ResultHandler = LocalTestingInvocationResultHandler
public typealias InvocationEncoder = LocalTestingInvocationEncoder
public typealias InvocationDecoder = LocalTestingInvocationDecoder
public typealias SerializationRequirement = any Codable


@usableFromInline
final class _Storage {
public typealias SerializationRequirement = Codable

var activeActors: [ActorID: any DistributedActor] = [:]
let activeActorsLock = _Lock()
private var activeActors: [ActorID: any DistributedActor] = [:]
private let activeActorsLock = _Lock()

var assignedIDs: Set<ActorID> = []
let assignedIDsLock = _Lock()
}
let storage: _Storage
var idProvider: ActorIDProvider = ActorIDProvider()
private var idProvider: ActorIDProvider = ActorIDProvider()
private var assignedIDs: Set<ActorID> = []
private let assignedIDsLock = _Lock()

public init() {
storage = .init()
}
public init() {}

public func resolve<Act>(id: ActorID, as actorType: Act.Type)
throws -> Act? where Act: DistributedActor, Act.ID == ActorID {
guard let anyActor = storage.activeActorsLock.withLock({ self.storage.activeActors[id] }) else {
guard let anyActor = self.activeActorsLock.withLock({ self.activeActors[id] }) else {
throw LocalTestingDistributedActorSystemError(message: "Unable to locate id '\(id)' locally")
}
guard let actor = anyActor as? Act else {
Expand All @@ -65,23 +56,25 @@ public struct LocalTestingDistributedActorSystem: DistributedActorSystem, @unche
public func assignID<Act>(_ actorType: Act.Type) -> ActorID
where Act: DistributedActor, Act.ID == ActorID {
let id = self.idProvider.next()
storage.assignedIDsLock.withLock {
self.storage.assignedIDs.insert(id)
self.assignedIDsLock.withLock {
self.assignedIDs.insert(id)
}
return id
}

public func actorReady<Act>(_ actor: Act)
where Act: DistributedActor, Act.ID == ActorID {
guard storage.assignedIDsLock.withLock({ self.storage.assignedIDs.contains(actor.id) }) else {
guard self.assignedIDsLock.withLock({ self.assignedIDs.contains(actor.id) }) else {
fatalError("Attempted to mark an unknown actor '\(actor.id)' ready")
}
self.storage.activeActors[actor.id] = actor
self.activeActorsLock.withLock {
self.activeActors[actor.id] = actor
}
}

public func resignID(_ id: ActorID) {
storage.activeActorsLock.withLock {
self.storage.activeActors.removeValue(forKey: id)
self.activeActorsLock.withLock {
self.activeActors.removeValue(forKey: id)
}
}

Expand All @@ -99,7 +92,7 @@ public struct LocalTestingDistributedActorSystem: DistributedActorSystem, @unche
where Act: DistributedActor,
Act.ID == ActorID,
Err: Error,
Res: Codable {
Res: SerializationRequirement {
fatalError("Attempted to make remote call to \(target) on actor \(actor) using a local-only actor system")
}

Expand All @@ -115,40 +108,51 @@ public struct LocalTestingDistributedActorSystem: DistributedActorSystem, @unche
fatalError("Attempted to make remote call to \(target) on actor \(actor) using a local-only actor system")
}

@usableFromInline
final class ActorIDProvider {
private struct ActorIDProvider {
private var counter: Int = 0
private let counterLock = _Lock()

@usableFromInline
init() {}

func next() -> LocalTestingActorAddress {
mutating func next() -> LocalTestingActorAddress {
let id: Int = self.counterLock.withLock {
self.counter += 1
return self.counter
}
return LocalTestingActorAddress(id)
return LocalTestingActorAddress(parse: "\(id)")
}
}
}

@available(SwiftStdlib 5.7, *)
public struct LocalTestingActorAddress: Hashable, Sendable, Codable {
public let uuid: String
@available(*, deprecated, renamed: "LocalTestingActorID")
public typealias LocalTestingActorAddress = LocalTestingActorID

public init(_ uuid: Int) {
self.uuid = "\(uuid)"
@available(SwiftStdlib 5.7, *)
public struct LocalTestingActorID: Hashable, Sendable, Codable {
@available(*, deprecated, renamed: "id")
public var address: String {
self.id
}
public let id: String

@available(*, deprecated, renamed: "init(id:)")
public init(parse id: String) {
self.id = id
}

public init(id: String) {
self.id = id
}

public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
self.uuid = try container.decode(String.self)
self.id = try container.decode(String.self)
}

public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(self.uuid)
try container.encode(self.id)
}
}

Expand Down Expand Up @@ -228,7 +232,7 @@ public struct LocalTestingDistributedActorSystemError: DistributedActorSystemErr
// === lock ----------------------------------------------------------------

@available(SwiftStdlib 5.7, *)
internal class _Lock {
fileprivate class _Lock {
#if os(iOS) || os(macOS) || os(tvOS) || os(watchOS)
private let underlying: UnsafeMutablePointer<os_unfair_lock>
#elseif os(Windows)
Expand Down Expand Up @@ -277,6 +281,7 @@ internal class _Lock {
#endif
}


@discardableResult
func withLock<T>(_ body: () -> T) -> T {
#if os(iOS) || os(macOS) || os(tvOS) || os(watchOS)
Expand Down
15 changes: 2 additions & 13 deletions test/Distributed/Runtime/distributed_actor_init_local.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@
// UNSUPPORTED: use_os_stdlib
// UNSUPPORTED: back_deployment_runtime

// REQUIRES: rdar92910719

import Distributed

enum MyError: Error {
Expand Down Expand Up @@ -92,15 +90,6 @@ distributed actor MaybeAfterAssign {
}
}

distributed actor LocalTestingSystemDA {
typealias ActorSystem = LocalTestingDistributedActorSystem
var x: Int
init() {
actorSystem = .init()
x = 100
}
}

distributed actor LocalTestingDA_Int {
typealias ActorSystem = LocalTestingDistributedActorSystem
var int: Int
Expand All @@ -111,7 +100,6 @@ distributed actor LocalTestingDA_Int {
}
}


// ==== Fake Transport ---------------------------------------------------------

struct ActorAddress: Sendable, Hashable, Codable {
Expand Down Expand Up @@ -286,7 +274,7 @@ func test() async {

let localDA = LocalTestingDA_Int()
print("localDA = \(localDA.id)")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would have crashed recently -- this patch fixes it.

// CHECK: localDA = LocalTestingActorAddress(uuid: "1")
// CHECK: localDA = LocalTestingActorID(id: "1")

// the following tests fail to initialize the actor's identity.
print("-- start of no-assign tests --")
Expand All @@ -297,6 +285,7 @@ func test() async {
// CHECK-NOT: assign
// CHECK: -- end of no-assign tests --


// resigns that come out of the deinits:

// CHECK-DAG: resign id:ActorAddress(address: "[[ID1]]")
Expand Down
6 changes: 4 additions & 2 deletions test/Distributed/distributed_actor_layout.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import FakeDistributedActorSystems
@available(SwiftStdlib 5.7, *)
typealias DefaultDistributedActorSystem = FakeActorSystem

class MyClass { }

// Ensure that the actor layout is (metadata pointer, default actor, id, system,
// <user fields>)

Expand All @@ -21,10 +23,10 @@ protocol HasActorSystem {

extension MyActor: HasActorSystem { }

// CHECK: %T27distributed_actor_accessors7MyActorC = type <{ %swift.refcounted, %swift.defaultactor, %T27FakeDistributedActorSystems0C7AddressV, %T27FakeDistributedActorSystems0aC6SystemV, %TSS }>
// CHECK: %T27distributed_actor_accessors7MyActorC = type <{ %swift.refcounted, %swift.defaultactor, %T27FakeDistributedActorSystems0C7AddressV, %T27FakeDistributedActorSystems0aC6SystemV, %T27distributed_actor_accessors7MyClassC* }>
@available(SwiftStdlib 5.7, *)
public distributed actor MyActor {
var field: String = ""
var field: MyClass = MyClass()

init(actorSystem: FakeActorSystem) {
self.actorSystem = actorSystem
Expand Down
Loading