|
| 1 | +- Feature Name: `ptr_tag_helpers` |
| 2 | +- Start Date: 2024-09-26 |
| 3 | +- RFC PR: [rust-lang/rfcs#3700](https://github.com/rust-lang/rfcs/pull/3700) |
| 4 | +- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000) |
| 5 | + |
| 6 | +# Summary |
| 7 | +[summary]: #summary |
| 8 | + |
| 9 | +Add helper methods on primitive pointer types to facilitate getting and setting the tag of a pointer. |
| 10 | +Intended to work with programs that make use of architecture features such as AArch64 |
| 11 | +Top-Byte Ignore (TBI), the primary use-case being writing tagging memory allocators. |
| 12 | + |
| 13 | +# Motivation |
| 14 | +[motivation]: #motivation |
| 15 | + |
| 16 | +Tagged pointers are pointers in which the unused top bits are set to contain some metadata - the tag. |
| 17 | +No 64-bit architecture today actually uses a 64-bit address space. Most operating systems only use |
| 18 | +the lower 48 bits, leaving higher bits unused. The remaining bits are for the most part used to |
| 19 | +distinguish userspace pointers (0x00) from kernelspace pointers (0xff). |
| 20 | +Certain architectures provide extensions, such as TBI on AArch64, that allow programs to make use of |
| 21 | +those unused bits to insert custom metadata into the pointer. |
| 22 | + |
| 23 | +Currently, Rust does not acknowledge TBI and related architecture extensions that enable the use of |
| 24 | +tagged pointers. This could potentially cause issues in cases such as working with TBI-enabled C/C++ |
| 25 | +components over FFI, or when writing a tagging memory allocator. |
| 26 | +These functions are worth including in the standard library, despite their relatively niche use case |
| 27 | +and relative simplicity, so that there is a single known location where Miri hooks can be called to |
| 28 | +update the canonical address. |
| 29 | +This will make it easier to modify tagged pointers without breaking the Rust memory model. |
| 30 | + |
| 31 | +# Guide-level explanation |
| 32 | +[guide-level-explanation]: #guide-level-explanation |
| 33 | + |
| 34 | +This RFC adds two methods on each primitive pointer type - `ptr.tag()` and |
| 35 | +`ptr.with_tag(tag: u8)`. |
| 36 | + |
| 37 | +``` |
| 38 | +assert!(ptr.tag() == 0); |
| 39 | +let tagged_ptr = unsafe { ptr.with_tag(63) }; |
| 40 | +assert!(tagged_ptr.tag() == 63); |
| 41 | +``` |
| 42 | + |
| 43 | +The primary use-case is implementing an allocator that tags pointers before returning them to the |
| 44 | +caller. |
| 45 | + |
| 46 | +# Reference-level explanation |
| 47 | +[reference-level-explanation]: #reference-level-explanation |
| 48 | + |
| 49 | +Within Rust's memory model, modifying the high bits offsets the pointer outside of the bounds of |
| 50 | +its original allocation, making any use of it Undefined Behaviour. |
| 51 | + |
| 52 | +The `with_tag()` method is only designed to be a helper for writing tagging allocators. |
| 53 | +Users *must* ensure that code using this method simulates a realloc from the untagged |
| 54 | +address to the tagged address, and that the underlying memory is only ever accessed |
| 55 | +using the tagged address from there onwards. Anything short of that explicitly violates the |
| 56 | +Rust memory model and will cause the program to break in unexpected ways. |
| 57 | + |
| 58 | +# Drawbacks |
| 59 | +[drawbacks]: #drawbacks |
| 60 | + |
| 61 | +Because the memory model we currently have is not fully compatible with memory tagging and |
| 62 | +tagged pointers, setting the high bits of a pointer must be done with great care in order to |
| 63 | +avoid introducing Undefined Behaviour. |
| 64 | + |
| 65 | +Every change to the high bits has to at least simulate a realloc and we must ensure the old pointers |
| 66 | +are invalidated. This is due to a fundamental discrepancy between how Rust & LLVM see a memory |
| 67 | +address and how the OS & hardware see memory addresses. |
| 68 | +From the OS & hardware perspective, the high bits are reserved for metadata and do not actually form |
| 69 | +part of the address (in the sense of an 'address' being an index into the memory array). |
| 70 | +From the LLVM perspective, the high bits are part of the address and changing them means we are now |
| 71 | +dealing with a different address altogether. Having to reconcile those two views necessarily creates |
| 72 | +some friction and extra considerations around Undefined Behaviour. |
| 73 | + |
| 74 | +More context on the aforementioned discrepancy can be found in a discussion about memory tagging |
| 75 | +on GitHub, [here](https://github.com/rust-lang/rust/issues/129010). |
| 76 | + |
| 77 | +# Rationale and alternatives |
| 78 | +[rationale-and-alternatives]: #rationale-and-alternatives |
| 79 | + |
| 80 | +Without having a dedicated library method for modifying the high bits, users wanting to write |
| 81 | +tagging memory allocators would have to resort to manually using bitwise operations. |
| 82 | +Having a method for doing so in the library creates a place where e.g. Miri hooks can be called |
| 83 | +to let Miri know that a pointer's cannonical address has been updated. |
| 84 | + |
| 85 | +# Prior art |
| 86 | +[prior-art]: #prior-art |
| 87 | + |
| 88 | +TBI already works in C, though mostly by default and care must be taken to make sure no |
| 89 | +Undefined Behaviour is introduced. The compiler does not take special steps to preserve the tags, |
| 90 | +but it doesn't try to remove them either. |
| 91 | +That being said, the C/C++ standard library does not take tags into account during alias analysis. |
| 92 | + |
| 93 | +Notably, [Android](https://source.android.com/docs/security/test/tagged-pointers) already makes |
| 94 | +extensive use of TBI by tagging all heap allocations. |
| 95 | + |
| 96 | +The idea is also not one specific to AArch64, as there are similar extensions present on other |
| 97 | +architectures that facilitate working with tagged pointers. |
| 98 | + |
| 99 | +# Unresolved questions |
| 100 | +[unresolved-questions]: #unresolved-questions |
| 101 | + |
| 102 | +How exactly should the API be structured in order to accommodate differences between architectures? |
| 103 | +Different architectures use different tagging schemes. For instance, on AArch64 the tag is the entire |
| 104 | +top byte. The number of bits used by other architectures can be different, and equally bit 63 might |
| 105 | +be reserved thus making the tag start at bit 62. |
| 106 | +To make the interface flexible enough for any arbitrary tagging scheme, the caller would have to pass |
| 107 | +the tag, the number of bits used for the tag in the architecture and the offset where the tag starts. |
| 108 | +This might make the interface somewhat unwieldy and require different invocations for each |
| 109 | +architecture. |
| 110 | +Alternatively, we could abstract away the architecture details by adding `cfg(target_arch)` checks |
| 111 | +inside the methods, so that the caller would just be able to write `ptr.with_tag(tag)` and have that |
| 112 | +automatically use whichever tagging scheme the code is being compiled for. |
| 113 | + |
| 114 | +It is most likely not feasible to make `with_tag()` safe to use regardless of the context, |
| 115 | +hence the current approach is to make it an unsafe method with a safety notice about the user's |
| 116 | +responsibilities. |
| 117 | + |
| 118 | +# Future possibilities |
| 119 | +[future-possibilities]: #future-possibilities |
| 120 | + |
| 121 | +The interface could be extended (or similar interfaces could be added) to accommodate similar |
| 122 | +architecture extensions e.g. on x86-64. There are subtle differences between platforms, e.g. |
| 123 | +on x86-64 modifying bit 63 is not allowed for pointer-tagging purposes, so unlike AArch64 |
| 124 | +not all possible u8 values would be safe to use. |
| 125 | + |
| 126 | +On compatible platforms, interesting use-cases might be possible, e.g. tagging pointers when |
| 127 | +allocating memory in Rust in order to insert metadata that could be used in experiments with |
| 128 | +pointer strict provenance. |
0 commit comments