Skip to content

Commit e5d711c

Browse files
authored
Merge pull request #19576 from lorentey/aged-indices
[stdlib] Set, Dictionary: Radically improved index validation
2 parents 64ef8ec + c0d1535 commit e5d711c

13 files changed

+512
-154
lines changed

stdlib/public/SwiftShims/GlobalObjects.h

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,10 @@ struct _SwiftEmptyArrayStorage _swiftEmptyArrayStorage;
4343
struct _SwiftDictionaryBodyStorage {
4444
__swift_intptr_t count;
4545
__swift_intptr_t capacity;
46-
__swift_intptr_t scale;
46+
__swift_int8_t scale;
47+
__swift_int8_t reservedScale;
48+
__swift_int16_t extra;
49+
__swift_int32_t age;
4750
__swift_intptr_t seed;
4851
void *rawKeys;
4952
void *rawValues;
@@ -52,7 +55,10 @@ struct _SwiftDictionaryBodyStorage {
5255
struct _SwiftSetBodyStorage {
5356
__swift_intptr_t count;
5457
__swift_intptr_t capacity;
55-
__swift_intptr_t scale;
58+
__swift_int8_t scale;
59+
__swift_int8_t reservedScale;
60+
__swift_int16_t extra;
61+
__swift_int32_t age;
5662
__swift_intptr_t seed;
5763
void *rawElements;
5864
};
@@ -86,6 +92,16 @@ struct _SwiftHashingParameters _swift_stdlib_Hashing_parameters;
8692

8793
#ifdef __cplusplus
8894

95+
static_assert(
96+
sizeof(_SwiftDictionaryBodyStorage) ==
97+
5 * sizeof(__swift_intptr_t) + sizeof(__swift_int64_t),
98+
"_SwiftDictionaryBodyStorage has unexpected size");
99+
100+
static_assert(
101+
sizeof(_SwiftSetBodyStorage) ==
102+
4 * sizeof(__swift_intptr_t) + sizeof(__swift_int64_t),
103+
"_SwiftSetBodyStorage has unexpected size");
104+
89105
static_assert(std::is_pod<_SwiftEmptyArrayStorage>::value,
90106
"empty array type should be POD");
91107
static_assert(std::is_pod<_SwiftEmptyDictionarySingleton>::value,

stdlib/public/core/Dictionary.swift

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
// | _count |
4848
// | _capacity |
4949
// | _scale |
50+
// | _age |
5051
// | _seed |
5152
// | _rawKeys |
5253
// | _rawValue |
@@ -149,6 +150,13 @@
149150
// dictionary storage are bounds-checked, this scheme never compromises memory
150151
// safety.
151152
//
153+
// As a safeguard against using invalid indices, Set and Dictionary maintain a
154+
// mutation counter in their storage header (`_age`). This counter gets bumped
155+
// every time an element is removed and whenever the contents are
156+
// rehashed. Native indices include a copy of this counter so that index
157+
// validation can verify it matches with current storage. This can't catch all
158+
// misuse, because counters may match by accident; but it does make indexing a
159+
// lot more reliable.
152160
//
153161
// Bridging
154162
// ========
@@ -1455,8 +1463,9 @@ extension Dictionary {
14551463
return _variant.value(at: position)
14561464
}
14571465
_modify {
1458-
let index = _variant.ensureUniqueNative(preserving: position)
1459-
let address = _variant.asNative._values + index.offset
1466+
let native = _variant.ensureUniqueNative()
1467+
let bucket = native.validatedBucket(for: position)
1468+
let address = native._values + bucket.offset
14601469
yield &address.pointee
14611470
_fixLifetime(self)
14621471
}
@@ -1485,6 +1494,27 @@ extension Dictionary {
14851494
public var debugDescription: String {
14861495
return _makeCollectionDescription(withTypeName: "Dictionary.Values")
14871496
}
1497+
1498+
@inlinable
1499+
public mutating func swapAt(_ i: Index, _ j: Index) {
1500+
guard i != j else { return }
1501+
let (a, b): (_HashTable.Bucket, _HashTable.Bucket)
1502+
switch _variant {
1503+
case .native(let native):
1504+
a = native.validatedBucket(for: i)
1505+
b = native.validatedBucket(for: j)
1506+
#if _runtime(_ObjC)
1507+
case .cocoa(let cocoa):
1508+
_variant.cocoaPath()
1509+
let native = _NativeDictionary<Key, Value>(cocoa)
1510+
a = native.validatedBucket(for: i)
1511+
b = native.validatedBucket(for: j)
1512+
_variant = .native(native)
1513+
#endif
1514+
}
1515+
let isUnique = _variant.isUniquelyReferenced()
1516+
_variant.asNative.swapValuesAt(a, b, isUnique: isUnique)
1517+
}
14881518
}
14891519
}
14901520

@@ -1691,7 +1721,7 @@ extension Dictionary {
16911721
@_frozen
16921722
@usableFromInline
16931723
internal enum _Variant {
1694-
case native(_NativeDictionary<Key, Value>.Index)
1724+
case native(_HashTable.Index)
16951725
#if _runtime(_ObjC)
16961726
case cocoa(_CocoaDictionary.Index)
16971727
#endif
@@ -1708,7 +1738,7 @@ extension Dictionary {
17081738

17091739
@inlinable
17101740
@inline(__always)
1711-
internal init(_native index: _NativeDictionary<Key, Value>.Index) {
1741+
internal init(_native index: _HashTable.Index) {
17121742
self.init(_variant: .native(index))
17131743
}
17141744

@@ -1740,13 +1770,14 @@ extension Dictionary.Index {
17401770
#endif
17411771

17421772
@usableFromInline @_transparent
1743-
internal var _asNative: _NativeDictionary<Key, Value>.Index {
1773+
internal var _asNative: _HashTable.Index {
17441774
switch _variant {
17451775
case .native(let nativeIndex):
17461776
return nativeIndex
17471777
#if _runtime(_ObjC)
17481778
case .cocoa:
1749-
_sanityCheckFailure("internal error: does not contain a native index")
1779+
_preconditionFailure(
1780+
"Attempting to access Dictionary elements using an invalid index")
17501781
#endif
17511782
}
17521783
}
@@ -1756,7 +1787,8 @@ extension Dictionary.Index {
17561787
internal var _asCocoa: _CocoaDictionary.Index {
17571788
switch _variant {
17581789
case .native:
1759-
_sanityCheckFailure("internal error: does not contain a Cocoa index")
1790+
_preconditionFailure(
1791+
"Attempting to access Dictionary elements using an invalid index")
17601792
case .cocoa(let cocoaIndex):
17611793
return cocoaIndex
17621794
}
@@ -1811,14 +1843,14 @@ extension Dictionary.Index: Hashable {
18111843
switch _variant {
18121844
case .native(let nativeIndex):
18131845
hasher.combine(0 as UInt8)
1814-
hasher.combine(nativeIndex.offset)
1846+
hasher.combine(nativeIndex.bucket.offset)
18151847
case .cocoa(let cocoaIndex):
18161848
_cocoaPath()
18171849
hasher.combine(1 as UInt8)
18181850
hasher.combine(cocoaIndex.currentKeyIndex)
18191851
}
18201852
#else
1821-
hasher.combine(_asNative.offset)
1853+
hasher.combine(_asNative.bucket.offset)
18221854
#endif
18231855
}
18241856
}

stdlib/public/core/DictionaryBridging.swift

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -403,7 +403,8 @@ final internal class _SwiftDeferredNSDictionary<Key: Hashable, Value>
403403
let unmanagedObjects = _UnmanagedAnyObjectArray(objects!)
404404
var bucket = _HashTable.Bucket(offset: Int(theState.extra.0))
405405
let endBucket = hashTable.endBucket
406-
_precondition(bucket == endBucket || hashTable.isOccupied(bucket))
406+
_precondition(bucket == endBucket || hashTable.isOccupied(bucket),
407+
"Invalid fast enumeration state")
407408
var stored = 0
408409

409410
// Only need to bridge once, so we can hoist it out of the loop.
@@ -528,7 +529,7 @@ extension _CocoaDictionary: _DictionaryBuffer {
528529
@inline(__always)
529530
func key(at index: Index) -> Key {
530531
_precondition(index.base.object === self.object, "Invalid index")
531-
return index.allKeys[index.currentKeyIndex]
532+
return index.key
532533
}
533534

534535
@inlinable
@@ -607,6 +608,25 @@ extension _CocoaDictionary {
607608
}
608609
}
609610

611+
extension _CocoaDictionary.Index {
612+
@inlinable
613+
@nonobjc
614+
internal var key: AnyObject {
615+
_precondition(currentKeyIndex < allKeys.value,
616+
"Attempting to access Dictionary elements using an invalid index")
617+
return allKeys[currentKeyIndex]
618+
}
619+
620+
@usableFromInline
621+
@nonobjc
622+
internal var age: Int32 {
623+
@_effects(releasenone)
624+
get {
625+
return _HashTable.age(for: base.object)
626+
}
627+
}
628+
}
629+
610630
extension _CocoaDictionary.Index: Equatable {
611631
@inlinable
612632
internal static func == (

stdlib/public/core/DictionaryStorage.swift

Lines changed: 66 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,14 @@ import SwiftShims
1616
/// Enough bytes are allocated to hold the bitmap for marking valid entries,
1717
/// keys, and values. The data layout starts with the bitmap, followed by the
1818
/// keys, followed by the values.
19-
//
20-
// See the docs at the top of the file for more details on this type
21-
//
22-
// NOTE: The precise layout of this type is relied on in the runtime
23-
// to provide a statically allocated empty singleton.
24-
// See stdlib/public/stubs/GlobalObjects.cpp for details.
2519
@_fixed_layout // FIXME(sil-serialize-all)
2620
@usableFromInline
2721
@_objc_non_lazy_realization
2822
internal class _RawDictionaryStorage: __SwiftNativeNSDictionary {
23+
// NOTE: The precise layout of this type is relied on in the runtime to
24+
// provide a statically allocated empty singleton. See
25+
// stdlib/public/stubs/GlobalObjects.cpp for details.
26+
2927
/// The current number of occupied entries in this dictionary.
3028
@usableFromInline
3129
@nonobjc
@@ -41,15 +39,37 @@ internal class _RawDictionaryStorage: __SwiftNativeNSDictionary {
4139
/// power of `scale`.
4240
@usableFromInline
4341
@nonobjc
44-
internal final var _scale: Int
42+
internal final var _scale: Int8
43+
44+
/// The scale corresponding to the highest `reserveCapacity(_:)` call so far,
45+
/// or 0 if there were none. This may be used later to allow removals to
46+
/// resize storage.
47+
///
48+
/// FIXME: <rdar://problem/18114559> Shrink storage on deletion
49+
@usableFromInline
50+
@nonobjc
51+
internal final var _reservedScale: Int8
52+
53+
// Currently unused, set to zero.
54+
@nonobjc
55+
internal final var _extra: Int16
56+
57+
/// A mutation count, enabling stricter index validation.
58+
@usableFromInline
59+
@nonobjc
60+
internal final var _age: Int32
4561

62+
/// The hash seed used to hash elements in this dictionary instance.
4663
@usableFromInline
4764
internal final var _seed: Int
4865

66+
/// A raw pointer to the start of the tail-allocated hash buffer holding keys.
4967
@usableFromInline
5068
@nonobjc
5169
internal final var _rawKeys: UnsafeMutableRawPointer
5270

71+
/// A raw pointer to the start of the tail-allocated hash buffer holding
72+
/// values.
5373
@usableFromInline
5474
@nonobjc
5575
internal final var _rawValues: UnsafeMutableRawPointer
@@ -272,7 +292,8 @@ final internal class _DictionaryStorage<Key: Hashable, Value>
272292
let unmanagedObjects = _UnmanagedAnyObjectArray(objects!)
273293
var bucket = _HashTable.Bucket(offset: Int(theState.extra.0))
274294
let endBucket = hashTable.endBucket
275-
_precondition(bucket == endBucket || _hashTable.isOccupied(bucket))
295+
_precondition(bucket == endBucket || hashTable.isOccupied(bucket),
296+
"Invalid fast enumeration state")
276297
var stored = 0
277298
for i in 0..<count {
278299
if bucket == endBucket { break }
@@ -345,23 +366,43 @@ extension _DictionaryStorage {
345366
_sanityCheck(capacity >= original._count)
346367
let scale = _HashTable.scale(forCapacity: capacity)
347368
let rehash = (scale != original._scale)
348-
let newStorage = _DictionaryStorage<Key, Value>.allocate(scale: scale)
369+
let newStorage = _DictionaryStorage<Key, Value>.allocate(
370+
scale: scale,
371+
// Invalidate indices if we're rehashing.
372+
age: rehash ? nil : original._age
373+
)
349374
return (newStorage, rehash)
350375
}
351376

352377
@usableFromInline
353378
@_effects(releasenone)
354379
static internal func allocate(capacity: Int) -> _DictionaryStorage {
355380
let scale = _HashTable.scale(forCapacity: capacity)
356-
return allocate(scale: scale)
381+
return allocate(scale: scale, age: nil)
357382
}
358383

359-
static internal func allocate(scale: Int) -> _DictionaryStorage {
384+
#if _runtime(_ObjC)
385+
@usableFromInline
386+
@_effects(releasenone)
387+
static internal func convert(
388+
_ cocoa: _CocoaDictionary,
389+
capacity: Int
390+
) -> _DictionaryStorage {
391+
let scale = _HashTable.scale(forCapacity: capacity)
392+
let age = _HashTable.age(for: cocoa.object)
393+
return allocate(scale: scale, age: age)
394+
}
395+
#endif
396+
397+
static internal func allocate(
398+
scale: Int8,
399+
age: Int32?
400+
) -> _DictionaryStorage {
360401
// The entry count must be representable by an Int value; hence the scale's
361402
// peculiar upper bound.
362403
_sanityCheck(scale >= 0 && scale < Int.bitWidth - 1)
363404

364-
let bucketCount = 1 &<< scale
405+
let bucketCount = (1 as Int) &<< scale
365406
let wordCount = _UnsafeBitset.wordCount(forCapacity: bucketCount)
366407
let storage = Builtin.allocWithTailElems_3(
367408
_DictionaryStorage<Key, Value>.self,
@@ -379,6 +420,18 @@ extension _DictionaryStorage {
379420
storage._count = 0
380421
storage._capacity = _HashTable.capacity(forScale: scale)
381422
storage._scale = scale
423+
storage._reservedScale = 0
424+
storage._extra = 0
425+
426+
if let age = age {
427+
storage._age = age
428+
} else {
429+
// The default mutation count is simply a scrambled version of the storage
430+
// address.
431+
storage._age = Int32(
432+
truncatingIfNeeded: ObjectIdentifier(storage).hashValue)
433+
}
434+
382435
storage._rawKeys = UnsafeMutableRawPointer(keysAddr)
383436
storage._rawValues = UnsafeMutableRawPointer(valuesAddr)
384437

@@ -390,7 +443,7 @@ extension _DictionaryStorage {
390443
// FIXME: Use true per-instance seeding instead. Per-capacity seeding still
391444
// leaves hash values the same in same-sized tables, which may affect
392445
// operations on two tables at once. (E.g., union.)
393-
storage._seed = scale
446+
storage._seed = Int(scale)
394447

395448
// Initialize hash table metadata.
396449
storage._hashTable.clear()

0 commit comments

Comments
 (0)