Highlight
If your project has legacy C/C++ code or depends on third-party C libraries, you already know
UnsafeMutablePointer. Swift is a safe language, but the moment you step into C/C++ territory, buffer overflows and use-after-free bugs lie in wait like landmines.
Core Content
Say you build a pet photo sharing app. The main body is Swift, but you reuse some old C/C++ code for image filters. When Swift calls invertImage(&imageData, imageSize), &imageData quietly becomes an UnsafeMutablePointer behind the scenes, and imageSize is a separate parameter. One day you slip and type 1000000000000 for the size. The compiler says nothing. At runtime, you write past the buffer. Bugs like this are exactly what attackers love.
Swift 6.2 offers a two-step fix. Step one is the Strict Memory Safety build mode: turn it on in build settings, and the compiler warns on every unsafe call, including code that “looks like Swift” but secretly constructs an unsafe pointer. Step two is adding annotations to your C/C++ headers (__counted_by, __noescape, __lifetimebound, SWIFT_NONESCAPABLE, SWIFT_SHARED_REFERENCE). These turn implicit facts like “how big is this pointer” and “how long does it live” into explicit contracts. Once the contract is written, the Swift compiler automatically bridges those C/C++ functions into safe signatures that take Span/MutableSpan. Out-of-bounds writes and use-after-free bugs get caught at compile time.
Detailed Content
1. Turn on Strict Memory Safety to surface hidden unsafe code (04:01)
// Swift
var imageData = [UInt8](repeating: 0, count: imageDataSize)
filterImage(&imageData, imageData.count)
//warning: Expression uses unsafe constructs but is not marked with 'unsafe'
Key points:
&imageDatabecomes anUnsafeMutablePointerat compile time. You cannot see this by reading the Swift code.- After you turn on Strict Memory Safety, the compiler emits a warning on this line, with a note explaining why.
- It only warns; it does not block the build. Treat it as a security audit scanner and roll it out gradually.
2. Use __counted_by to bind buffer size to the pointer (09:58)
// C/C++
void invertImage(uint8_t *__counted_by(imageSize) imagePtr __noescape, size_t imageSize);
Key points:
__counted_by(imageSize)tells the compiler that the bufferimagePtrpoints to has length equal to parameterimageSize.- With this annotation, the Swift side no longer sees a raw
pointer + sizepair. Instead it receives aMutableSpan<UInt8>directly. __noescapedeclares that the pointer is not held after the function returns. This is the foundation for the lifetime checks that follow.- The annotation must appear on both the declaration and the definition, or Swift will not import the new signature.
3. Call site: from raw pointer to Span (08:54)
// Swift
var imageDataSpan = imageData.mutableSpan
invertImage(&imageDataSpan)
Key points:
- Stop passing
&imageDataplus a size. PassmutableSpaninstead. MutableSpancarries correct bounds information. A wrong-size bug becomes impossible at the type level.- The compiler automatically extracts the pointer and size to feed the underlying C function during bridging.
4. Use __noescape to shut down use-after-free (15:18)
// C++
CxxSpanOfByte cachedView;
void applyGrayscale(CxxSpanOfByte imageView __noescape) {
// Apply effect on image ...
}
Key points:
- C++
std::spancarries no lifetime information. Storing it in a globalcachedViewplants a dangling pointer. __noescapeis a contract the function author makes with the compiler: the parameter will not be kept after the function returns.- After adding the annotation, the Swift side treats it as a
MutableSpan. Storing it in an external variable triggers a compile error:lifetime dependent value escapes its scope(14:08).
5. Use __lifetimebound to express “return value depends on parameter” (18:47)
// C++
CxxSpanOfByte scanImageRow(CxxSpanOfByte imageView __lifetimebound,
size_t width, size_t rowIndex);
Key points:
- The function returns a view of one row inside
imageView. Its lifetime must not outliveimageView. __lifetimeboundtells the compiler about this dependency, so the Swift side can correctly import the returnedMutableSpan.- Without this annotation, Swift reports
a function with a ~Escapable result requires '@lifetime(...)'(18:06).
6. Custom C++ types: SWIFT_NONESCAPABLE and SWIFT_SHARED_REFERENCE (22:29)
// C++
struct ImageView {
std::span<uint8_t> pixelBytes;
int width;
int height;
} SWIFT_NONESCAPABLE;
struct ImageBuffer {
std::vector<uint8_t> data;
int width;
int height;
std::atomic<unsigned> refCount;
} SWIFT_SHARED_REFERENCE(retain_image_buffer, release_image_buffer);
Key points:
SWIFT_NONESCAPABLEexports a view type as non-escapable in Swift, forbidding escape.SWIFT_SHARED_REFERENCEhooks a reference-counted C++ type into Swift’s ARC. Retain and release are managed by Swift.- Companion macros
SWIFT_RETURNS_RETAINED/SWIFT_RETURNS_UNRETAINEDmark whether a return value already holds a +1 reference, preventing +1/+0 mismatches (23:57).
7. Harden the C++ project itself: bounds-safe buffer usage (28:59)
// C++
void fill_array_with_indices(uint8_t *buffer, size_t count) {
for (size_t i = 0; i < count; ++i) {
buffer[i] = i; // error: unsafe buffer access
}
}
Key points:
- Set
Enforce Bounds-Safe Buffer Usageto Yes in build settings. The compiler treats raw pointer subscript access as an error. - Rewrite with
std::span, andbuffer[i]gains runtime bounds checking. - C projects can use the
-fbounds-safetyextension for compile-time plus runtime double-checking on__counted_byannotated pointers (30:11).
Core Takeaways
-
What to do: Turn on Strict Memory Safety immediately in any security-sensitive app (accounts, payments, user files).
- Why it matters: It only warns, never blocks the build. It scans every cross-language call site in one pass. That is a free security audit baseline.
- How to start: Xcode → Build Settings → search “Strict Memory Safety” → set to Yes, rebuild, then prioritize by the warning list.
-
What to do: Start adding
__counted_by+__noescapeannotations to C/C++ headers from leaf functions outward.- Why it matters: Leaf functions have no downstream dependencies. If you get an annotation wrong, the blast radius is small. Working bottom-up lets call sites enjoy Span benefits immediately.
- How to start: Pick a small function that calls no other C/C++ code (for example,
invertorgrayscalein image processing). Add annotations to both declaration and definition. Confirm the Swift call site can pass aMutableSpan.
-
What to do: Migrate reference-counted C++ types in your project to
SWIFT_SHARED_REFERENCE.- Why it matters: Hand-written bridging layers are a hotbed of use-after-free bugs. Handing retain/release to ARC lets the compiler guarantee correct pairing.
- How to start: Find existing
retain_xxx/release_xxxfunction pairs. AddSWIFT_SHARED_REFERENCE(retain_xxx, release_xxx)to the type, then mark factory functions that return a +1 reference withSWIFT_RETURNS_RETAINED.
-
What to do: Enable Standard Library Hardening and Enforce Bounds-Safe Buffer Usage in C++ subprojects.
- Why it matters: It forces old “raw pointer + size” code toward
std::span, blocking out-of-bounds access at the source. - How to start: Set
Enforce Bounds-Safe Buffer Usageto Yes in build settings. Follow compile errors to replace hot-path interfaces withstd::span<T>.
- Why it matters: It forces old “raw pointer + size” code toward
Related Sessions
- Improve memory usage and performance with Swift — the design motivation behind Span / MutableSpan, which this article bridges into
- Explore Swift and Java interoperability — cross-language interoperability in the same series; the approach is comparable
- Embracing Swift concurrency — the lifetime meaning of annotated Spans in concurrent scenarios
- What’s new in Swift — an overview of Swift 6.2 new features, including the Strict Memory Safety entry point
Comments
GitHub Issues · utterances