Skip to content

Commit 5e352c8

Browse files
authored
[5.8] [stdlib] Fix String.reserveCapacity underallocation (#65988)
* [stdlib] Fix String.reserveCapacity underallocation (#65902) When called on a string that is not uniquely referenced, `String.reserveCapacity(_:)` ignores the current capacity, using the passed-in capacity for the size of its new storage. This can result in an underallocation and write past the end of the new buffer. This fix changes the new size calculation to use the current UTF-8 count as the minimum. Non-native or non-unique strings now allocate the requested capacity (or space enough for the current contents, if that's larger than what's requested). rdar://109275875 Fixes #53483 * Fix String capacity growth tests for watchOS watchOS devices can have different allocation characteristics from other devices. This modifies the string capacity growth tests to be more flexible about measuring the growth in capacity, specifically when more is allocated than requested.
1 parent 473da29 commit 5e352c8

File tree

2 files changed

+57
-2
lines changed

2 files changed

+57
-2
lines changed

stdlib/public/core/StringGutsRangeReplaceable.swift

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,14 +74,28 @@ extension _StringGuts {
7474
// Grow to accommodate at least `n` code units
7575
@usableFromInline
7676
internal mutating func grow(_ n: Int) {
77-
defer { self._invariantCheck() }
77+
defer {
78+
self._invariantCheck()
79+
_internalInvariant(
80+
self.uniqueNativeCapacity != nil && self.uniqueNativeCapacity! >= n)
81+
}
7882

7983
_internalInvariant(
8084
self.uniqueNativeCapacity == nil || self.uniqueNativeCapacity! < n)
8185

86+
// If unique and native, apply a 2x growth factor to avoid problematic
87+
// performance when used in a loop. If one if those doesn't apply, we
88+
// can just use the requested capacity (at least the current utf-8 count).
8289
// TODO: Don't do this! Growth should only happen for append...
83-
let growthTarget = Swift.max(n, (self.uniqueNativeCapacity ?? 0) * 2)
90+
let growthTarget: Int
91+
if let capacity = self.uniqueNativeCapacity {
92+
growthTarget = Swift.max(n, capacity * 2)
93+
} else {
94+
growthTarget = Swift.max(n, self.utf8Count)
95+
}
8496

97+
// `isFastUTF8` is not the same as `isNative`. It can include small
98+
// strings or foreign strings that provide contiguous UTF-8 access.
8599
if _fastPath(isFastUTF8) {
86100
let isASCII = self.isASCII
87101
let storage = self.withFastUTF8 {

validation-test/stdlib/String.swift

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2370,4 +2370,45 @@ StringTests.test("SmallString.zeroTrailingBytes") {
23702370
}
23712371
}
23722372

2373+
StringTests.test("String.CoW.reserveCapacity") {
2374+
// Test that reserveCapacity(_:) doesn't actually shrink capacity
2375+
var str = String(repeating: "a", count: 20)
2376+
str.reserveCapacity(10)
2377+
expectGE(str.capacity, 20)
2378+
str.reserveCapacity(30)
2379+
expectGE(str.capacity, 30)
2380+
let preGrowCapacity = str.capacity
2381+
2382+
// Growth shouldn't be linear
2383+
let newElementCount = (preGrowCapacity - str.count) + 10
2384+
str.append(contentsOf: String(repeating: "z", count: newElementCount))
2385+
expectGE(str.capacity, preGrowCapacity * 2)
2386+
2387+
// Capacity can shrink when copying, but not below the count
2388+
var copy = str
2389+
copy.reserveCapacity(30)
2390+
expectGE(copy.capacity, copy.count)
2391+
expectLT(copy.capacity, str.capacity)
2392+
}
2393+
2394+
StringTests.test("NSString.CoW.reserveCapacity") {
2395+
#if _runtime(_ObjC)
2396+
func newNSString() -> NSString {
2397+
NSString(string: String(repeating: "a", count: 20))
2398+
}
2399+
2400+
// Test that reserveCapacity(_:) doesn't actually shrink capacity
2401+
var str = newNSString() as String
2402+
var copy = str
2403+
copy.reserveCapacity(10)
2404+
copy.reserveCapacity(30)
2405+
expectGE(copy.capacity, 30)
2406+
2407+
var str2 = newNSString() as String
2408+
var copy2 = str2
2409+
copy2.append(contentsOf: String(repeating: "z", count: 10))
2410+
expectGE(copy2.capacity, 30)
2411+
#endif
2412+
}
2413+
23732414
runAllTests()

0 commit comments

Comments
 (0)