Skip to content

Commit 01693c2

Browse files
committed
add weak protectors
1 parent 36661ec commit 01693c2

File tree

1 file changed

+37
-17
lines changed

1 file changed

+37
-17
lines changed

wip/stacked-borrows.md

+37-17
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Changes since publication of the paper:
2424

2525
* Items with `SharedReadWrite` permission are not protected even with `FnEntry` retagging.
2626
* Removal of `Untagged`.
27+
* Addition of "weak protectors" to justify `noalias` on `Box` parameters.
2728

2829
[Miri]: https://github.com/solson/miri/
2930
[all-hands]: https://paper.dropbox.com/doc/Topic-Stacked-borrows--AXAkoFfUGViWL_PaSryqKK~hAg-2q57v4UM7cIkxCq9PQc22
@@ -58,15 +59,35 @@ pub enum Permission {
5859
Disabled,
5960
}
6061

62+
/// The flavor of the protector.
63+
enum ProtectorKind {
64+
/// Protected against aliasing violations from other pointers.
65+
///
66+
/// Items protected like this cause UB when they are invalidated, *but* the pointer itself may
67+
/// still be used to issue a deallocation.
68+
///
69+
/// This is required for LLVM IR pointers that are `noalias` but *not* `dereferenceable`.
70+
WeakProtector,
71+
72+
/// Protected against any kind of invalidation.
73+
///
74+
/// Items protected like this cause UB when they are invalidated or the memory is deallocated.
75+
/// This is strictly stronger protection than `WeakProtector`.
76+
///
77+
/// This is required for LLVM IR pointers that are `dereferenceable` (and also allows `noalias`).
78+
StrongProtector,
79+
}
80+
6181
/// An item on the per-location stack, controlling which pointers may access this location and how.
6282
pub struct Item {
6383
/// The permission this item grants.
6484
perm: Permission,
6585
/// The pointers the permission is granted to.
6686
tag: Tag,
6787
/// An optional protector, ensuring the item cannot get popped until `CallId` is over.
68-
protector: Option<CallId>,
88+
protector: Option<(ProtectorKind, CallId)>,
6989
}
90+
7091
/// Per-location stack of borrow items.
7192
pub struct Stack {
7293
/// Used *mostly* as a stack; never empty.
@@ -159,13 +180,12 @@ To attach metadata to a particular function call, we assign a fresh ID to every
159180
In other words, the per-stack-frame `CallId` is initialized by `Tracking::new_call`.
160181

161182
We say that a `CallId` is *active* if the call stack contains a stack frame with that ID.
162-
In the following, we pretend there exists a function `call_is_active(id)` that can check this.
163183

164184
**Note**: Miri uses a slightly more complex system to track the set of active `CallId`; that is just an optimization to avoid having to scan the call stack all the time.
165185

166186
### Preliminaries for items
167187

168-
For brevity, we will write `(tag: perm)` to represent `Item { tag, perm, protector: None }`, and `(tag: perm; call)` to represent `Item { tag, perm, protector: Some(call) }`.
188+
For brevity, we will write `(tag: perm)` to represent `Item { tag, perm, protector: None }`, and `(tag: perm; kind, call)` to represent `Item { tag, perm, protector: Some((kind, call)) }`.
169189

170190
The following defines whether a permission grants a particular kind of memory access to a pointer with the right tag:
171191
`Unique` and `SharedReadWrite` grant all accesses, `SharedReadOnly` grants only read access.
@@ -221,12 +241,13 @@ When allocating memory, we have to initialize the `Stack` associated with the ne
221241

222242
### Accessing memory
223243

224-
On every memory access, we perform the following extra operation for every location that gets accessed (i.e., for a 4-byte access, this happens for each of the 4 bytes):
244+
On every memory access (reads and writes -- see below for deallocation),
245+
we perform the following extra operation for every location that gets accessed (i.e., for a 4-byte access, this happens for each of the 4 bytes):
225246

226247
1. Find the granting item. If there is none, this is UB.
227248
2. Check if this is a read access or a write access.
228-
- For write accesses, pop all *blocks* above the one containing the granting item. That is, remove all items above the granting one, except if the granting item is a `SharedReadWrite` in which case the consecutive `SharedReadWrite` above it are kept (but everything beyond is popped).
229-
- For read accesses, disable all `Unique` items above the granting one: change their permission to `Disabled`. This means they cannot be used any more. We do not remove them from the stack to avoid merging two blocks of `SharedReadWrite`.
249+
- For write accesses, pop all *blocks* above the one containing the granting item. That is, remove all items above the granting one, except if the granting item is a `SharedReadWrite` in which case the consecutive `SharedReadWrite` above it are kept (but everything beyond is popped). If any of the popped items is protected (weakly or strongly) with a `CallId` of an active call, we have UB.
250+
- For read accesses, disable all `Unique` items above the granting one: change their permission to `Disabled`. This means they cannot be used any more. We do not remove them from the stack to avoid merging two blocks of `SharedReadWrite`. If any disabled item is protected (weakly or strongly) with a `CallId` of an active call, we have UB.
230251

231252
### Reborrowing
232253

@@ -256,7 +277,7 @@ To reborrow a pointer, we are given:
256277
- a (typed) place, i.e., a location in memory, a tag and the type of the data we expect there (from which we can compute the size);
257278
- which kind of reference/pointer this is (`Unique`, `Shared` or a raw pointer which might be mutable or not);
258279
- a `new_tag: Tag` for the reborrowed pointer;
259-
- whether this reborrow needs to be protected.
280+
- whether this reborrow needs to be protected, and if yes, how (weak or strong protection).
260281

261282
The type of the place and the kind of reference/pointer together give the full type of the reference/pointer (or as much of it was we need).
262283
As a Rust signature, this would be:
@@ -275,7 +296,7 @@ fn reborrow(
275296
place: MPlaceTy<Tag>,
276297
kind: RefKind,
277298
new_tag: Tag,
278-
protect: bool,
299+
protect: Option<ProtectorKind>,
279300
)
280301
```
281302

@@ -307,14 +328,10 @@ let (perm, protect) = match ref_kind {
307328
(Permission::SharedReadWrite, protect),
308329
RefKind::Raw { mutable: false } |
309330
RefKind::Shared =>
310-
if inside_unsafe_cell { (Permission::SharedReadWrite, /* do not protect */ false) }
331+
if inside_unsafe_cell { (Permission::SharedReadWrite, /* do not protect */ None) }
311332
else { (Permission::SharedReadOnly, protect) }
312333
};
313-
let protector = if protect {
314-
Some(current_call_id())
315-
} else {
316-
None
317-
};
334+
let protector = protect.map(|kind| (kind, current_call_id()));
318335

319336
location.stack.grant(
320337
place.tag,
@@ -328,8 +345,10 @@ When executing `Retag(kind, place)`, we check if `place` holds a reference (`&[m
328345
For those we perform the following steps:
329346

330347
1. We compute a fresh tag: `Tracking::new_ptr_id()`.
331-
2. We determine if we will want to protect the items we are going to generate:
332-
This is the case only if `kind == FnEntry` and the type of this pointer is a reference (not a box).
348+
2. We determine if and how will want to protect the items we are going to generate:
349+
If `kind == FnEntry`, then a protector will be added; for references, we use a `StrongProtector`, for box a `WeakProtector`.
350+
(This means for both of them there is UB if the pointer gets invalidated while the call is active; and for references, additionally there is UB if the memory the pointer points to gets deallocated in anyway -- even if the pointer itself is used for that deallocation.)
351+
For other `kind`, no protector is added.
333352
3. We perform reborrowing of the memory this pointer points to with the new tag and indicating whether we want protection, treating boxes as `RefKind::Unique { two_phase: false }`.
334353

335354
We do not recurse into fields of structs or other compound types, only "bare" references/... get retagged.
@@ -340,7 +359,8 @@ We never recurse through a pointer indirection.
340359
### Deallocating memory
341360

342361
Memory deallocation first acts like a write access through the pointer used for deallocation.
343-
After that is done, we additionally check all protectors remaining in the stack: if any of them is still active, we have undefined behavior.
362+
After that is done, we additionally check all *strong* protectors remaining in the stack: if any of them is still active, we have undefined behavior.
363+
(Weak protectors do not matter here.)
344364

345365
## Adjustments to libstd
346366

0 commit comments

Comments
 (0)