Skip to content

Commit adc807c

Browse files
authored
Merge pull request #2680 from DougGregor/se-0458-safe-subsume-args
SE-0458: Revisions based on review discussion
2 parents 4c7c8a3 + 15f57c7 commit adc807c

File tree

1 file changed

+113
-2
lines changed

1 file changed

+113
-2
lines changed

proposals/0458-strict-memory-safety.md

Lines changed: 113 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* Feature name: `StrictMemorySafety`
88
* Vision: [Opt-in Strict Memory Safety Checking (Prospective)](https://github.com/swiftlang/swift-evolution/pull/2581)
99
* Implementation: On main with experimental feature flags `AllowUnsafeAttribute` and `WarnUnsafe`
10+
* Previous Revision: [1](https://github.com/swiftlang/swift-evolution/blob/f2cab4ddc3381d1dc7a970e813ed29e27b5ae43f/proposals/0458-strict-memory-safety.md)
1011
* Review: ([pitch](https://forums.swift.org/t/pitch-opt-in-strict-memory-safety-checking/76689)) ([review](https://forums.swift.org/t/se-0458-opt-in-strict-memory-safety-checking/77274))
1112

1213
## Introduction
@@ -125,7 +126,7 @@ extension UnsafeMutableBufferPointer {
125126
@unsafe public func swapAt(_ i: Index, _ j: Index) {
126127
guard i != j else { return }
127128
precondition(i >= 0 && j >= 0)
128-
precondition(unsafe i < endIndex && j < endIndex)
129+
precondition(i < endIndex && j < endIndex)
129130
let pi = unsafe (baseAddress! + i)
130131
let pj = unsafe (baseAddress! + j)
131132
let tmp = unsafe pi.move()
@@ -217,7 +218,7 @@ There are a few exemptions to the rule that any unsafe constructs within the sig
217218

218219
### `@safe` attribute
219220

220-
Like the `@unsafe` attribute, the `@safe` attribute ise used on declarations whose signatures involve unsafe types. However, the `@safe` attribute means that the declaration is consider safe to use even though its signature includes unsafe types. For example, marking `UnsafeBufferPointer` as `@unsafe` means that all operations involving an unsafe buffer pointer are implicitly considered `@unsafe`. The `@safe` attribute can be used to say that those particular operations are actually safe. For example, any operation involving buffer indices or count are safe, because they don't touch the memory itself. This can be indicated by marking these APIs `@safe`:
221+
Like the `@unsafe` attribute, the `@safe` attribute is used on declarations whose signatures involve unsafe types. However, the `@safe` attribute means that the declaration is considered safe to use even though its signature includes unsafe types. For example, marking `UnsafeBufferPointer` as `@unsafe` means that all operations involving an unsafe buffer pointer are implicitly considered `@unsafe`. The `@safe` attribute can be used to say that those particular operations are actually safe. For example, any operation involving buffer indices or count are safe, because they don't touch the memory itself. This can be indicated by marking these APIs `@safe`:
221222

222223
```swift
223224
extension UnsafeBufferPointer {
@@ -249,6 +250,20 @@ extension Array<Int> {
249250
}
250251
```
251252

253+
The `@safe` annotation on a declaration takes responsibility for any variables of unsafe type that are used as its direct arguments (including the `self`). If such a variable is used to access a `@safe` property or subscript, or in a function call to a `@safe` function, it will not be diagnosed as unsafe:
254+
255+
```swift
256+
extension Array<Int> {
257+
func sum() -> Int {
258+
withUnsafeBufferPointer { buffer in
259+
let count = buffer.count // count is `@safe`, no diagnostic even though 'buffer' has unsafe type
260+
let address = buffer.baseAddress // warning: 'buffer' and 'baseAddress' are both unsafe
261+
c_library_sum_function(address, count, 0) // warning: 'c_library_sum_function' and 'address' are both unsafe
262+
}
263+
}
264+
}
265+
```
266+
252267
### `unsafe` expression
253268

254269
When a declaration is marked `@unsafe`, it is free to use any other unsafe types as part of its interface. Any time there is executable code that makes use of unsafe constructs, that code must be within an `unsafe` expression or it will receive a diagnostic about uses of unsafe code. In the example from the previous section, `wrapper` can be marked as `@unsafe` to suppress diagnostics by explicitly propagating unsafety to their clients:
@@ -317,6 +332,35 @@ There are a number of compiler flags that intentionally disable some safety-rela
317332
* `-strict-concurrency=` for anything other than "complete", because the memory safety model requires strict concurrency to eliminate thread safety issues.
318333
* `-disable-access-control`, which allows one to break invariants of a type that can lead to memory-safety issues, such as breaking the invariant of `Range` that the lower bound not exceed the upper bound.
319334

335+
### Types with unsafe storage
336+
337+
Types that wrap unsafe types will often encapsulate the unsafe behavior to provide safe interfaces. However, this requires deliberate design and implementation, potentially involving adding specific preconditions. When strict safety checking is enabled, a type whose storage is unsafe will be diagnosed as involving unsafe code. This diagnostic can be suppressed by marking the type as `@safe` or `@unsafe`, in the same manner as any other declaration that has unsafe types or conformances in its signature:
338+
339+
```swift
340+
// @safe is required to suppress a diagnostic about the 'buffer' property's use
341+
// of an unsafe type.
342+
@safe
343+
struct ImmortalBufferWrapper<Element> : Collection {
344+
let buffer: UnsafeBufferPointer<Element>
345+
346+
@unsafe init(_ withImmortalBuffer: UnsafeBufferPointer<Element>) {
347+
self.buffer = unsafe buffer
348+
}
349+
350+
subscript(index: Index) -> Element {
351+
precondition(index >= 0 && index < buffer.count)
352+
return unsafe buffer[index]
353+
}
354+
355+
/* Also: Index, startIndex, endIndex, index(after:) */
356+
}
357+
```
358+
359+
A type has unsafe storage if:
360+
361+
* Any stored instance property (for `actor`, `class`, and `struct` types) or associated value (for cases of `enum` types) have a type that involves an unsafe type or conformance.
362+
* Any stored instance property uses one of the unsafe language features (such as `unowned(unsafe)`).
363+
320364
### Unsafe overrides
321365

322366
Overriding a safe method within an `@unsafe` one could introduce unsafety, so it will produce a diagnostic in the strict safety mode:
@@ -468,8 +512,67 @@ We have several options here:
468512
if case unsafe .rawOffsetIntoGlobalArray(let offset) = weirdAddress { ... }
469513
```
470514

515+
### Handling unsafe code in macro expansions
516+
517+
A macro can expand to any code. If the macro-expanded code contains uses of unsafe constructs not properly covered by `@safe`, `@unsafe`, or an `unsafe` expression within the macro, then strict safety checking will diagnose those safety issues within the macro expansion. In this case, the client of the macro does not have any way to suppress diagnostics within the macro expansion itself without modifying the implementation of the macro.
518+
519+
There are a number of possible approaches that one could use for suppression. The `unsafe` expression could be made to apply to everything in the macro expansion, which would also require some spelling for attached attributes and other places where expressions aren't permitted. Alternatively, Swift could introduce a general syntax for suppressing a class of warnings within a block of code, and that could be used to surround the macro expansion.
520+
521+
Note that both of these approaches trade away some of the benefits of the strict safety mode for the convenience of suppressing safety-related diagnostics.
522+
471523
## Alternatives considered
472524

525+
### Prohibiting unsafe conformances and overrides entirely
526+
527+
This proposal introduces two places where polymorphism interacts with unsafety: protocol conformances and overrides. In both cases, a safe abstraction (e.g., a superclass or protocol) has a specific implementation that is unsafe, and there is a way to note the unsafety:
528+
529+
* When overriding a safe declaration with an unsafe one, the overriding subclass must be marked `@unsafe`.
530+
* When implementing a safe protocol requirement with an unsafe declaration, the corresponding conformance must be marked `@unsafe`.
531+
532+
In both cases, the current proposal will consider uses of the type (in the overriding case) or conformance (for that case) as unsafe, respectively. However, that unsafety is not localized, because code that's generally safe can now cause safety problems when calling through polymorphic operations. For example, consider a function that operates on a general collection:
533+
534+
```swift
535+
func parse(_ input: some Collection<UInt8>) -> ParseResult
536+
```
537+
538+
Calling this function with an unsafe buffer pointer will produce a diagnostic due to the use of the unsafe conformance of `UnsafeBufferPointer` to `Collection`:
539+
540+
```swift
541+
let result = parse(unsafeBufferPointer) // warning: use of unsafe conformance
542+
```
543+
544+
Marking the call as `unsafe` will address the diagnostic. However, because `UnsafeBufferPointer` doesn't perform bounds checking, the `parse` function itself can introduce a memory safety problem if it subscripts into the collection with an invalid index. There isn't a way to communicate how the code that is `unsafe` is addressing memory safety issues within the context of the call.
545+
546+
This proposal could prohibit use of unsafe conformances and overrides entirely, for example by making it impossible to suppress the diagnostics associated with their definition and use. This would require the `parse(unsafeBufferPointer)` call to be refactored to avoid the unsafe conformance, for example by introducing a wrapper type:
547+
548+
```swift
549+
@safe struct ImmortalBufferWrapper<Element> : Collection {
550+
let buffer: UnsafeBufferPointer<Element>
551+
552+
@unsafe init(_ withImmortalBuffer: UnsafeBufferPointer<Element>) {
553+
self.buffer = unsafe buffer
554+
}
555+
556+
subscript(index: Index) -> Element {
557+
precondition(index >= 0 && index < buffer.count)
558+
return unsafe buffer[index]
559+
}
560+
561+
/* Also: Index, startIndex, endIndex, index(after:) */
562+
}
563+
```
564+
565+
The call would then look like this:
566+
567+
```swift
568+
let wrapper = unsafe ImmortalBufferWrapper(withImmortalBuffer: buffer)
569+
let result = parse(wrapper)
570+
```
571+
572+
This approach is better than the prior one: it improves bounds safety by introducing bounds checking. It clearly documents the assumptions made around lifetime safety. It is both functionally safer (due to bounds checks) and makes it easier to reason that the `unsafe` is correctly used. It does require a lot more code, and the code itself requires careful reasoning about safety (e.g., the right preconditions for bounds checking; the right naming to capture the lifetime implications).
573+
574+
Unsafe conformances and overrides remain part of this proposal because prohibiting them doesn't fundamentally change the safety model. Rather, it requires the introduction of more abstractions that could be safer--or could just be boilerplate. Swift has a number of constructs that are functionally similar to unsafe conformances, where safety checking can be disabled locally despite that having wide-ranging consequences: `@unchecked Sendable`, `nonisolated(unsafe)`, `unowned(unsafe)`, and `@preconcurrency` all fall into this category.
575+
473576
### `@unsafe` implying `unsafe` throughout a function body
474577

475578
A function marked `@unsafe` is unsafe to use, so any clients that have enabled strict safety checking will need to put uses of the function into an `unsafe` expression. The implementation of that function is likely to use unsafe code (possibly a lot of it), which could result in a large number of annotations:
@@ -635,6 +738,14 @@ There are downsides to this approach. It partially undermines the source compati
635738

636739
We could introduce an optional `message` argument to the `@unsafe` attribute, which would allow programmers to indicate *why* the use of a particular declaration is unsafe and, more importantly, how to safely write code that uses it. However, this argument isn't strictly necessary: a comment could provide the same information, and there is established tooling to expose comments to programmers that wouldn't be present for this attribute's message, so we have omitted this feature.
637740

741+
## Revision history
742+
743+
* **Revision 2 (following first review)**
744+
* Specified that variables of unsafe type passed in to uses of `@safe` declarations (e.g., calls, property accesses) are not diagnosed as themselves being unsafe. This makes means that expressions like `unsafeBufferePointer.count` will be considered safe.
745+
* Require types whose storage involves an unsafe type or conformance to be marked as `@safe` or `@unsafe`, much like other declarations that have unsafe types or conformances in their signature.
746+
* Add an Alternatives Considered section on prohibiting unsafe conformances and overrides.
747+
* Add a Future Directions section on handling unsafe code in macro expansions.
748+
638749
## Acknowledgments
639750

640751
This proposal has been greatly improved by the feedback from Félix Cloutier, Geoff Garen, Gábor Horváth, Frederick Kellison-Linn, Karl Wagner, and Xiaodi Wu.

0 commit comments

Comments
 (0)