Skip to content

Commit 9487a49

Browse files
committed
Cleanup lifetime management around NSData.bytes.
Several blocks of code in Foundation use the .bytes member on NSData to construct an interior pointer. However, NSData can potentially be freed very early, and in newer Swift releases is more likely to do so, leading to this pointer dangling. This patch attempts a minimal change by forcing the NSData object to live longer using withExtendedLifetime. This should resolve the misuse fairly effectively. It does not address cases where the code could potentially be entirely rewritten to be more efficient or more sensible, because I wasn't here to engage in a complete refactor.
1 parent 2232811 commit 9487a49

File tree

8 files changed

+144
-101
lines changed

8 files changed

+144
-101
lines changed

Sources/Foundation/Data.swift

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2020,7 +2020,9 @@ public struct Data : ReferenceConvertible, Equatable, Hashable, RandomAccessColl
20202020
@inlinable // This is @inlinable as a convenience initializer.
20212021
public init(contentsOf url: __shared URL, options: Data.ReadingOptions = []) throws {
20222022
let d = try NSData(contentsOf: url, options: ReadingOptions(rawValue: options.rawValue))
2023-
self.init(bytes: d.bytes, count: d.length)
2023+
self = withExtendedLifetime(d) {
2024+
return Data(bytes: d.bytes, count: d.length)
2025+
}
20242026
}
20252027

