Skip to content

[stdlib] Review and fix some problems with unsafe buffer and Range initialization #34961

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 18 additions & 8 deletions stdlib/public/core/ClosedRange.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,14 @@ public struct ClosedRange<Bound: Comparable> {
/// The range's upper bound.
public let upperBound: Bound

// This works around _debugPrecondition() impacting the performance of
// optimized code. (rdar://72246338)
@_alwaysEmitIntoClient @inline(__always)
internal init(_uncheckedBounds bounds: (lower: Bound, upper: Bound)) {
self.lowerBound = bounds.lower
self.upperBound = bounds.upper
}

/// Creates an instance with the given bounds.
///
/// Because this initializer does not perform any checks, it should be used
Expand All @@ -78,8 +86,9 @@ public struct ClosedRange<Bound: Comparable> {
/// - Parameter bounds: A tuple of the lower and upper bounds of the range.
@inlinable
public init(uncheckedBounds bounds: (lower: Bound, upper: Bound)) {
self.lowerBound = bounds.lower
self.upperBound = bounds.upper
_debugPrecondition(bounds.lower <= bounds.upper,
"ClosedRange requires lowerBound <= upperBound")
self.init(_uncheckedBounds: (lower: bounds.lower, upper: bounds.upper))
}
}

Expand All @@ -100,8 +109,9 @@ extension ClosedRange: RangeExpression {
public func relative<C: Collection>(to collection: C) -> Range<Bound>
where C.Index == Bound {
return Range(
uncheckedBounds: (
lower: lowerBound, upper: collection.index(after: self.upperBound)))
_uncheckedBounds: (
lower: lowerBound,
upper: collection.index(after: self.upperBound)))
}

/// Returns a Boolean value indicating whether the given element is contained
Expand Down Expand Up @@ -336,7 +346,7 @@ extension Comparable {
public static func ... (minimum: Self, maximum: Self) -> ClosedRange<Self> {
_precondition(
minimum <= maximum, "Range requires lowerBound <= upperBound")
return ClosedRange(uncheckedBounds: (lower: minimum, upper: maximum))
return ClosedRange(_uncheckedBounds: (lower: minimum, upper: maximum))
}
}

Expand Down Expand Up @@ -423,7 +433,7 @@ extension ClosedRange {
limits.upperBound < self.upperBound ? limits.upperBound
: limits.lowerBound > self.upperBound ? limits.lowerBound
: self.upperBound
return ClosedRange(uncheckedBounds: (lower: lower, upper: upper))
return ClosedRange(_uncheckedBounds: (lower: lower, upper: upper))
}
}

Expand All @@ -439,7 +449,7 @@ extension ClosedRange where Bound: Strideable, Bound.Stride: SignedInteger {
public init(_ other: Range<Bound>) {
_precondition(!other.isEmpty, "Can't form an empty closed range")
let upperBound = other.upperBound.advanced(by: -1)
self.init(uncheckedBounds: (lower: other.lowerBound, upper: upperBound))
self.init(_uncheckedBounds: (lower: other.lowerBound, upper: upperBound))
}
}

Expand Down Expand Up @@ -476,7 +486,7 @@ extension ClosedRange: Decodable where Bound: Decodable {
codingPath: decoder.codingPath,
debugDescription: "Cannot initialize \(ClosedRange.self) with a lowerBound (\(lowerBound)) greater than upperBound (\(upperBound))"))
}
self.init(uncheckedBounds: (lower: lowerBound, upper: upperBound))
self.init(_uncheckedBounds: (lower: lowerBound, upper: upperBound))
}
}

