Stack protectors (also referred to as “stack canaries”) are additional code which is inserted to detect buffer overflows on the stack frame which can potentially overwrite the return address.
The clang compiler already has this feature for C/C++ code. We'd like to add this security feature also in the Swift compiler.
Swift is a safe language. So why do we need stack protectors in Swift? For two reasons:
- In case unsafe operations (e.g.
UnsafeMutablePointer
) are used on stack allocated variables
func foo() {
var x = 0
withUnsafeMutablePointer(to: &x) {
$0[1] = 0xdeadbeef
}
}
- If C/C++/ObjectiveC functions are called from Swift with pointers to stack allocated variables. Even if the buffer overflow happens on the C/C++/ObjectiveC side, clang’s stack protection doesn’t help here because the stack allocation is done on the Swift side.
func foo() {
var x = 0
withUnsafeMutablePointer(to: &x) {
_ = memset($0, 0, 10)
}
}
Stack allocation happens in swift in the following cases:
- for large or generic variables in local scopes
- contexts of non-escaping closures are allocated on the stack
- class allocation can be stack promoted (including Array buffers).
According to some initial experiments, turning on stack protectors unconditionally for all Swift functions (which do stack allocations), results in significant code size increases. This is bad.
So the question is how can we detect the cases listed above and only insert stack protectors in functions where a potential overflow can occur?
Function-Local Analysis
The compiler can detect if an address of a stack allocated variable is in danger of causing an overflow within the scope of a function. Technically, it’s a simple analysis which checks all uses of alloc_stack
, alloc_ref [stack]
or partial_apply [on_stack]
instructions and triggers if there is e.g. an address_to_pointer
instruction in the use list.
Functions which contain such SIL patterns are marked as needing a stack protector.
Inter-procedural Analysis
It gets much more complicated if the stack allocation and the potential overflow are in different functions. This can happen if a stack address is passed to other functions, e.g. via an inout
parameter. It would still be too expensive to do a conservative default approach and insert stack protectors for all functions which pass stack addresses to other functions.
We can do two things to solve this problem:
If all call paths from a function with a stack allocation to functions with potential overflows are statically known, the compiler can do an inter-procedural analysis and insert stack protectors accordingly.
func foo() {
var x = 0 // allocation is done here
bar(&x)
}
func bar(_ x: inout Int) { // only called from foo()
withUnsafeMutablePointer(to: &x) {
$0[1] = 0xdeadbeef // overflow happens here
}
}
Sometimes it’s not the case that a call path is statically known. For example if a function call is dynamically dispatched, e.g. a vtable or witness table call. Or if a function is public and can be called from other modules.
But in this case it’s still possible to let such functions handle stack protection locally: the value which is passed via inout can be moved to a function-local stack area and then the stack protection logic can be done in such a function instead of its caller.
public func bar(_ x: inout Int) { // callers are unknown
var tmp = x // move, inserted by the compiler
withUnsafeMutablePointer(to: &tmp) {
$0[1] = 0xdeadbeef
}
x = tmp // move, inserted by the compiler
}
Implementation: the inserted move is a copy_addr [take] to [initialization]
which compiles down to a simple move. This even works with generic types (which are potentially not bitwise movable, e.g. weak ObjectiveC references). In this case the “initializeWithTake” value witness function is called which in most cases does a simple memcpy.
This approach has the benefit that we only have to pay for stack protection if there is a risk of an overflow, but not for the majority of regular “safe” Swift code.
The downside is that moving a value to a temporary stack location and back again can be slow in case of large values. Also, we are losing pointer identity, but Swift does not guarantee that anyway.
Escape Hatch
In some cases the user might want to opt out of stack protection, e.g. if moving a value causes too much performance overhead or if pointer identity must be preserved.
Stack protection for a specific address-to-pointer conversion can be disabled by using “unprotected” versions of withUnsafePointer
:
_withUnprotectedUnsafeMutablePointer
_withUnprotectedUnsafePointer
_withUnprotectedUnsafeBytes
Those functions are underscored for now, but might be go through a swift evolution proposal.
Originally, I considered a function attribute, e.g. _noStackProtection
. But this is problematic because it might get lost during function inlining. Also, I think it’s better to control the behavior of specific pointers rather than all pointers in a function.
In addition to this fine grained opt-out mechanism, it’s also possible to disable stack protection with a command line option for the whole compilation unit.
Debug Builds
Stack protection should also be enabled in debug builds. The main reason is that in case the compiler has to move an argument (see above), this should also be done in a debug build so that this behavior is tested (tests are usually run with debug builds).
In fact, in debug builds all pointers will be moved, because the library functions withUnsafePointer
et al are not inlined and the move is done in those library functions.
Initial Implementation
An initial implementation is here: https://github.com/apple/swift/pull/60933