Using TBI and Address Math for detecting use-after-free in heap #9497
Labels
proposal
This issue suggests modifications. If it also has the "accepted" label then it is planned.
Milestone
I think there could be another way that Zig could detect use-after-free, with a different set of tradeoffs from the current method. I hope this issue will spark some ideas; this issue's "proposal" is really just a starting point, I suspect that with more Zig knowledge one could refine this to something much better than what I have written here!
This method is partially based on Top-Byte-Ignore (https://arxiv.org/ftp/arxiv/papers/1802/1802.09517.pdf), available for certain processors. TL;DR of their approach: Every 16 bytes chunk in memory has a corresponding 4 bits which contains a (pseudo-)random "tag", changed whenever we deallocate that 16 bytes. If we ever have a pointer pointing at that chunk, then the top byte of that pointer will contain the same tag value. Whenever we dereference the pointer, we assert that the pointer's tag value and the allocation's tag value still match.
The method described there requires 4 bits per 16-byte chunk, and the check is done on every load and store. They did it at the LLVM/instrumentation level, but I think a language could offer more opportunities here:
So here's a possible idea for #2 and #3, inspired by some things from Vale's generational references.
First, we'd have a custom allocator instead of malloc in this mode (Zig already does this, IIRC). Let's assume that every 4kb page has allocations of the same size (only 16-byte allocations in one 4kb page, only 32-byte allocations in another, etc).
The allocator can decide what virtual address to map a new physical page to, and therefore determines the addresses of the allocations. We'll use that to give some guarantees, using the (arbitrarily chosen) "middle byte", byte #5.
When the allocator decides what virtual address to map a new physical page to, it might use these rules:
and so on.
At the top of these allocations, it will put one pseudo-random non-zero byte to serve as our tag. When the allocator returns the address of a shiny new allocation to the user, it will include this tag in the top byte of that returned pointer.
Whenever Zig dereferences such a pointer, if the top byte is non-zero, it will use the middle byte to find the allocation's current tag and compare that to the pointer's top byte.
To find the top of the allocation (and therefore the tag):
(addr & ~0xF) - 1
).(addr & ~0x1F) - 1
).(addr & ~0x3F) - 1
).(addr & ~0x7F) - 1
).and so on.
We can generalize this to:
(addr & ((1 << middlebyte) - 1)) - 1)
As mentioned, we would only do this check if the top byte is non-zero. It might look like this:
Some benefits from this scheme so far:
Some drawbacks so far:
Some potential improvements/mitigations:
Can I confidently say that this locality would be worth the extra instructions and possible branching? Nope! But it's an interesting possible approach. Vale has shown us that combining weird ideas with compiler flexibility and profiling will often show us crazy efficient solutions, previously hidden. In this case, we were able to use single ownership and static analysis to eliminate most these checks as unnecessary, and then adjust struct layouts to eliminate that first branch and reduce the above 6-10 instructions to only four. So, unexpected flexibilities might be a factor in this idea's potential for Zig, who knows!
The text was updated successfully, but these errors were encountered: