Skip to content

Commit af48452

Browse files
committed
ptr-tag-helpers: Add RFC
1 parent ffb2c46 commit af48452

File tree

1 file changed

+128
-0
lines changed

1 file changed

+128
-0
lines changed

text/3700-ptr-tag-helpers.md

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
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

Comments
 (0)