Skip to content

Commit ef4a89c

Browse files
devincoughlinatrick
authored andcommitted
[Exclusivity] Enforce exclusive access for class offsets in KeyPaths
Add dynamic enforcement of exclusive access when a KeyPath directly accesses a final stored property on an instance of a class. For read-only projections, this begins and ends the access immediately. For mutable projections, this uses the ClassHolder to perform a long-term access that lasts as long as the lifetime of the ClassHolder. rdar://problem/31972680
1 parent 636b5f6 commit ef4a89c

File tree

4 files changed

+208
-36
lines changed

4 files changed

+208
-36
lines changed

stdlib/public/core/KeyPath.swift

Lines changed: 74 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -626,22 +626,73 @@ internal enum KeyPathComponent: Hashable {
626626
}
627627

628628
// A class that maintains ownership of another object while a mutable projection
629-
// into it is underway.
629+
// into it is underway. The lifetime of the instance of this class is also used
630+
// to begin and end exclusive 'modify' access to the projected address.
630631
@_fixed_layout // FIXME(sil-serialize-all)
631632
@usableFromInline // FIXME(sil-serialize-all)
632-
internal final class ClassHolder {
633+
internal final class ClassHolder<ProjectionType> {
634+
635+
/// The type of the scratch record passed to the runtime to record
636+
/// accesses to guarantee exlcusive access.
637+
internal typealias AccessRecord = Builtin.UnsafeValueBuffer
638+
633639
@usableFromInline // FIXME(sil-serialize-all)
634-
internal let previous: AnyObject?
640+
internal var previous: AnyObject?
635641
@usableFromInline // FIXME(sil-serialize-all)
636-
internal let instance: AnyObject
642+
internal var instance: AnyObject
637643

638644
@inlinable // FIXME(sil-serialize-all)
639645
internal init(previous: AnyObject?, instance: AnyObject) {
640646
self.previous = previous
641647
self.instance = instance
642648
}
649+
643650
@inlinable // FIXME(sil-serialize-all)
644-
deinit {}
651+
@usableFromInline // FIXME(sil-serialize-all)
652+
internal final class func _create(
653+
previous: AnyObject?,
654+
instance: AnyObject,
655+
accessingAddress address: UnsafeRawPointer,
656+
type: ProjectionType.Type
657+
) -> ClassHolder {
658+
659+
// Tail allocate the UnsafeValueBuffer used as the AccessRecord.
660+
// This avoids a second heap allocation since there is no source-level way to
661+
// initialize a Builtin.UnsafeValueBuffer type and thus we cannot have a
662+
// stored property of that type.
663+
let holder: ClassHolder = Builtin.allocWithTailElems_1(self,
664+
1._builtinWordValue,
665+
AccessRecord.self)
666+
667+
// Initialize the ClassHolder's instance variables. This is done via
668+
// withUnsafeMutablePointer(to:) because the instance was just allocated with
669+
// allocWithTailElems_1 and so we need to make sure to use an initialization
670+
// rather than an assignment.
671+
withUnsafeMutablePointer(to: &holder.previous) {
672+
$0.initialize(to: previous)
673+
}
674+
675+
withUnsafeMutablePointer(to: &holder.instance) {
676+
$0.initialize(to: instance)
677+
}
678+
679+
let accessRecordPtr = Builtin.projectTailElems(holder, AccessRecord.self)
680+
681+
// Begin a 'modify' access to the address. This access is ended in
682+
// ClassHolder's deinitializer.
683+
Builtin.beginUnpairedModifyAccess(address._rawValue, accessRecordPtr, type)
684+
685+
return holder
686+
}
687+
688+
@inlinable // FIXME(sil-serialize-all)
689+
@usableFromInline // FIXME(sil-serialize-all)
690+
deinit {
691+
let accessRecordPtr = Builtin.projectTailElems(self, AccessRecord.self)
692+
693+
// Ends the access begun in _create().
694+
Builtin.endUnpairedAccess(accessRecordPtr)
695+
}
645696
}
646697

647698
// A class that triggers writeback to a pointer when destroyed.
@@ -1185,7 +1236,15 @@ internal struct RawKeyPathComponent {
11851236
let baseObj = unsafeBitCast(base, to: AnyObject.self)
11861237
let basePtr = UnsafeRawPointer(Builtin.bridgeToRawPointer(baseObj))
11871238
defer { _fixLifetime(baseObj) }
1188-
return .continue(basePtr.advanced(by: offset)
1239+
1240+
let offsetAddress = basePtr.advanced(by: offset)
1241+
1242+
// Perform an instaneous record access on the address in order to
1243+
// ensure that the read will not conflict with an already in-progress
1244+
// 'modify' access.
1245+
Builtin.performInstantaneousReadAccess(offsetAddress._rawValue,
1246+
NewValue.self)
1247+
return .continue(offsetAddress
11891248
.assumingMemoryBound(to: NewValue.self)
11901249
.pointee)
11911250

@@ -1244,12 +1303,16 @@ internal struct RawKeyPathComponent {
12441303
// AnyObject memory can alias any class reference memory, so we can
12451304
// assume type here
12461305
let object = base.assumingMemoryBound(to: AnyObject.self).pointee
1247-
// The base ought to be kept alive for the duration of the derived access
1248-
keepAlive = keepAlive == nil
1249-
? object
1250-
: ClassHolder(previous: keepAlive, instance: object)
1251-
return UnsafeRawPointer(Builtin.bridgeToRawPointer(object))
1306+
let offsetAddress = UnsafeRawPointer(Builtin.bridgeToRawPointer(object))
12521307
.advanced(by: offset)
1308+
1309+
// Keep the base alive for the duration of the derived access and also
1310+
// enforce exclusive access to the address.
1311+
keepAlive = ClassHolder._create(previous: keepAlive, instance: object,
1312+
accessingAddress: offsetAddress,
1313+
type: NewValue.self)
1314+
1315+
return offsetAddress
12531316

12541317
case .mutatingGetSet(id: _, get: let rawGet, set: let rawSet,
12551318
argument: let argument):

test/Interpreter/enforce_exclusive_access.swift

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -241,26 +241,95 @@ ExclusiveAccessTestSuite.test("InoutWriteEscapeWrite")
241241
}
242242

243243
class ClassWithStoredProperty {
244-
var f = 7
244+
final var f = 7
245245
}
246246

247-
// FIXME: This should trap with a modify/modify violation at run time.
248-
ExclusiveAccessTestSuite.test("KeyPathClassStoredProp")
247+
ExclusiveAccessTestSuite.test("KeyPathInoutDirectWriteClassStoredProp")
249248
.skip(.custom(
250249
{ _isFastAssertConfiguration() },
251250
reason: "this trap is not guaranteed to happen in -Ounchecked"))
252-
// .crashOutputMatches("Previous access (a modification) started at")
253-
// .crashOutputMatches("Current access (a modification) started at")
251+
.crashOutputMatches("Previous access (a modification) started at")
252+
.crashOutputMatches("Current access (a modification) started at")
253+
.code
254+
{
255+
let getF = \ClassWithStoredProperty.f
256+
let c = ClassWithStoredProperty()
257+
258+
expectCrashLater()
259+
modifyAndPerform(&c[keyPath: getF]) {
260+
c.f = 12
261+
}
262+
}
263+
264+
ExclusiveAccessTestSuite.test("KeyPathInoutDirectReadClassStoredProp")
265+
.skip(.custom(
266+
{ _isFastAssertConfiguration() },
267+
reason: "this trap is not guaranteed to happen in -Ounchecked"))
268+
.crashOutputMatches("Previous access (a modification) started at")
269+
.crashOutputMatches("Current access (a read) started at")
254270
.code
255271
{
256272
let getF = \ClassWithStoredProperty.f
257273
let c = ClassWithStoredProperty()
258274

259-
// expectCrashLater()
275+
expectCrashLater()
276+
modifyAndPerform(&c[keyPath: getF]) {
277+
let x = c.f
278+
_blackHole(x)
279+
}
280+
}
281+
282+
// Unlike inout accesses, read-only inout-to-pointer conversions on key paths for
283+
// final stored-properties do not perform a long-term read access. Instead, they
284+
// materialize a location on the stack, perform an instantaneous read
285+
// from the storage indicated by the key path and write the read value to the
286+
// stack location. The stack location is then passed as the pointer for the
287+
// inout-to-pointer conversion.
288+
//
289+
// This means that there is no conflict between a read-only inout-to-pointer
290+
// conversion of the key path location for a call and an access to the
291+
// the same location within the call.
292+
ExclusiveAccessTestSuite.test("KeyPathReadOnlyInoutToPtrDirectWriteClassStoredProp") {
293+
let getF = \ClassWithStoredProperty.f
294+
let c = ClassWithStoredProperty()
295+
296+
// This performs an instantaneous read
297+
readAndPerform(&c[keyPath: getF]) {
298+
c.f = 12 // no-trap
299+
}
300+
}
301+
302+
ExclusiveAccessTestSuite.test("SequentialKeyPathWritesDontOverlap") {
303+
let getF = \ClassWithStoredProperty.f
304+
let c = ClassWithStoredProperty()
305+
306+
c[keyPath: getF] = 7
307+
c[keyPath: getF] = 8 // no-trap
308+
c[keyPath: getF] += c[keyPath: getF] + 1 // no-trap
309+
}
310+
311+
// This does not trap, for now, because the standard library (and thus KeyPath) is
312+
// compiled in Swift 3 mode and we currently log rather than trap in Swift mode.
313+
ExclusiveAccessTestSuite.test("KeyPathInoutKeyPathWriteClassStoredProp")
314+
{
315+
let getF = \ClassWithStoredProperty.f
316+
let c = ClassWithStoredProperty()
317+
260318
modifyAndPerform(&c[keyPath: getF]) {
261319
c[keyPath: getF] = 12
262320
}
263321
}
264322

323+
// This does not currently trap because the standard library is compiled in Swift 3 mode,
324+
// which logs.
325+
ExclusiveAccessTestSuite.test("KeyPathInoutKeyPathReadClassStoredProp") {
326+
let getF = \ClassWithStoredProperty.f
327+
let c = ClassWithStoredProperty()
328+
329+
modifyAndPerform(&c[keyPath: getF]) {
330+
let y = c[keyPath: getF]
331+
_blackHole(y)
332+
}
333+
}
265334

266335
runAllTests()

test/Interpreter/enforce_exclusive_access_backtrace.swift

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,61 @@ func readAndPerform<T>(_ _: UnsafePointer<T>, closure: () ->()) {
1717
closure()
1818
}
1919

20+
func writeAndPerform<T>(_ _: UnsafeMutablePointer<T>, closure: () ->()) {
21+
closure()
22+
}
23+
2024
var globalX = X()
2125
withUnsafePointer(to: &globalX) { _ = fputs(String(format: "globalX: 0x%lx\n", Int(bitPattern: $0)), stderr) }
2226
// CHECK: globalX: [[ADDR:0x.*]]
2327

28+
fputs("Global Access\n", stderr);
2429
readAndPerform(&globalX) {
2530
globalX = X()
31+
// CHECK-LABEL: Global Access
2632
// CHECK: Simultaneous accesses to [[ADDR]], but modification requires exclusive access.
2733
// CHECK: Previous access (a read) started at a.out`main + {{.*}} (0x{{.*}}).
2834
// CHECK: Current access (a modification) started at:
2935
// CHECK: a.out {{.*}} closure
3036
// CHECK: a.out {{.*}} readAndPerform
3137
// CHECK: a.out {{.*}} main
3238
}
39+
40+
class C {
41+
final var f = 7
42+
}
43+
44+
var c = C()
45+
withUnsafePointer(to: &c.f) { _ = fputs(String(format: "c.f: 0x%lx\n", Int(bitPattern: $0)), stderr) }
46+
// CHECK: c.f: [[C_F_ADDR:0x.*]]
47+
48+
// Key path accesses are performed in the Standard Library. The Standard Library
49+
// is currently compiled in Swift 3 mode and the compiler currently logs conflicts
50+
// (rather than trapping on them) code compiled in Swift 3 mode. For this reason
51+
// conflicts where the second conflicting access is via a key path will log rather than
52+
// trap.
53+
54+
fputs("Overlapping Key Path Write Accesses\n", stderr);
55+
writeAndPerform(&c[keyPath: \C.f]) {
56+
c[keyPath: \C.f] = 8
57+
// CHECK-LABEL: Overlapping Key Path Write Accesses
58+
// CHECK: Simultaneous accesses to [[C_F_ADDR]], but modification requires exclusive access.
59+
// CHECK: Previous access (a modification)
60+
// CHECK: Current access (a modification)
61+
// CHECK: a.out {{.*}} closure
62+
// CHECK: a.out {{.*}} writeAndPerform
63+
// CHECK: a.out {{.*}} main
64+
}
65+
66+
fputs("Overlapping Key Path Write Access and Key Path Read Access\n", stderr);
67+
writeAndPerform(&c[keyPath: \C.f]) {
68+
let x = c[keyPath: \C.f]
69+
_blackHole(x)
70+
// CHECK-LABEL: Overlapping Key Path Write Access and Key Path Read Access
71+
// CHECK: Simultaneous accesses to [[C_F_ADDR]], but modification requires exclusive access.
72+
// CHECK: Previous access (a modification)
73+
// CHECK: Current access (a read)
74+
// CHECK: a.out {{.*}} closure
75+
// CHECK: a.out {{.*}} writeAndPerform
76+
// CHECK: a.out {{.*}} main
77+
}

test/SILGen/builtins.swift

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -370,31 +370,26 @@ func getTailAddr<T1, T2>(start: Builtin.RawPointer, i: Builtin.Word, ty1: T1.Typ
370370
}
371371

372372
// CHECK-LABEL: sil hidden @$S8builtins25beginUnpairedModifyAccess{{[_0-9a-zA-Z]*}}F
373-
// CHECK: [[P2A_ADDR:%.*]] = pointer_to_address %0
374-
// CHECK: [[P2A_SCRATCH:%.*]] = pointer_to_address %1
375-
// CHECK: begin_unpaired_access [modify] [dynamic] [[P2A_ADDR]] : $*T1, [[P2A_SCRATCH]] : $*Builtin.UnsafeValueBuffer
376-
// CHECK: [[RESULT:%.*]] = tuple ()
377-
// CHECK: [[RETURN:%.*]] = tuple ()
378-
// CHECK: return [[RETURN]] : $()
379-
// CHECK: } // end sil function '$S8builtins25beginUnpairedModifyAccess{{[_0-9a-zA-Z]*}}F'
380373
func beginUnpairedModifyAccess<T1>(address: Builtin.RawPointer, scratch: Builtin.RawPointer, ty1: T1.Type) {
381-
Builtin.beginUnpairedModifyAccess(address, scratch, ty1);
382-
}
374+
// CHECK: [[P2A_ADDR:%.*]] = pointer_to_address %0
375+
// CHECK: [[P2A_SCRATCH:%.*]] = pointer_to_address %1
376+
// CHECK: begin_unpaired_access [modify] [dynamic] [[P2A_ADDR]] : $*T1, [[P2A_SCRATCH]] : $*Builtin.UnsafeValueBuffer
377+
// CHECK: [[RESULT:%.*]] = tuple ()
378+
// CHECK: [[RETURN:%.*]] = tuple ()
379+
// CHECK: return [[RETURN]] : $()
383380

384-
// CHECK-LABEL: sil hidden @$S8builtins17endUnpairedAccess{{[_0-9a-zA-Z]*}}F : $@convention(thin) (Builtin.RawPointer) -> () {
385-
// CHECK: [[P2A_ADDR:%.*]] = pointer_to_address %0
386-
// CHECK: end_unpaired_access [dynamic] [[P2A_ADDR]]
387-
// CHECK: } // end sil function '$S8builtins17endUnpairedAccess{{[_0-9a-zA-Z]*}}F'
388-
func endUnpairedAccess(address: Builtin.RawPointer) {
389-
Builtin.endUnpairedAccess(address)
381+
Builtin.beginUnpairedModifyAccess(address, scratch, ty1);
390382
}
391383

392384
// CHECK-LABEL: sil hidden @$S8builtins30performInstantaneousReadAccess{{[_0-9a-zA-Z]*}}F
393-
// CHECK: [[P2A_ADDR:%.*]] = pointer_to_address %0
394-
// CHECK: [[ACCESS:%.*]] = begin_access [read] [dynamic] [no_nested_conflict] [[P2A_ADDR]] : $*T1
395-
// CHECK-NEXT: end_access [[ACCESS]] : $*T1
396-
// CHECK: } // end sil function '$S8builtins30performInstantaneousReadAccess{{[_0-9a-zA-Z]*}}F'
397385
func performInstantaneousReadAccess<T1>(address: Builtin.RawPointer, scratch: Builtin.RawPointer, ty1: T1.Type) {
386+
// CHECK: [[P2A_ADDR:%.*]] = pointer_to_address %0
387+
// CHECK: [[ACCESS:%.*]] = begin_access [read] [dynamic] [no_nested_conflict] [[P2A_ADDR]] : $*T1
388+
// CHECK-NEXT: end_access [[ACCESS]] : $*T1
389+
// CHECK: [[RESULT:%.*]] = tuple ()
390+
// CHECK: [[RETURN:%.*]] = tuple ()
391+
// CHECK: return [[RETURN]] : $()
392+
398393
Builtin.performInstantaneousReadAccess(address, ty1);
399394
}
400395

0 commit comments

Comments
 (0)