update memory tagging documentation

pull/238/head
Daniel Micay 2024-01-03 11:55:30 -05:00
parent 365ee6900d
commit abe54dba27
1 changed files with 29 additions and 71 deletions

100
README.md
View File

@ -724,80 +724,38 @@ freeing as there would be if the kernel supported these features directly.
## Memory tagging
**Memory tagging has been implemented and this section is currently
out-of-date.**
Random tags are set for all slab allocations when allocated. 5 possible values
are excluded: the default 0 tag, a statically reserved free tag, the previous
tag used for the slot, the current (or previous) tag used for the slot to the
left and the current (or previous) tag used for the slot to the right. 3 of
these are dynamic random values. When a slab allocation is freed, the reserved
free tag is set for the slot. Linear overflows are deterministically detected.
Use-after-free has deterministic detection until the freed slot goes through
both the random and FIFO quarantines, gets allocated again, goes through both
quarantines again and then finally gets allocated again for a 2nd time. Since
the default 0 tag isn't used, untagged memory can't access malloc allocations
and vice versa, although it may make sense to reuse the default tag for free
data to avoid reducing the possible random tags from 15 to 14, since freed
data is always zeroed anyway.
Integrating extensive support for ARMv8.5 memory tagging is planned and this
section will be expanded to cover the details on the chosen design. The approach
for slab allocations is currently covered, but it can also be used for the
allocator metadata region and large allocations.
Slab allocations are done in a statically reserved region for each size class
and all metadata is in a statically reserved region, so interactions between
different uses of the same address space is not applicable.
Memory allocations are already always multiples of naturally aligned 16 byte
units, so memory tags are a natural fit into a malloc implementation due to the
16 byte alignment requirement. The only extra memory consumption will come from
the hardware supported storage for the tag values (4 bits per 16 bytes).
Large allocations beyond the largest slab allocation size class (128k by
default) are guaranteed to have randomly sized guard regions to the left and
right. Random and FIFO address space quarantines provide use-after-free
detection. Random tags would still be useful for probabilistic detection of
overflows, probabilistic detection of use-after-free once the address space is
out of the quarantine and reused for another allocation and deterministic
detection of use-after-free for reuse by another allocator. We need to test
whether the cost is acceptable for enabling this by default.
The baseline policy will be to generate random tags for each slab allocation
slot on first use. The highest value will be reserved for marking freed memory
allocations to detect any accesses to freed memory so it won't be part of the
generated range. Adjacent slots will be guaranteed to have distinct memory tags
in order to guarantee that linear overflows are detected. There are a few ways
of implementing this and it will end up depending on the performance costs of
different approaches. If there's an efficient way to fetch the adjacent tag
values without wasting extra memory, it will be possible to check for them and
skip them either by generating a new random value in a loop or incrementing
past them since the tiny bit of bias wouldn't matter. Another approach would be
alternating odd and even tag values but that would substantially reduce the
overall randomness of the tags and there's very little entropy from the start.
Once a slab allocation has been freed, the tag will be set to the reserved
value for free memory and the previous tag value will be stored inside the
allocation itself. The next time the slot is allocated, the chosen tag value
will be the previous value incremented by one to provide use-after-free
detection between generations of allocations. The stored tag will be wiped
before retagging the memory, to avoid leaking it and as part of preserving the
security property of newly allocated memory being zeroed due to zero-on-free.
It will eventually wrap all the way around, but this ends up providing a strong
guarantee for many allocation cycles due to the combination of 4 bit tags with
the FIFO quarantine feature providing delayed free. It also benefits from
random slot allocation and the randomized portion of delayed free, which result
in a further delay along with preventing a deterministic bypass by forcing a
reuse after a certain number of allocation cycles. Similarly to the initial tag
generation, tag values for adjacent allocations will be skipped by incrementing
past them.
For example, consider this slab of allocations that are not yet used with 15
representing the tag for free memory. For the sake of simplicity, there will be
no quarantine or other slabs for this example:
| 15 | 15 | 15 | 15 | 15 | 15 |
Three slots are randomly chosen for allocations, with random tags assigned (2,
7, 14) since these slots haven't ever been used and don't have saved values:
| 15 | 2 | 15 | 7 | 14 | 15 |
The 2nd allocation slot is freed, and is set back to the tag for free memory
(15), but with the previous tag value stored in the freed space:
| 15 | 15 | 15 | 7 | 14 | 15 |
The first slot is allocated for the first time, receiving the random value 3:
| 3 | 15 | 15 | 7 | 14 | 15 |
The 2nd slot is randomly chosen again, so the previous tag (2) is retrieved and
incremented to 3 as part of the use-after-free mitigation. An adjacent
allocation already uses the tag 3, so the tag is further incremented to 4 (it
would be incremented to 5 if one of the adjacent tags was 4):
| 3 | 4 | 15 | 7 | 14 | 15 |
The last slot is randomly chosen for the next allocation, and is assigned the
random value 14. However, it's placed next to an allocation with the tag 14 so
the tag is incremented and wraps around to 0:
| 3 | 4 | 15 | 7 | 14 | 0 |
When memory tagging is enabled, checking for write-after-free at allocation
time and checking canaries are both disabled. Canaries will be more thoroughly
disabled when using memory tagging in the future, but Android currently has
very dynamic memory tagging support where it can be enabled/disabled at any
time which creates a barrier to optimizing by disabling redundant features.
## API extensions