Asserting-Conflicting-Access Model #26
Description
This is my best effort to describe the model put forward by @arielb1 and @ubsan. Naturally they should correct any misconceptions that I have. We should edit this description to be accurate. Also, we need a catchy name. =) I'm calling it the Asserting-Conflicting-Access (ACA) model based on its core rule.
The Aliasing Rule
The heart of the model is "the aliasing rule":
If a reference r
(the head reference) with lifetime 'r
is created, then afterwards, before 'r
ends but at no particular order
- Some memory location
l
is accessed through a borrow-chain fromr
- this is the asserting access. - The same memory location is accessed in any other way - this is the conflicting access.
- One of these accesses is a write.
Then an aliasing access has occured.
Aliasing accesses are UB, unless there exists a reference s
to l
with lifetime 's
, such that
s
was created by a borrow-chain fromr
s
has sufficient permissions for the conflicting access.'s
is alive at the point of access.s
is potentially alive. This means either:s
is an immutable references
is the reference used for the conflicting accesss
was converted to a raw pointer before the conflicting access
In that case, s
is called the guaranteeing reference.
Note the use of lifetimes in the model. From conversation with @arielb1, I gather that the intution is that, in a safe function, the lifetimes inferred by the compiler are to be used. But in unsafe code, the compiler must assume that the user could have chosen whatever (legal) lifetimes they would like. These lifetimes would still be subject to the basic constraints of the type system.
Facets
The model is access-based and defined in terms of undefined behavior. That is, the mere possession of a reference or overlapping pointer does not create undefined behavior. What creates undefined behavior is the conflicting use thereof. Moreover, undefined behavior does not necessarily define "blame" -- that is, the model can tell you what trace is illegal, but it can't always tell you if a function will lead to undefined behavior in isolation.
Intuition
I think the best intuition is to think of having permission to access memory at a particular point. Permissions flow from safe references. Raw pointers created from a reference get the permission of the reference they came from. When we exit the lifetime of a safe reference s
that was borrowed from borrowed from r
, the permissions for s
return to r
.
Example 1
Shows a mutable reference a
that is aliased by a raw pointer p
. a
is then reborrowed to b
. Using b
is ok and it's ok to use p
after we're done using b
.
unsafe {
let mut a: &mut i32 = i32;
let p: *mut i32 = a;
*a += 1; // asserting access
{
// This is ok, because `p` derives from `a`.
// Therefore although `*p` is a conflicting access,
// there exists a reference (`a`) with a lifetime that
// covers this access, sufficient permissions, and which
// was converted to a raw pointer.
*p += 1;
}
{
// `b` is derived from a borrow chain
let mut b: &mut i32 = &mut *a;
// Conflicting access relative to `*a += 1` above.
// OK because of the existence of reference `b`.
//
// Also serves as "asserting access" to the next line.
*b += 1;
// Conflicting access. Is this UB?
// There exists no reference created by a borrow chain from `b`.
// So the question hinges on whether the lifetime of `b` includes
// this point. Because unsafe code authors can pick the lifetimes
// they want, the answer would presumably be no.
//
// Intuitively, this makes sense, because you can think of the lifetimes
// as forming a kind of stack, but the unsafe code author gets to pick where the "pop"
// occurs. So by defining (and then using) `b`, we pushed `b` onto the
// stack, but here we can say we popped it.
*p += 1;
}
}
Example 2
Like example 1, except that we intermingle uses of b
and p
. I think this is expected to be illegal, but I can't figure out how it plays out in terms of the formal language (@arielb1 or @ubsan, can you interpret this for me?).
unsafe {
let mut a: &mut i32 = i32;
let p: *mut i32 = a;
*a += 1; // asserting access
{
let mut b: &mut i32 = &mut *a;
*b += 1;
// Can this serve as an asserting access? I can't
// tell if accesses through a raw pointer count
// as being accessed through a "borrow chain" from `a`.
// I think the answer is no.
*p += 1;
*b += 1;
}
}
The reason I expect this to be illegal is that it could correspond to something like:
fn patsy<F>(b: &mut i32, f: F) where F: FnOnce() {
*b += 1;
f(); // should not affect `*b`
*b += 1;
}
being invoked like:
fn villain(a: &mut i32) {
let p: *mut i32 = a;
patsy(a, || *p += 1)
}