20262028
/// Initialize a `Data` from a Base-64 encoded String using the given options.
@@ -2031,7 +2033,9 @@ public struct Data : ReferenceConvertible, Equatable, Hashable, RandomAccessColl
20312033
@inlinable // This is @inlinable as a convenience initializer.
20322034
public init?(base64Encoded base64String: __shared String, options: Data.Base64DecodingOptions = []) {
20332035
if let d = NSData(base64Encoded: base64String, options: Base64DecodingOptions(rawValue: options.rawValue)) {
2034-
self.init(bytes: d.bytes, count: d.length)
2036+
self = withExtendedLifetime(d) {
2037+
Data(bytes: d.bytes, count: d.length)
2038+
}
20352039
} else {
20362040
return nil
20372041
}
@@ -2046,7 +2050,9 @@ public struct Data : ReferenceConvertible, Equatable, Hashable, RandomAccessColl
20462050
@inlinable // This is @inlinable as a convenience initializer.
20472051
public init?(base64Encoded base64Data: __shared Data, options: Data.Base64DecodingOptions = []) {
20482052
if let d = NSData(base64Encoded: base64Data, options: Base64DecodingOptions(rawValue: options.rawValue)) {
2049-
self.init(bytes: d.bytes, count: d.length)
2053+
self = withExtendedLifetime(d) {
2054+
Data(bytes: d.bytes, count: d.length)
2055+
}
20502056
} else {
20512057
return nil
20522058
}

Sources/Foundation/NSAttributedString.swift

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -558,27 +558,29 @@ internal func _NSReadIntFromMutableAttributedStringCoding(_ data: NSData, _ star
558558
let length = data.length
559559

560560
var value = 0
561-
562-
while offset < length {
563-
let i = Int(data.bytes.load(fromByteOffset: offset, as: UInt8.self))
564-
565-
offset += 1
566-
567-
let isLast = i < 128
568-
569-
let intermediateValue = multiplier.multipliedReportingOverflow(by: isLast ? i : (i - 128))
570-
guard !intermediateValue.overflow else { return nil }
571-
572-
let newValue = value.addingReportingOverflow(intermediateValue.partialValue)
573-
guard !newValue.overflow else { return nil }
574-
575-
value = newValue.partialValue
576561

577-
if isLast {
578-
return (value: value, newOffset: offset)
562+
withExtendedLifetime(data) {
563+
while offset < length {
564+
let i = Int(data.bytes.load(fromByteOffset: offset, as: UInt8.self))
565+
566+
offset += 1
567+
568+
let isLast = i < 128
569+
570+
let intermediateValue = multiplier.multipliedReportingOverflow(by: isLast ? i : (i - 128))
571+
guard !intermediateValue.overflow else { return nil }
572+
573+
let newValue = value.addingReportingOverflow(intermediateValue.partialValue)
574+
guard !newValue.overflow else { return nil }
575+
576+
value = newValue.partialValue
577+
578+
if isLast {
579+
return (value: value, newOffset: offset)
580+
}
581+
582+
multiplier *= 128
579583
}
580-
581-
multiplier *= 128
582584
}
583585

584586
return nil // Getting to the end of the stream indicates error, since we were still expecting more bytes

Sources/Foundation/NSData.swift

Lines changed: 39 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -153,15 +153,20 @@ open class NSData : NSObject, NSCopying, NSMutableCopying, NSSecureCoding {
153153
public init(contentsOfFile path: String, options readOptionsMask: ReadingOptions = []) throws {
154154
super.init()
155155
let readResult = try NSData.readBytesFromFileWithExtendedAttributes(path, options: readOptionsMask)
156-
_init(bytes: readResult.bytes, length: readResult.length, copy: false, deallocator: readResult.deallocator)
156+
157+
withExtendedLifetime(readResult) {
158+
_init(bytes: readResult.bytes, length: readResult.length, copy: false, deallocator: readResult.deallocator)
159+
}
157160
}
158161

159162
/// Initializes a data object with the contents of the file at a given path.
160163
public init?(contentsOfFile path: String) {
161164
do {
162165
super.init()
163166
let readResult = try NSData.readBytesFromFileWithExtendedAttributes(path, options: [])
164-
_init(bytes: readResult.bytes, length: readResult.length, copy: false, deallocator: readResult.deallocator)
167+
withExtendedLifetime(readResult) {
168+
_init(bytes: readResult.bytes, length: readResult.length, copy: false, deallocator: readResult.deallocator)
169+
}
165170
} catch {
166171
return nil
167172
}
@@ -179,15 +184,19 @@ open class NSData : NSObject, NSCopying, NSMutableCopying, NSSecureCoding {
179184
public init(contentsOf url: URL, options readOptionsMask: ReadingOptions = []) throws {
180185
super.init()
181186
let (data, _) = try NSData.contentsOf(url: url, options: readOptionsMask)
182-
_init(bytes: UnsafeMutableRawPointer(mutating: data.bytes), length: data.length, copy: true)
187+
withExtendedLifetime(data) {
188+
_init(bytes: UnsafeMutableRawPointer(mutating: data.bytes), length: data.length, copy: true)
189+
}
183190
}
184191

185192
/// Initializes a data object with the data from the location specified by a given URL.
186193
public init?(contentsOf url: URL) {
187194
super.init()
188195
do {
189196
let (data, _) = try NSData.contentsOf(url: url)
190-
_init(bytes: UnsafeMutableRawPointer(mutating: data.bytes), length: data.length, copy: true)
197+
withExtendedLifetime(data) {
198+
_init(bytes: UnsafeMutableRawPointer(mutating: data.bytes), length: data.length, copy: true)
199+
}
191200
} catch {
192201
return nil
193202
}
@@ -375,15 +384,19 @@ open class NSData : NSObject, NSCopying, NSMutableCopying, NSSecureCoding {
375384
guard let data = aDecoder._decodePropertyListForKey("NS.data") as? NSData else {
376385
return nil
377386
}
378-
_init(bytes: UnsafeMutableRawPointer(mutating: data.bytes), length: data.length, copy: true)
387+
withExtendedLifetime(data) {
388+
_init(bytes: UnsafeMutableRawPointer(mutating: data.bytes), length: data.length, copy: true)
389+
}
379390
} else {
380391
let result : Data? = aDecoder.withDecodedUnsafeBufferPointer(forKey: "NS.bytes") {
381392
guard let buffer = $0 else { return nil }
382393
return Data(buffer: buffer)
383394
}
384395

385-
guard let r = result else { return nil }
386-
_init(bytes: UnsafeMutableRawPointer(mutating: r._nsObject.bytes), length: r.count, copy: true)
396+
guard var r = result else { return nil }
397+
result.withUnsafeMutableBytes {
398+
_init(bytes: $0.baseAddress, length: $0.count, copy: true)
399+
}
387400
}
388401
}
389402

@@ -559,24 +572,27 @@ open class NSData : NSObject, NSCopying, NSMutableCopying, NSSecureCoding {
559572
/// Finds and returns the range of the first occurrence of the given data, within the given range, subject to given options.
560573
open func range(of dataToFind: Data, options mask: SearchOptions = [], in searchRange: NSRange) -> NSRange {
561574
let dataToFind = dataToFind._nsObject
562-
guard dataToFind.length > 0 else {return NSRange(location: NSNotFound, length: 0)}
563-
guard let searchRange = Range(searchRange) else {fatalError("invalid range")}
564-
565-
precondition(searchRange.upperBound <= self.length, "range outside the bounds of data")
566575

567-
let basePtr = self.bytes.bindMemory(to: UInt8.self, capacity: self.length)
568-
let baseData = UnsafeBufferPointer<UInt8>(start: basePtr, count: self.length)[searchRange]
569-
let searchPtr = dataToFind.bytes.bindMemory(to: UInt8.self, capacity: dataToFind.length)
570-
let search = UnsafeBufferPointer<UInt8>(start: searchPtr, count: dataToFind.length)
571-
572-
let location : Int?
573-
let anchored = mask.contains(.anchored)
574-
if mask.contains(.backwards) {
575-
location = NSData.searchSubSequence(search.reversed(), inSequence: baseData.reversed(),anchored : anchored).map {$0.base-search.count}
576-
} else {
577-
location = NSData.searchSubSequence(search, inSequence: baseData,anchored : anchored)
576+
return withExtendedLifetime(dataToFind) {
577+
guard dataToFind.length > 0 else {return NSRange(location: NSNotFound, length: 0)}
578+
guard let searchRange = Range(searchRange) else {fatalError("invalid range")}
579+
580+
precondition(searchRange.upperBound <= self.length, "range outside the bounds of data")
581+
582+
let basePtr = self.bytes.bindMemory(to: UInt8.self, capacity: self.length)
583+
let baseData = UnsafeBufferPointer<UInt8>(start: basePtr, count: self.length)[searchRange]
584+
let searchPtr = dataToFind.bytes.bindMemory(to: UInt8.self, capacity: dataToFind.length)
585+
let search = UnsafeBufferPointer<UInt8>(start: searchPtr, count: dataToFind.length)
586+
587+
let location : Int?
588+
let anchored = mask.contains(.anchored)
589+
if mask.contains(.backwards) {
590+
location = NSData.searchSubSequence(search.reversed(), inSequence: baseData.reversed(),anchored : anchored).map {$0.base-search.count}
591+
} else {
592+
location = NSData.searchSubSequence(search, inSequence: baseData,anchored : anchored)
593+
}
594+
return location.map {NSRange(location: $0, length: search.count)} ?? NSRange(location: NSNotFound, length: 0)
578595
}
579-
return location.map {NSRange(location: $0, length: search.count)} ?? NSRange(location: NSNotFound, length: 0)
580596
}
581597

582598
private static func searchSubSequence<T : Collection, T2 : Sequence>(_ subSequence : T2, inSequence seq: T,anchored : Bool) -> T.Index? where T.Iterator.Element : Equatable, T.Iterator.Element == T2.Iterator.Element {

Sources/Foundation/NSString.swift

Lines changed: 48 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1368,12 +1368,16 @@ extension NSString {
13681368
public convenience init(contentsOf url: URL, encoding enc: UInt) throws {
13691369
let readResult = try NSData(contentsOf: url, options: [])
13701370

1371-
let bytePtr = readResult.bytes.bindMemory(to: UInt8.self, capacity: readResult.length)
1372-
guard let cf = CFStringCreateWithBytes(kCFAllocatorDefault, bytePtr, readResult.length, CFStringConvertNSStringEncodingToEncoding(numericCast(enc)), true) else {
1373-
throw NSError(domain: NSCocoaErrorDomain, code: CocoaError.fileReadInapplicableStringEncoding.rawValue, userInfo: [
1374-
NSDebugDescriptionErrorKey : "Unable to create a string using the specified encoding."
1375-
])
1371+
let cf = try withExtendedLifetime(readResult) {
1372+
let bytePtr = readResult.bytes.bindMemory(to: UInt8.self, capacity: readResult.length)
1373+
guard let cf = CFStringCreateWithBytes(kCFAllocatorDefault, bytePtr, readResult.length, CFStringConvertNSStringEncodingToEncoding(numericCast(enc)), true) else {
1374+
throw NSError(domain: NSCocoaErrorDomain, code: CocoaError.fileReadInapplicableStringEncoding.rawValue, userInfo: [
1375+
NSDebugDescriptionErrorKey : "Unable to create a string using the specified encoding."
1376+
])
1377+
}
1378+
return cf
13761379
}
1380+
13771381
var str: String?
13781382
if String._conditionallyBridgeFromObjectiveC(cf._nsObject, result: &str) {
13791383
self.init(str!)
@@ -1393,42 +1397,47 @@ extension NSString {
13931397

13941398
let encoding: UInt
13951399
let offset: Int
1396-
// Look for a BOM (Byte Order Marker) to try and determine the text Encoding, this also skips
1397-
// over the bytes. This takes precedence over the textEncoding in the http header
1398-
let bytePtr = readResult.bytes.bindMemory(to: UInt8.self, capacity:readResult.length)
1399-
if readResult.length >= 4 && bytePtr[0] == 0xFF && bytePtr[1] == 0xFE && bytePtr[2] == 0x00 && bytePtr[3] == 0x00 {
1400-
encoding = String.Encoding.utf32LittleEndian.rawValue
1401-
offset = 4
1402-
}
1403-
else if readResult.length >= 2 && bytePtr[0] == 0xFE && bytePtr[1] == 0xFF {
1404-
encoding = String.Encoding.utf16BigEndian.rawValue
1405-
offset = 2
1406-
}
1407-
else if readResult.length >= 2 && bytePtr[0] == 0xFF && bytePtr[1] == 0xFE {
1408-
encoding = String.Encoding.utf16LittleEndian.rawValue
1409-
offset = 2
1410-
}
1411-
else if readResult.length >= 4 && bytePtr[0] == 0x00 && bytePtr[1] == 0x00 && bytePtr[2] == 0xFE && bytePtr[3] == 0xFF {
1412-
encoding = String.Encoding.utf32BigEndian.rawValue
1413-
offset = 4
1414-
}
1415-
else if let charSet = textEncodingNameMaybe, let textEncoding = String.Encoding(charSet: charSet) {
1416-
encoding = textEncoding.rawValue
1417-
offset = 0
1418-
} else {
1419-
//Need to work on more conditions. This should be the default
1420-
encoding = String.Encoding.utf8.rawValue
1421-
offset = 0
1422-
}
14231400

1424-
// Since the encoding being passed includes the byte order the BOM wont be checked or skipped, so pass offset to
1425-
// manually skip the BOM header.
1426-
guard let cf = CFStringCreateWithBytes(kCFAllocatorDefault, bytePtr + offset, readResult.length - offset,
1427-
CFStringConvertNSStringEncodingToEncoding(numericCast(encoding)), true) else {
1428-
throw NSError(domain: NSCocoaErrorDomain, code: CocoaError.fileReadInapplicableStringEncoding.rawValue, userInfo: [
1429-
NSDebugDescriptionErrorKey : "Unable to create a string using the specified encoding."
1430-
])
1401+
let cf = try withExtendedLifetime(readResult) {
1402+
// Look for a BOM (Byte Order Marker) to try and determine the text Encoding, this also skips
1403+
// over the bytes. This takes precedence over the textEncoding in the http header
1404+
let bytePtr = readResult.bytes.bindMemory(to: UInt8.self, capacity:readResult.length)
1405+
if readResult.length >= 4 && bytePtr[0] == 0xFF && bytePtr[1] == 0xFE && bytePtr[2] == 0x00 && bytePtr[3] == 0x00 {
1406+
encoding = String.Encoding.utf32LittleEndian.rawValue
1407+
offset = 4
1408+
}
1409+
else if readResult.length >= 2 && bytePtr[0] == 0xFE && bytePtr[1] == 0xFF {
1410+
encoding = String.Encoding.utf16BigEndian.rawValue
1411+
offset = 2
1412+
}
1413+
else if readResult.length >= 2 && bytePtr[0] == 0xFF && bytePtr[1] == 0xFE {
1414+
encoding = String.Encoding.utf16LittleEndian.rawValue
1415+
offset = 2
1416+
}
1417+
else if readResult.length >= 4 && bytePtr[0] == 0x00 && bytePtr[1] == 0x00 && bytePtr[2] == 0xFE && bytePtr[3] == 0xFF {
1418+
encoding = String.Encoding.utf32BigEndian.rawValue
1419+
offset = 4
1420+
}
1421+
else if let charSet = textEncodingNameMaybe, let textEncoding = String.Encoding(charSet: charSet) {
1422+
encoding = textEncoding.rawValue
1423+
offset = 0
1424+
} else {
1425+
//Need to work on more conditions. This should be the default
1426+
encoding = String.Encoding.utf8.rawValue
1427+
offset = 0
1428+
}
1429+
1430+
// Since the encoding being passed includes the byte order the BOM wont be checked or skipped, so pass offset to
1431+
// manually skip the BOM header.
1432+
guard let cf = CFStringCreateWithBytes(kCFAllocatorDefault, bytePtr + offset, readResult.length - offset,
1433+
CFStringConvertNSStringEncodingToEncoding(numericCast(encoding)), true) else {
1434+
throw NSError(domain: NSCocoaErrorDomain, code: CocoaError.fileReadInapplicableStringEncoding.rawValue, userInfo: [
1435+
NSDebugDescriptionErrorKey : "Unable to create a string using the specified encoding."
1436+
])
1437+
}
1438+
return cf
14311439
}
1440+
14321441
var str: String?
14331442
if String._conditionallyBridgeFromObjectiveC(cf._nsObject, result: &str) {
14341443
self.init(str!)

Sources/FoundationNetworking/NSURLRequest.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,8 +237,10 @@ open class NSURLRequest : NSObject, NSSecureCoding, NSCopying, NSMutableCopying
237237
aCoder.encode(self.cachePolicy.rawValue._bridgeToObjectiveC(), forKey: "NS._cachePolicy")
238238
aCoder.encode(self.timeoutInterval._bridgeToObjectiveC(), forKey: "NS._timeoutInterval")
239239
if let httpBody = self.httpBody?._bridgeToObjectiveC() {
240-
let bytePtr = httpBody.bytes.bindMemory(to: UInt8.self, capacity: httpBody.length)
241-
aCoder.encodeBytes(bytePtr, length: httpBody.length, forKey: "NS.httpBody")
240+
withExtendedLifetime(httpBody) {
241+
let bytePtr = httpBody.bytes.bindMemory(to: UInt8.self, capacity: httpBody.length)
242+
aCoder.encodeBytes(bytePtr, length: httpBody.length, forKey: "NS.httpBody")
243+
}
242244
}
243245
//On macOS input stream is not encoded.
244246
aCoder.encode(self.networkServiceType.rawValue._bridgeToObjectiveC(), forKey: "NS._networkServiceType")

Sources/FoundationNetworking/URLSession/NativeProtocol.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,9 @@ internal class _NativeProtocol: URLProtocol, _EasyHandleDelegate {
256256
if case .inMemory(let bodyData) = bodyDataDrain {
257257
var data = Data()
258258
if let body = bodyData {
259-
data = Data(bytes: body.bytes, count: body.length)
259+
withExtendedLifetime(body) {
260+
data = Data(bytes: body.bytes, count: body.length)
261+
}
260262
}
261263
self.client?.urlProtocol(self, didLoad: data)
262264
self.internalState = .taskCompleted

Sources/FoundationNetworking/URLSession/NetworkingSpecific.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ class _NSNonfileURLContentLoader: _NSNonfileURLContentLoading {
8585
switch statusCode {
8686
// These are the only valid response codes that data will be returned for, all other codes will be treated as error.
8787
case 101, 200...399, 401, 407:
88-
return (NSData(bytes: UnsafeMutableRawPointer(mutating: (data as NSData).bytes), length: data.count), urlResponse?.textEncodingName)
88+
return (data as NSData, urlResponse?.textEncodingName)
8989

9090
default:
9191
break

0 commit comments

Comments
 (0)