Skip to content

Commit a33fda9

Browse files
authored
Merge pull request #4590 from glessard/rdar93255079
Mitigate an out-of-bounds memory read
2 parents bd0355c + d8af9b6 commit a33fda9

File tree

2 files changed

+233
-14
lines changed

2 files changed

+233
-14
lines changed

Sources/Foundation/NSStringAPI.swift

Lines changed: 120 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the Swift.org open source project
44
//
5-
// Copyright (c) 2014 - 2018 Apple Inc. and the Swift project authors
5+
// Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors
66
// Licensed under Apache License v2.0 with Runtime Library Exception
77
//
88
// See https://swift.org/LICENSE.txt for license information
@@ -20,7 +20,7 @@
2020
// This file is shared between two projects:
2121
//
2222
// 1. https://github.com/apple/swift/tree/master/stdlib/public/Darwin/Foundation
23-
// 2. https://github.com/apple/swift-corelibs-foundation/tree/master/Foundation
23+
// 2. https://github.com/apple/swift-corelibs-foundation/tree/main/Foundation
2424
//
2525
// If you change this file, you must update it in both places.
2626

@@ -176,7 +176,7 @@ extension String {
176176
// + (instancetype)stringWithUTF8String:(const char *)bytes
177177

178178
/// Creates a string by copying the data from a given
179-
/// C array of UTF8-encoded bytes.
179+
/// null-terminated C array of UTF8-encoded bytes.
180180
public init?(utf8String bytes: UnsafePointer<CChar>) {
181181
if let str = String(validatingUTF8: bytes) {
182182
self = str
@@ -188,6 +188,54 @@ extension String {
188188
return nil
189189
}
190190
}
191+
192+
/// Creates a string by copying the data from a given
193+
/// null-terminated array of UTF8-encoded bytes.
194+
@_alwaysEmitIntoClient
195+
public init?(utf8String bytes: [CChar]) {
196+
// the stdlib's validatingUTF8 [CChar] overload checks for null termination.
197+
if let str = String(validatingUTF8: bytes) {
198+
self = str
199+
return
200+
}
201+
guard let nullPosition = bytes.firstIndex(of: 0) else {
202+
fatalError(
203+
"input of String.init(utf8String:) must be null-terminated"
204+
)
205+
}
206+
let ns = bytes.withUnsafeBytes {
207+
NSString(bytes: $0.baseAddress!,
208+
length: nullPosition,
209+
encoding: Encoding.utf8.rawValue)
210+
}
211+
guard let ns = ns else {
212+
return nil
213+
}
214+
self = String._unconditionallyBridgeFromObjectiveC(ns)
215+
}
216+
217+
@_alwaysEmitIntoClient
218+
@available(*, deprecated, message: "Use a copy of the String argument")
219+
public init?(utf8String bytes: String) {
220+
var decoded = bytes
221+
decoded.makeContiguousUTF8()
222+
if let null = decoded.firstIndex(of: "\0") {
223+
decoded = String(decoded[..<null])
224+
}
225+
self = decoded
226+
}
227+
228+
@_alwaysEmitIntoClient
229+
@available(*, deprecated, message: "Use String(_ scalar: Unicode.Scalar)")
230+
public init?(utf8String bytes: inout CChar) {
231+
// a byte interpreted as a buffer is valid only if the value is zero.
232+
guard bytes == 0 else {
233+
fatalError(
234+
"input of String.init(utf8String:) must be null-terminated"
235+
)
236+
}
237+
self = ""
238+
}
191239
}
192240

193241
extension String {
@@ -369,15 +417,16 @@ extension String {
369417
// initWithCString:(const char *)nullTerminatedCString
370418
// encoding:(NSStringEncoding)encoding
371419

372-
/// Produces a string containing the bytes in a given C array,
373-
/// interpreted according to a given encoding.
374-
public init?(
375-
cString: UnsafePointer<CChar>,
376-
encoding enc: Encoding
377-
) {
378-
if enc == .utf8, let str = String(validatingUTF8: cString) {
379-
self = str
380-
return
420+
/// Produces a string by copying the null-terminated bytes
421+
/// in a given C array, interpreted according to a given encoding.
422+
public init?(cString: UnsafePointer<CChar>, encoding enc: Encoding) {
423+
if enc == .utf8 || enc == .ascii {
424+
if let str = String(validatingUTF8: cString) {
425+
if enc == .utf8 || str._guts._isContiguousASCII {
426+
self = str
427+
return
428+
}
429+
}
381430
}
382431
if let ns = NSString(cString: cString, encoding: enc.rawValue) {
383432
self = String._unconditionallyBridgeFromObjectiveC(ns)
@@ -386,6 +435,64 @@ extension String {
386435
}
387436
}
388437

438+
/// Produces a string by copying the null-terminated bytes
439+
/// in a given array, interpreted according to a given encoding.
440+
@_alwaysEmitIntoClient
441+
public init?(cString: [CChar], encoding enc: Encoding) {
442+
if enc == .utf8 || enc == .ascii {
443+
// the stdlib's validatingUTF8 [CChar] overload checks for null termination.
444+
if let str = String(validatingUTF8: cString) {
445+
if enc == .utf8 || str._guts._isContiguousASCII {
446+
self = str
447+
return
448+
}
449+
}
450+
}
451+
guard let nullPosition = cString.firstIndex(of: 0) else {
452+
fatalError(
453+
"input of String.init(cString:encoding:) must be null-terminated"
454+
)
455+
}
456+
let ns = cString.withUnsafeBytes {
457+
NSString(bytes: $0.baseAddress!,
458+
length: nullPosition,
459+
encoding: enc.rawValue)
460+
}
461+
guard let ns = ns else {
462+
return nil
463+
}
464+
self = String._unconditionallyBridgeFromObjectiveC(ns)
465+
}
466+
467+
@_alwaysEmitIntoClient
468+
@available(*, deprecated, message: "Use a copy of the String argument")
469+
public init?(cString: String, encoding enc: Encoding) {
470+
if enc == .utf8 || enc == .ascii {
471+
var decoded = cString
472+
decoded.makeContiguousUTF8()
473+
if let null = decoded.firstIndex(of: "\0") {
474+
decoded = String(decoded[..<null])
475+
}
476+
if enc == .utf8 || decoded.utf8.allSatisfy({ $0 < 128 }) {
477+
self = decoded
478+
return
479+
}
480+
}
481+
return nil
482+
}
483+
484+
@_alwaysEmitIntoClient
485+
@available(*, deprecated, message: "Use String(_ scalar: Unicode.Scalar)")
486+
public init?(cString: inout CChar, encoding enc: Encoding) {
487+
// a byte interpreted as a buffer is valid only if the value is zero.
488+
guard cString == 0 else {
489+
fatalError(
490+
"input of String.init(cString:encoding:) must be null-terminated"
491+
)
492+
}
493+
self = ""
494+
}
495+
389496
// FIXME: handle optional locale with default arguments
390497

391498
// - (instancetype)
@@ -463,7 +570,7 @@ extension String {
463570
}
464571
}
465572

466-
extension StringProtocol where Index == String.Index {
573+
extension StringProtocol {
467574
//===--- Bridging Helpers -----------------------------------------------===//
468575
//===--------------------------------------------------------------------===//
469576

Tests/Foundation/Tests/TestNSString.swift

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// This source file is part of the Swift.org open source project
22
//
3-
// Copyright (c) 2014 - 2016 Apple Inc. and the Swift project authors
3+
// Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors
44
// Licensed under Apache License v2.0 with Runtime Library Exception
55
//
66
// See http://swift.org/LICENSE.txt for license information
@@ -1644,6 +1644,112 @@ class TestNSString: LoopbackServerTest {
16441644
XCTAssertEqual(String(ns), "Test")
16451645
}
16461646

1647+
func test_initString_utf8StringWithArrayInput() {
1648+
var source: [CChar] = [0x61, 0x62, 0, 0x63]
1649+
var str: String?
1650+
str = String(utf8String: source)
1651+
XCTAssertNotNil(str)
1652+
source.withUnsafeBufferPointer {
1653+
XCTAssertEqual(str, String(utf8String: $0.baseAddress!))
1654+
}
1655+
// substitute a value not valid in UTF-8
1656+
source[1] = CChar(bitPattern: 0xff)
1657+
str = String(utf8String: source)
1658+
XCTAssertNil(str)
1659+
}
1660+
1661+
@available(*, deprecated) // silence the deprecation warning within
1662+
func test_initString_utf8StringWithStringInput() {
1663+
let source = "ab\0c"
1664+
var str: String?
1665+
str = String(utf8String: source)
1666+
XCTAssertNotNil(str)
1667+
source.withCString {
1668+
XCTAssertEqual(str, String(utf8String: $0))
1669+
}
1670+
str = String(utf8String: "")
1671+
XCTAssertNotNil(str)
1672+
XCTAssertEqual(str?.isEmpty, true)
1673+
}
1674+
1675+
@available(*, deprecated) // silence the deprecation warning within
1676+
func test_initString_utf8StringWithInoutConversion() {
1677+
var c = CChar.zero
1678+
var str: String?
1679+
str = String(utf8String: &c)
1680+
// Any other value of `c` would violate the null-terminated precondition
1681+
XCTAssertNotNil(str)
1682+
XCTAssertEqual(str?.isEmpty, true)
1683+
}
1684+
1685+
func test_initString_cStringWithArrayInput() {
1686+
var source: [CChar] = [0x61, 0x62, 0, 0x63]
1687+
var str: String?
1688+
str = String(cString: source, encoding: .utf8)
1689+
XCTAssertNotNil(str)
1690+
source.withUnsafeBufferPointer {
1691+
XCTAssertEqual(
1692+
str, String(cString: $0.baseAddress!, encoding: .utf8)
1693+
)
1694+
}
1695+
str = String(cString: source, encoding: .ascii)
1696+
XCTAssertNotNil(str)
1697+
source.withUnsafeBufferPointer {
1698+
XCTAssertEqual(
1699+
str, String(cString: $0.baseAddress!, encoding: .ascii)
1700+
)
1701+
}
1702+
str = String(cString: source, encoding: .macOSRoman)
1703+
XCTAssertNotNil(str)
1704+
source.withUnsafeBufferPointer {
1705+
XCTAssertEqual(
1706+
str, String(cString: $0.baseAddress!, encoding: .macOSRoman)
1707+
)
1708+
}
1709+
// substitute a value not valid in UTF-8
1710+
source[1] = CChar(bitPattern: 0xff)
1711+
str = String(cString: source, encoding: .utf8)
1712+
XCTAssertNil(str)
1713+
str = String(cString: source, encoding: .macOSRoman)
1714+
XCTAssertNotNil(str)
1715+
source.withUnsafeBufferPointer {
1716+
XCTAssertEqual(
1717+
str, String(cString: $0.baseAddress!, encoding: .macOSRoman)
1718+
)
1719+
}
1720+
}
1721+
1722+
@available(*, deprecated) // silence the deprecation warning within
1723+
func test_initString_cStringWithStringInput() {
1724+
let source = "ab\0c"
1725+
var str: String?
1726+
str = String(cString: source, encoding: .utf8)
1727+
XCTAssertNotNil(str)
1728+
source.withCString {
1729+
XCTAssertEqual(str, String(cString: $0, encoding: .utf8))
1730+
}
1731+
str = String(cString: source, encoding: .ascii)
1732+
XCTAssertNotNil(str)
1733+
source.withCString {
1734+
XCTAssertEqual(str, String(cString: $0, encoding: .ascii))
1735+
}
1736+
str = String(cString: "", encoding: .utf8)
1737+
XCTAssertNotNil(str)
1738+
XCTAssertEqual(str?.isEmpty, true)
1739+
str = String(cString: "Caractères", encoding: .ascii)
1740+
XCTAssertNil(str)
1741+
}
1742+
1743+
@available(*, deprecated) // silence the deprecation warning within
1744+
func test_initString_cStringWithInoutConversion() {
1745+
var c = CChar.zero
1746+
var str: String?
1747+
str = String(cString: &c, encoding: .ascii)
1748+
// Any other value of `c` would violate the null-terminated precondition
1749+
XCTAssertNotNil(str)
1750+
XCTAssertEqual(str?.isEmpty, true)
1751+
}
1752+
16471753
static var allTests: [(String, (TestNSString) -> () throws -> Void)] {
16481754
var tests = [
16491755
("test_initData", test_initData),
@@ -1718,6 +1824,12 @@ class TestNSString: LoopbackServerTest {
17181824
("test_enumerateSubstrings", test_enumerateSubstrings),
17191825
("test_paragraphRange", test_paragraphRange),
17201826
("test_initStringWithNSString", test_initStringWithNSString),
1827+
("test_initString_utf8StringWithArrayInput", test_initString_utf8StringWithArrayInput),
1828+
("test_initString_utf8StringWithStringInput", test_initString_utf8StringWithStringInput),
1829+
("test_initString_utf8StringWithInoutConversion", test_initString_utf8StringWithInoutConversion),
1830+
("test_initString_cStringWithArrayInput", test_initString_cStringWithArrayInput),
1831+
("test_initString_cStringWithStringInput", test_initString_cStringWithStringInput),
1832+
("test_initString_cStringWithInoutConversion", test_initString_cStringWithInoutConversion),
17211833
]
17221834

17231835
#if !os(Windows)

0 commit comments

Comments
 (0)