AD5008: EnableClangSafeStackMachO¶
Summary¶
| Property | Value |
|---|---|
| ID | AD5008 |
| Name | EnableClangSafeStackMachO |
| Category | Security |
| Severity | Warning |
| Applies to | Mach-O executables compiled with Clang |
Description¶
Clang SafeStack provides strong protection against stack buffer overflows by separating the stack into two regions:
- Safe stack: Contains return addresses, register spills, and safe local variables
- Unsafe stack: Contains local variables that may be accessed through pointers
This makes it much harder for attackers to overwrite return addresses via buffer overflows.
How It Works¶
The rule checks for SafeStack symbols:
__safestack_init__safestack_unsafe_stack_ptr__safestack_pointer_address
The rule also verifies: - The binary was compiled with Clang - It's not a shared library (SafeStack doesn't support DSOs) - It doesn't use ucontext.h functions (incompatible)
Why This Matters¶
SafeStack provides architectural protection against stack buffer overflows by physically separating vulnerable data from return addresses. Unlike canaries that detect overflows, SafeStack makes overflows structurally unable to reach critical control-flow data.
The Fundamental Protection¶
Traditional Stack (vulnerable):
┌───────────────┐
│ Return Addr │ ← Overflow reaches here
├───────────────┤
│ Saved RBP │
├───────────────┤
│ char buf[64] │ ← Overflow starts
└───────────────┘
SafeStack (protected):
Safe Stack: Unsafe Stack:
┌───────────┐ ┌───────────┐
│ Return Addr│ │ char buf[]│ ← Overflow here
├───────────┤ │ │ Cannot reach
│ Saved RBP │ │ │ safe stack!
└───────────┘ └───────────┘
Comparison with Other Protections¶
| Protection | Mechanism | Bypass Possible |
|---|---|---|
| Stack canary | Detection | Yes (info leak) |
| ASLR | Randomization | Yes (info leak) |
| SafeStack | Physical separation | No path exists |
What Goes Where¶
| Safe Stack | Unsafe Stack |
|---|---|
| Return addresses | Arrays |
| Spilled registers | Address-taken vars |
| Safe scalars | Structs with arrays |
macOS-Specific Considerations¶
| Platform | SafeStack Support |
|---|---|
| macOS x86_64 | Clang supported |
| macOS ARM64 | Limited support |
| iOS | Not commonly used (PAC preferred) |
When to Use SafeStack vs PAC¶
| Platform | Recommendation |
|---|---|
| Intel Mac | SafeStack |
| Apple Silicon | PAC (hardware) |
| Cross-platform | Both where applicable |
Performance Considerations¶
SafeStack has minimal runtime overhead:
| Metric | Overhead |
|---|---|
| Runtime | ~1% average |
| Memory | Second stack per thread (~8KB) |
| Code size | Minimal |
Why overhead is low: - No runtime checks (unlike stack canaries) - Uses thread-local storage for unsafe stack pointer - Most variables remain on safe stack - Architectural protection requires no validation
Workload impact:
| Workload | Overhead |
|---|---|
| Compute-bound | <0.5% |
| Server applications | <1% |
| Buffer-intensive | 1-2% |
Comparison with alternatives on macOS:
| Protection | Overhead | Hardware Req |
|---|---|---|
| Stack canary | <1% | None |
| SafeStack | ~1% | None |
| PAC | <1% | Apple Silicon |
SafeStack is one of the lowest-overhead protections available.
- Stronger than stack canaries: Prevents overwrites entirely, not just detects them
- Low overhead: Typically 1-5% performance cost
- Backward compatible: Works with existing code
- Comprehensive: Protects all stack-based vulnerabilities
Resolution¶
Command Line¶
Xcode¶
Add to Other C Flags:
CMake¶
target_compile_options(myapp PRIVATE -fsanitize=safe-stack)
target_link_options(myapp PRIVATE -fsanitize=safe-stack)
Makefile¶
When to Suppress¶
This rule may be suppressed for:
- Shared libraries: SafeStack doesn't support DSOs
- ucontext.h usage:
getcontext/setcontext/makecontext/swapcontextare incompatible - Multi-compiler builds: All code must be compiled with Clang SafeStack
- Signal handlers: Using
sigaltstack()may cause issues - GCC-compiled code: Only Clang supports SafeStack
Compatibility Limitations¶
SafeStack is NOT compatible with:
#include <ucontext.h>
// These functions don't work with SafeStack:
getcontext(&ctx);
setcontext(&ctx);
makecontext(&ctx, func, 0);
swapcontext(&oldctx, &newctx);