Expand Down
23 changes: 16 additions & 7 deletions stdlib/public/core/Range.swift
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,14 @@ public struct Range<Bound: Comparable> {
/// instance does not contain its upper bound.
public let upperBound: Bound

// This works around _debugPrecondition() impacting the performance of
// optimized code. (rdar://72246338)
@_alwaysEmitIntoClient @inline(__always)
internal init(_uncheckedBounds bounds: (lower: Bound, upper: Bound)) {
self.lowerBound = bounds.lower
self.upperBound = bounds.upper
}

/// Creates an instance with the given bounds.
///
/// Because this initializer does not perform any checks, it should be used
Expand All @@ -167,8 +175,9 @@ public struct Range<Bound: Comparable> {
/// - Parameter bounds: A tuple of the lower and upper bounds of the range.
@inlinable
public init(uncheckedBounds bounds: (lower: Bound, upper: Bound)) {
self.lowerBound = bounds.lower
self.upperBound = bounds.upper
_debugPrecondition(bounds.lower <= bounds.upper,
"Range requires lowerBound <= upperBound")
self.init(_uncheckedBounds: (lower: bounds.lower, upper: bounds.upper))
}

/// Returns a Boolean value indicating whether the given element is contained
Expand Down Expand Up @@ -308,7 +317,7 @@ extension Range where Bound: Strideable, Bound.Stride: SignedInteger {
/// require an upper bound of `Int.max + 1`, which is not representable as
public init(_ other: ClosedRange<Bound>) {
let upperBound = other.upperBound.advanced(by: 1)
self.init(uncheckedBounds: (lower: other.lowerBound, upper: upperBound))
self.init(_uncheckedBounds: (lower: other.lowerBound, upper: upperBound))
}
}

Expand All @@ -325,7 +334,7 @@ extension Range: RangeExpression {
@inlinable // trivial-implementation
public func relative<C: Collection>(to collection: C) -> Range<Bound>
where C.Index == Bound {
return Range(uncheckedBounds: (lower: lowerBound, upper: upperBound))
self
}
}

Expand Down Expand Up @@ -359,7 +368,7 @@ extension Range {
limits.upperBound < self.upperBound ? limits.upperBound
: limits.lowerBound > self.upperBound ? limits.lowerBound
: self.upperBound
return Range(uncheckedBounds: (lower: lower, upper: upper))
return Range(_uncheckedBounds: (lower: lower, upper: upper))
}
}

Expand Down Expand Up @@ -435,7 +444,7 @@ extension Range: Decodable where Bound: Decodable {
codingPath: decoder.codingPath,
debugDescription: "Cannot initialize \(Range.self) with a lowerBound (\(lowerBound)) greater than upperBound (\(upperBound))"))
}
self.init(uncheckedBounds: (lower: lowerBound, upper: upperBound))
self.init(_uncheckedBounds: (lower: lowerBound, upper: upperBound))
}
}

Expand Down Expand Up @@ -729,7 +738,7 @@ extension Comparable {
public static func ..< (minimum: Self, maximum: Self) -> Range<Self> {
_precondition(minimum <= maximum,
"Range requires lowerBound <= upperBound")
return Range(uncheckedBounds: (lower: minimum, upper: maximum))
return Range(_uncheckedBounds: (lower: minimum, upper: maximum))
}

/// Returns a partial range up to, but not including, its upper bound.
Expand Down
2 changes: 1 addition & 1 deletion stdlib/public/core/SmallString.swift
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ extension _SmallString {
// Restore the memory type of self._storage
_ = rawPtr.bindMemory(to: RawBitPattern.self, capacity: 1)
}
return try f(UnsafeBufferPointer(start: ptr, count: count))
return try f(UnsafeBufferPointer(_uncheckedStart: ptr, count: count))
}
}

