Skip to content

Commit 86c1028

Browse files
committed
SE-0458: Add Alternatives Considered section on unsafe conformances/overrides
1 parent 46c6781 commit 86c1028

File tree

1 file changed

+58
-6
lines changed

1 file changed

+58
-6
lines changed

proposals/0458-strict-memory-safety.md

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ There are a few exemptions to the rule that any unsafe constructs within the sig
217217

218218
### `@safe` attribute
219219

220-
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 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`:
220+
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`:
221221

222222
```swift
223223
extension UnsafeBufferPointer {
@@ -430,11 +430,6 @@ Other than the source break above, the introduction of this strict safety checki
430430

431431
The attributes, `unsafe` expression, and strict memory-safety checking model proposed here have no impact on ABI.
432432

433-
## Revision history
434-
435-
* **Revision 2 (following first review)**
436-
* 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.
437-
438433
## Future Directions
439434

440435
### The `SerialExecutor` and `Actor` protocols
@@ -489,6 +484,57 @@ We have several options here:
489484

490485
## Alternatives considered
491486

487+
### Prohibiting unsafe conformances and overrides entirely
488+
489+
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:
490+
491+
* When overriding a safe declaration with an unsafe one, the overriding subclass must be marked `@unsafe`.
492+
* When implementing a safe protocol requirement with an unsafe declaration, the corresponding conformance must be marked `@unsafe`.
493+
494+
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:
495+
496+
```swift
497+
func parse(_ input: some Collection<UInt8>) -> ParseResult
498+
```
499+
500+
Calling this function with an unsafe buffer pointer will produce a diagnostic due to the use of the unsafe conformance of `UnsafeBufferPointer` to `Collection`:
501+
502+
```swift
503+
let result = parse(unsafeBufferPointer) // warning: use of unsafe conformance
504+
```
505+
506+
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.
507+
508+
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:
509+
510+
```swift
511+
@safe struct ImmortalBufferWrapper<Element> : Collection {
512+
let buffer: UnsafeBufferPointer<Element>
513+
514+
@unsafe init(_ withImmortalBuffer: UnsafeBufferPointer<Element>) {
515+
self.buffer = unsafe buffer
516+
}
517+
518+
subscript(index: Index) -> Element {
519+
precondition(index >= 0 && index < buffer.count)
520+
return unsafe buffer[index]
521+
}
522+
523+
/* Also: Index, startIndex, endIndex, index(after:) */
524+
}
525+
```
526+
527+
The call would then look like this:
528+
529+
```swift
530+
let wrapper = unsafe ImmortalBufferWrapper(withImmortalBuffer: buffer)
531+
let result = parse(wrapper)
532+
```
533+
534+
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).
535+
536+
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.
537+
492538
### `@unsafe` implying `unsafe` throughout a function body
493539

494540
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:
@@ -654,6 +700,12 @@ There are downsides to this approach. It partially undermines the source compati
654700

655701
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.
656702

703+
## Revision history
704+
705+
* **Revision 2 (following first review)**
706+
* 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.
707+
* Add an Alternatives Considered section on prohibiting unsafe conformances and overrides.
708+
657709
## Acknowledgments
658710

659711
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)