-
-
Notifications
You must be signed in to change notification settings - Fork 3k
Description
I didn't see anywhere this was clearly written up so here it is.
It's not possible to catch use-after-free of stack memory at compile-time (#5725) because it can be equated to the halting problem. I'll leave that proof as an exercise for the reader.
So, we catch it at runtime, in safe build modes.
Step 1, do escape analysis. Only stack locals which have pointers captured which might outlive their scope are subject to these safety checks.
Step 2, introduce an API for heap-allocating memory, like this:
extern fn safe_alloc(size: usize, alignment: u8) ?[*]u8;
extern fn safe_free(ptr: [*]u8, size: usize, alignment: u8, rbp: usize) void;
These would default to using a slimmed down version of std.heap.DebugAllocator
- one that avoids reuse of memory addresses, but does not capture stack traces. Perhaps this would be implemented in compiler_rt so that it could be optimized and be compiled without these safety checks, which would otherwise be recursive.
The subset of stack values which have possibly escaped pointers would then be allocated this way. As an optimization, if there were multiple escaped values in the stack frame, they could be allocated together and freed together.
The allocation function could fail, so the stack slots would still be reserved for such case. Stack base address is passed to safe_free
so that it can ignore such fallback pointers.
Heap-allocating instead of stack-allocating is obviously significantly slower, so that's why it's important for Step 1 to work well, in order to make Step 2 rare.
So then, when a dangling stack pointer is used, it either segfaults, or its bytes have been memset to the 0xaa
pattern, making it very likely to immediately trigger a crash. Even if it does not trigger a crash, however, it is still memory safe in the sense that the memory does not alias any other allocations, making it easier to debug in Debug mode, and avoiding a certain class of bugs in ReleaseSafe mode.