Expand Down
1 change: 1 addition & 0 deletions stdlib/public/core/String.swift
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,7 @@ extension String {
contigBytes._providesContiguousBytesNoCopy
{
self = contigBytes.withUnsafeBytes { rawBufPtr in
Builtin.onFastPath() // encourage SIL Optimizer to inline this closure
return String._fromUTF8Repairing(
UnsafeBufferPointer(
start: rawBufPtr.baseAddress?.assumingMemoryBound(to: UInt8.self),
Expand Down
2 changes: 1 addition & 1 deletion stdlib/public/core/StringObject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -902,7 +902,7 @@ extension _StringObject {
return sharedUTF8
}
return UnsafeBufferPointer(
start: self.nativeUTF8Start, count: self.largeCount)
_uncheckedStart: self.nativeUTF8Start, count: self.largeCount)
}

// Whether the object stored can be bridged directly as a NSString
Expand Down
2 changes: 1 addition & 1 deletion stdlib/public/core/StringProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ extension StringProtocol {
let end = endIndex
_internalInvariant(
start.transcodedOffset == 0 && end.transcodedOffset == 0)
return Range(uncheckedBounds: (start._encodedOffset, end._encodedOffset))
return Range(_uncheckedBounds: (start._encodedOffset, end._encodedOffset))
}
}

Expand Down
2 changes: 1 addition & 1 deletion stdlib/public/core/StringStorageBridge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ extension _AbstractStringStorage {
"Range out of bounds")

let range = Range(
uncheckedBounds: (aRange.location, aRange.location+aRange.length))
_uncheckedBounds: (aRange.location, aRange.location+aRange.length))
let str = asString
str._copyUTF16CodeUnits(
into: UnsafeMutableBufferPointer(start: buffer, count: range.count),
Expand Down
11 changes: 6 additions & 5 deletions stdlib/public/core/Substring.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ public struct Substring {

self._slice = Slice(
base: slice.base,
bounds: Range(uncheckedBounds: (start, end)))
bounds: Range(_uncheckedBounds: (start, end)))
_invariantCheck()
}

Expand All @@ -132,7 +132,7 @@ extension Substring {
@inlinable @inline(__always)
internal var _offsetRange: Range<Int> {
return Range(
uncheckedBounds: (startIndex._encodedOffset, endIndex._encodedOffset))
_uncheckedBounds: (startIndex._encodedOffset, endIndex._encodedOffset))
}

#if !INTERNAL_CHECKS_ENABLED
Expand Down Expand Up @@ -322,7 +322,8 @@ extension Substring: CustomDebugStringConvertible {

extension Substring: LosslessStringConvertible {
public init(_ content: String) {
self = content[...]
let range = Range(_uncheckedBounds: (content.startIndex, content.endIndex))
self.init(Slice(base: content, bounds: range))
}
}

Expand Down Expand Up @@ -727,14 +728,14 @@ extension Substring: RangeReplaceableCollection {
public init<S: Sequence>(_ elements: S)
where S.Element == Character {
if let str = elements as? String {
self = str[...]
self.init(str)
return
}
if let subStr = elements as? Substring {
self = subStr
return
}
self = String(elements)[...]
self.init(String(elements))
}

@inlinable // specialize
Expand Down
38 changes: 33 additions & 5 deletions stdlib/public/core/UnsafeBufferPointer.swift.gyb
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,14 @@ extension Unsafe${Mutable}BufferPointer: ${Mutable}Collection, RandomAccessColle
// optimizer is not capable of creating partial specializations yet.
// NOTE: Range checks are not performed here, because it is done later by
// the subscript function.
return end - start
// NOTE: We allow the subtraction to silently overflow in release builds
// to eliminate a superflous check when `start` and `end` are both valid
// indices. (The operation can only overflow if `start` is negative, which
// implies it's an invalid index.) `Collection` does not specify what
// `distance` should return when given an invalid index pair.
let result = end.subtractingReportingOverflow(start)
_debugPrecondition(!result.overflow)
return result.partialValue
}

@inlinable // unsafe-performance
Expand Down Expand Up @@ -387,6 +394,17 @@ extension Unsafe${Mutable}BufferPointer: ${Mutable}Collection, RandomAccessColle
}

extension Unsafe${Mutable}BufferPointer {
// This works around _debugPrecondition() impacting the performance of
// optimized code. (rdar://72246338)
@_alwaysEmitIntoClient
internal init(
@_nonEphemeral _uncheckedStart start: Unsafe${Mutable}Pointer<Element>?,
count: Int
) {
_position = start
self.count = count
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this also be @inlinable? I don't think AEIC implies that, but I don't recall the details.

What is @_nonEphemeral?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@_aeic does imply @inlinable -- indeed, it is a stronger variant of it.

The @_nonEphemeral attribute indicates that the associated function escapes the pointer. The compiler emits a warning when a temporary pointer (such as the ones resulting from the implicit inout-to-pointer conversion feature) is passed for a parameter that has this attribute. It's a nice (if limited) safety feature for a common issue.

/// Creates a new buffer pointer over the specified number of contiguous
/// instances beginning at the given pointer.
///
Expand All @@ -401,13 +419,12 @@ extension Unsafe${Mutable}BufferPointer {
public init(
@_nonEphemeral start: Unsafe${Mutable}Pointer<Element>?, count: Int
) {
_precondition(
_debugPrecondition(
count >= 0, "Unsafe${Mutable}BufferPointer with negative count")
_precondition(
_debugPrecondition(
count == 0 || start != nil,
"Unsafe${Mutable}BufferPointer has a nil start and nonzero count")
_position = start
self.count = count
self.init(_uncheckedStart: start, count: _assumeNonNegative(count))
}

@inlinable // unsafe-performance
Expand Down Expand Up @@ -498,6 +515,17 @@ extension Unsafe${Mutable}BufferPointer {
/// - Parameter slice: The buffer slice to rebase.
@inlinable // unsafe-performance
public init(rebasing slice: Slice<UnsafeBufferPointer<Element>>) {
// NOTE: `Slice` does not guarantee that its start/end indices are valid
// in `base` -- it merely ensures that `startIndex <= endIndex`.
// We need manually check that we aren't given an invalid slice,
// or the resulting collection would allow access that was
// out-of-bounds with respect to the original base buffer.
// We only do this in debug builds to prevent a measurable performance
// degradation wrt passing around pointers not wrapped in a BufferPointer
// construct.
_debugPrecondition(
slice.startIndex >= 0 && slice.endIndex <= slice.base.count,
"Invalid slice")
let base = slice.base.baseAddress?.advanced(by: slice.startIndex)
let count = slice.endIndex &- slice.startIndex
self.init(start: base, count: count)
Expand Down
19 changes: 15 additions & 4 deletions stdlib/public/core/UnsafeRawBufferPointer.swift.gyb
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ extension Unsafe${Mutable}RawBufferPointer: ${Mutable}Collection {
@inlinable
public var count: Int {
if let pos = _position {
return _end! - pos
return _assumeNonNegative(_end! - pos)
}
return 0
}
Expand Down Expand Up @@ -432,11 +432,11 @@ extension Unsafe${Mutable}RawBufferPointer {
public init(
@_nonEphemeral start: Unsafe${Mutable}RawPointer?, count: Int
) {
_precondition(count >= 0, "${Self} with negative count")
_precondition(count == 0 || start != nil,
_debugPrecondition(count >= 0, "${Self} with negative count")
_debugPrecondition(count == 0 || start != nil,
"${Self} has a nil start and nonzero count")
_position = start
_end = start.map { $0 + count }
_end = start.map { $0 + _assumeNonNegative(count) }
}

/// Creates a new buffer over the same memory as the given buffer.
Expand Down Expand Up @@ -511,6 +511,17 @@ extension Unsafe${Mutable}RawBufferPointer {
/// - Parameter slice: The raw buffer slice to rebase.
@inlinable
public init(rebasing slice: Slice<UnsafeRawBufferPointer>) {
// NOTE: `Slice` does not guarantee that its start/end indices are valid
// in `base` -- it merely ensures that `startIndex <= endIndex`.
// We need manually check that we aren't given an invalid slice,
// or the resulting collection would allow access that was
// out-of-bounds with respect to the original base buffer.
// We only do this in debug builds to prevent a measurable performance
// degradation wrt passing around pointers not wrapped in a BufferPointer
// construct.
_debugPrecondition(
slice.startIndex >= 0 && slice.endIndex <= slice.base.count,
"Invalid slice")
let base = slice.base.baseAddress?.advanced(by: slice.startIndex)
let count = slice.endIndex &- slice.startIndex
self.init(start: base, count: count)
Expand Down
18 changes: 18 additions & 0 deletions test/stdlib/RangeTraps.swift
Original file line number Diff line number Diff line change
Expand Up @@ -116,5 +116,23 @@ RangeTraps.test("throughNaN")
_ = ...Double.nan
}

RangeTraps.test("UncheckedHalfOpen")
.xfail(.custom(
{ !_isDebugAssertConfiguration() },
reason: "assertions are disabled in Release and Unchecked mode"))
.code {
expectCrashLater()
var range = Range(uncheckedBounds: (lower: 1, upper: 0))
}

RangeTraps.test("UncheckedClosed")
.xfail(.custom(
{ !_isDebugAssertConfiguration() },
reason: "assertions are disabled in Release and Unchecked mode"))
.code {
expectCrashLater()
var range = ClosedRange(uncheckedBounds: (lower: 1, upper: 0))
}

runAllTests()

4 changes: 2 additions & 2 deletions validation-test/stdlib/ArrayTraps.swift.gyb
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,8 @@ ${ArrayTy}Traps.test("${'remove(at:)/%s' % index}")

ArrayTraps.test("unsafeLength")
.skip(.custom(
{ _isFastAssertConfiguration() },
reason: "this trap is not guaranteed to happen in -Ounchecked"))
{ !_isDebugAssertConfiguration() },
reason: "this trap is not guaranteed to happen in -O or -Ounchecked"))
.crashOutputMatches(_isDebugAssertConfiguration() ?
"UnsafeBufferPointer with negative count" : "")
.code {
Expand Down
Loading