Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC for a Rust Memory Model #1578

Closed
wants to merge 3 commits into from
Closed

RFC for a Rust Memory Model #1578

wants to merge 3 commits into from

Conversation

strega-nil
Copy link

with thanks to Amanieu, huonw, durka42, aatch, acrichto, nmatsakis, and anyone
else I might have missed.

Fixes, or at least starts the process of fixing, #1447

with thanks to Amanieu, huonw, durka42, aatch, acrichto, nmatsakis, and anyone
else I might have missed.
@strega-nil
Copy link
Author

@Amanieu
Copy link
Member

Amanieu commented Apr 10, 2016

This opens up the possibility of performing TBAA on types which have implementation-defined bytes since these types are only allowed to be read through a pointer of the correct type. I don't really see this as a big issue since this is only possible from unsafe code and the struct layout isn't guaranteed to match any other type.

@strega-nil
Copy link
Author

It's not that they're only allowed to be read through a pointer of a correct type. It's that if they are then used, it's undefined behavior (and therefore, TBAA might be a thing).

Although, I'm thinking about changing it to implementation defined offsets instead of implementation defined bytes... I'm not sure.

@petrochenkov
Copy link
Contributor

Enthusiasm is commendable, but I'd prefer this particular work to be done by professionals.

@ticki
Copy link
Contributor

ticki commented Apr 10, 2016

I agree with @petrochenkov, there are several shortfallings in this model.

@petrochenkov
Copy link
Contributor

@ticki
I'm not talking about concrete shortcomings, but about general approach.
I'd expect this to be a published academic paper by @RalfJung or someone else, with level appropriate for such papers, overview of prior art etc.

Anything less, e.g. "obvious" useful guarantees on struct layout or valid values of primitive types, can be specified on case by case basis.

@ticki
Copy link
Contributor

ticki commented Apr 10, 2016

I'm not talking about concrete shortcomings, but about general approach.

Exactly. It should be formalized (to avoid these ambigiuities). It is very vague for a memory model, as is.

@strega-nil
Copy link
Author

@ticki @petrochenkov Honestly, this has been simplified and such a lot from how it would appear originally, in order to fit in the RFC format. Being a "professional" also has very little to do with the work here; I've been thinking about this memory model for months, and I'm likely one of the experts on Rust's. People send people to me to explain it. And, I get it; you would be more comfortable if the memory model was written by a professional. But I'm not a professional, and I wrote (part of) a memory model (the important part, the pointer aliasing part). The other things are just a basis for pointer aliasing, which is what really needs to be defined.

@ticki
Copy link
Contributor

ticki commented Apr 10, 2016

@usban Don't get me wrong. I don't doubt that you know a lot about this subject, and have put a lot of effort into it. What I am saying is just that this wasn't really what was inteded with a memory model. What was inteded is a formalized model, instead.

@strega-nil
Copy link
Author

@ticki well, I am working on formalizing the model now... I just wanted to talk about it first, in public. It's important that we are all able to give feedback on it.

@archshift
Copy link
Contributor

The fact is, anything documentation is better than nothing, and nothing is exactly what Rust has right now. People are clueless as to the precise rules when it comes to pointer aliasing, which aren't currently documented anywhere.

EDIT: Clarification

@dgrunwald
Copy link
Contributor

This RFC reads to me as if there are two different operations that are both called ptr::read, but follow different rules (based on whether the argument is a pointer or reference).

In fact, if I see it correctly, replacing ptr::read(int_ref as *const i32) with *int_ref introduces the side-effect of making other pointers derived from that reference non-derived. Which means one can probably build programs where this harmless-looking code change introduces undefined behavior?

@steveklabnik
Copy link
Member

I am not gonna weigh in on this RFC very much, but one comment:

The fact is, anything is better than nothing,

When it comes to guarantees that you're making, nothing can often be better. If we were to adopt a poor model, it could inhibit Rust for the rest of its life. You really want something like this to be absolutely foolproof, or as close to it as possible. Adopting something that's not ready because "something is better than nothing" is not a good idea here.

@archshift
Copy link
Contributor

For the moment, then, can we at least have some documentation that pointer aliasing of any form is Undefined Behavior? Because that's what it really is without any rules on its use.

@strega-nil
Copy link
Author

@archshift This is, in fact, true currently.

@strega-nil
Copy link
Author

@steveklabnik One thing I found really compelling was something that @dikaiosune said in IRC:

"is it just me, or is waiting for 5 or more years for any guarantees about unsafe pointers kinda untenable for rust adoption?"

Currently, this:

let mut x = 0;
let ptr1 = &mut x as *mut i32;
let ptr2 = &mut x as *mut i32;
*ptr1 = 1;
*ptr2 = 2;
*ptr1

is completely undefined. And most unsafe code assumes it is defined.

@RalfJung
Copy link
Member

I'd expect this to be a published academic paper by @RalfJung or someone else, with level appropriate for such papers, overview of prior art etc.

Just to be clear here, I am not right now working on a memory model for Rust. Nobody should feel blocked by me here. For my Rust formalization I decided to shortcut this discussion, and instead define too many behaviors. This means that at least I can prove the desired libraries to be safe in my model (hopefully), but they may still turn out to rely on behavior that Rust wishes not to define.

@archshift
Copy link
Contributor

@ubsan Even that is unclear, as according to the Rust Doc:

* pointers are allowed to alias, allowing them to be used to write shared-ownership types, and even thread-safe shared memory types
...
[Raw pointers] have no guarantees about aliasing or mutability other than mutation not being allowed directly through a *const T.

@strega-nil
Copy link
Author

@archshift Exactly. It's completely undefined.

@anp
Copy link
Member

anp commented Apr 11, 2016

Since @ubsan pinged me here, I'd like to share some of my thoughts on the level of formalism or academic chops present in the RFC.

One of the things that most impresses me about and attracts me to open source in general (and to a greater degree, Rust specifically) is the power that "expert amateurs" can have when they ignore credentials and focus on a substantive problem, emphasizing collaboration and results over social status and signalling. In many cases when one lacks the time, specific knowledge, or some other resource to substantively critique a work, the easiest signal to process can be from a contributor's credentials and prior achievements, or from the format of the presentation. But I think that's a bad trap for an inclusive community like Rust's to fall into.

I have always been impressed by the pragmatism in the other discussions I've read here, and I think it's a distraction to focus (explicitly or implicitly) on ubsan's qualifications or the current level of formalism of the RFC, especially when its author has approached it in this way:

I am working on formalizing the model now... I just wanted to talk about it first, in public. It's important that we are all able to give feedback on it.

I don't see any reason to reject this out of hand. The product of this work will be very important for Rust's community and its future, and I think it deserves concrete feedback and a substantive appraisal.

@ticki
Copy link
Contributor

ticki commented Apr 11, 2016

I have to disagree with @steveklabnik on this one. In this case something is better than nothing, as long as we make sure that the guarantees are not going to get broken in the future.

Personally, I would prefer an access based model over the one presented here. I am using an access based model in my formal verification of Redox. I have outlined my idea below:

(unfortunately, github doesn't support Latex, so I have rendered it as images)

and formalized through:

@steveklabnik
Copy link
Member

I have to disagree with @steveklabnik on this one. In this case something is better than nothing, as long as we make sure that the guarantees are not going to get broken in the future.

I think we're on the same page. It's not so much that it has to be absolutely, fully, 100% perfect at first blush, just that we're not painted into a corner.

@ticki
Copy link
Contributor

ticki commented Apr 11, 2016

@steveklabnik, so what you are saying is that you think it should rather make one promise less than one promise more?

@steveklabnik
Copy link
Member

@ticki I'm not familiar with that idiom. But I don't mean "something is worse than nothing" in the sense that it's an all-or-nothing enterprise. But I wouldn't want to rush to get something down just to have some kind of guarantee, only to find out that we're not actually happy with it later.

Anyway, I said I wasn't going to say much, and now I've made three posts, so I'll stop 😉

@comex
Copy link

comex commented Apr 11, 2016

Clearly we must aspire to the level of clarity and precision provided by the gold standard of standards, ISO C. [1] [2] [3] [4] [5] [6] [7]

@arielb1
Copy link
Contributor

arielb1 commented Apr 11, 2016

let mut x = 0;
let ptr1 = &mut x as *mut i32;
let ptr2 = &mut x as *mut i32;
*ptr1 = 1;
*ptr2 = 2;
*ptr1

This will actually work on every model that we plan to use. It is undefined in the exact same sense as everything else.

What is more dubious, is for example

#[derive(Debug)] struct Foo<T>(T);
fn main() {
    let five = 5;
    let five_ref = &five;
    let mut x = Some(Foo(five_ref));
    let x_addr = &mut x as *mut Option<_>;
    match x {
        x_copy => {
            if let Some(ref inner) = x_copy {
                unsafe { *x_addr = None; }
                println!("{:?}", unsafe { (*(inner as *const Foo<_>)).0 });
            }
        }
    }
}

This generates code that crashes on current rustc, through I don't think it should.

In any case, I don't like the derived pointer model because it is fundamentally based on "confidentality", while Rust basically tries to guarantee "integrity".

@arielb1
Copy link
Contributor

arielb1 commented Apr 11, 2016

Additionally, LLVM likes attributes on loads, so lvalue-to-rvalue-conversions require the rvalue to be valid.

@arielb1
Copy link
Contributor

arielb1 commented Jun 6, 2016

@ubsan

So have drop glue call DerefMove to get the interior to drop? That... could work.

OTOH, you ought to be able to do partial drops, which would make DerefMove unsafe:

struct Foo(Box<u32>, Box<u32>);
fn main() {
    let x = Box::new(Foo(Box::new(0), Box::new(1)));
    drop(x.1);
    // drop glue:
    let d = DerefMove::deref_move(&mut x); // unsafe! can access `x.1`!
    drop(d.0);
    mem::forget(d.0);
    Drop::drop(&mut x);
}

@Ericson2314
Copy link
Contributor

Ericson2314 commented Jun 6, 2016

@ubsan and I had talked a bit about &move on IRC, but I am wary of extending the language if we are still lift with unsafety/magic. So yes &move alone is still useful (e.g. for drop in place) but we ought to hold of on DerefMove until it can be done right.

My "4 types of unique borrowed pointer" proposal is really less about pointers and more about making "initializedness" first-class---part of the type system instead some extra static analysis tacked onto borrow checking. Once you have that, the generalized pointer types are fairly inevitable.

@strega-nil
Copy link
Author

strega-nil commented Jun 6, 2016

@arielb1 well, in that case, it's not unsafe :)

Notice how you're deref_moveing in drop(x.1). That would turn into

let inner = x.deref_move();
drop(inner.1); 
// drop glue
drop(inner.0);
Drop::drop(&mut x);

@Ericson2314 see above ^

@strega-nil
Copy link
Author

So there's gotta be some sort of guarantee about only being called once, I'm thinking... something like that.

@nikomatsakis
Copy link
Contributor

I have just opened #1643, which proposes that rather than adopting one RFC, we adopt a "strike-team-based" approach to work these rules out in a more systematic way.

@arielb1
Copy link
Contributor

arielb1 commented Jun 9, 2016

@ubsan

I think we want the DerefPure (https://internals.rust-lang.org/t/pre-rfc-box-patterns-and-derefpure/2080) unsafe trait, and to not allow using DerefMove unless it is implemented.

@strega-nil
Copy link
Author

@arielb1

But why? DerefMove seems fine if we just... don't call DerefMove multiple times. I think it's actually a really nice guarantee that DerefMove doesn't get called multiple times, just like Drop.

@strega-nil
Copy link
Author

This allows for things like dropping in DerefMove, since it's only called once. If, for example, you have a cache, and you don't need it once you're moved out of, it would be nice to get rid of it once you're no longer useful; and, I don't see the point of requiring DerefPure. It just makes it more annoying to implement DerefMove for users.

@RalfJung
Copy link
Member

@Ericson2314

Back on topic, I'm still confused on ZST-deref being unsafe. IMO, *x: () should be safe, *Void: () should not. This is one of the reasons I want Void-iso types to have size -∞.

Most of the discussion here is well beyond the scope currently covered by my formalization, but this one part is actually already "in scope" and I have definitions which I think make sense. So I will try to share my formal thoughts on this. Funny enough, the outcome is exactly the opposite if what you suggest.

Let me try to explain the relevant parts of my model of types:
A type is a set of lists of values. Values are, for example, integers, addresses (i.e., memory locations), or booleans. These lists describe the layout of the type in memory. This is a very abstract model that does not distinguish integers of different size, or the fact that integers actually overflow. All these issues can be modeled faithfully, but they are mostly irrelevant for what I am interested in right now, so I am making my life simpler.
For example, the type i32 is the set of singleton lists whose only element is an integer.
The type struct { i32, bool } is the set of two-element lists, the first element being a number and the second being a boolean. The type () is the set consisting only of the empty list. The type Void is the empty set. Every type has a size, which is the length of the lists in the set. (They all have to have the same length -- no unsized types so far.) Clearly, the size of () must be 0. The size of Void can be anything, since there is no list in that set.

The type &mut 'a T is (very roughly speaking) the set of singleton lists whose element is a location l. Furthermore, the locations in [l, l+T.size) (left-inclusive, right-exclusive) have to be valid, allocated addresses that we can access and mutate for the duration of lifetime 'a, and the list of values stored at these addresses must be in T. (I am using mutable borrows here because they are way simpler than shared borrows. I hope to write a blog post some point on why that is, I "just" need to come up with a nice explanation...)

So, if we consider &mut 'a (), what do we have? We have a location l such that [l, l+0) are valid addresses, and the list of values stored at these locations is in the set representing (). This interval is empty. Hence we do actually not know anything about the location, so in particular, we must not dereference it.

Let's consider &mut 'a Void. We have a location l such that [l, l+Void.size) are valid addresses, and the list of values stored at these locations is in the set representing Void. That's the empty set. There cannot be anything in the empty set, so we have a contradiction. From this, we can derive anything, and hence in particular, we are allowed to dereference the pointer.
What I am saying here, essentially, is that &mut 'a Void is actually the same type as Void in the sense that there is no possible value of that type, and hence we can always assume during compilation that no value of type &mut 'a Void exists. (This is actually assuming that 'a is an active lifetime.) Because of this, you can do literally anything if you have a value of type around; that's unreachable code.

@eternaleye
Copy link

@RalfJung But the topic was *mut Void, not &'a mut Void - does the same reasoning apply? If so, wouldn't that make using it for FFI pointers violently invalid?

@eddyb
Copy link
Member

eddyb commented Jun 14, 2016

@RalfJung I believe there's a distinction to be made here that &mut Void is Void but *mut Void isn't.
Specifically, one can create a *mut Void in safe code, but dereferencing it (in unsafe code) creates a value of type Void, which can make reachable code behave as if it wasn't (literally "invoking" UB).

About ZSTs, I believe you're talking about memory reads, not syntactical dereferences, which do not touch memory at all to "read" a ZST.
Sorry if I sound like a broken record, but this distinction in general can lead to a lot of confusion, because *ptr is a memory no-op in C, C++ and Rust (the latter 2 only if not overloaded) in that it converts a pointer rvalue into an lvalue while performing no memory access, but even if, say, &*ptr == ptr can be considered to be trivially true, AFAIK syntactical dereference can be UB in C if ptr == NULL.

@Ericson2314
Copy link
Contributor

@RalfJung still reading the rest of your post, but btw I totally agree that &mut is way easier to understand deeply. In https://internals.rust-lang.org/t/a-stateful-mir-for-rust/3596/15 (which you might be interested in anyways :)) I have a pretty good idea of what I will write for &mut but much less so for &.

@Ericson2314
Copy link
Contributor

Ah OK, so even besides @eddyb's concerns I consider a load from &mut () to be safe because that load must have size 0, so the load is actually a no-op. Totally agree &mut Void is absurd.

Also I'd like to point out that @RalfJung's model is another good reason why the conflation of ()-isomorphic and Void-isomorphic types as both having size 0 is a dangerous thing to do.

@strega-nil
Copy link
Author

@Ericson2314 To be specific, lvalue->rvalue conversion for a () is a no-op, whereas lvalue->rvalue conversion for a ! is undefined behavior (I believe I can start using ! now as the Void type, it seems that will go through).

@RalfJung
Copy link
Member

RalfJung commented Jun 15, 2016

@eternaleye

But the topic was *mut Void, not &'a mut Void - does the same reasoning apply? If so, wouldn't that make using it for FFI pointers violently invalid?

Oh, I see. As far as I can tell, Rust attaches absolutely no guarantees to raw pointers, so their dereferencability depends entirely on the meaning you assign to them. If your invariants ensure that the given *mut T points to X bytes of valid memory, then sure you can make use of that. Of course, if you put the result into a variable of type T, you have to make sure it is a valid T. So, you better don't dereference the *mut Void, and (naturally) there is a manual proof obligation when creating a *mut Void that this pointer actually satisfies the guarantees you'd like to attach to it.

EDIT: I should add that, for this reasoning, I think using *mut () makes more sense. If you accidentally dereference it, you don't invoke UB. Also, () in Rust corresponds exactly to *void in C, so *mut () is like *void, which is typically used for "pointers to something" in C.

@eddyb

Sorry if I sound like a broken record, but this distinction in general can lead to a lot of confusion, because _ptr is a memory no-op in C, C++ and Rust (the latter 2 only if not overloaded) in that it converts a pointer rvalue into an lvalue while performing no memory access, but even if, say, &_ptr == ptr can be considered to be trivially true, AFAIK syntactical dereference can be UB in C if ptr == NULL.

Oh, the good ol' lvalue-rvalue confusion. I am entirely sidestepping that issue in my formalization by not having lvalues, which is why I payed no attention to it. ;-) Anyway, when I wrote about dereferencing pointers above, I was talking about actual load operations that happen on the machine.

@Ericson2314

Also I'd like to point out that @RalfJung's model is another good reason why the conflation of ()-isomorphic and Void-isomorphic types as both having size 0 is a dangerous thing to do.

I am not sure that is the case. ;-) Actually, I did not even assume any particular size for ! above -- the reasoning works for any size. Truth is, the size of ! could be literally anything, it just doesn't matter as the type is not inhabited. I doubt it is worth complicating the algebra of sizes for this corner-case. It would be nice to have things like Option<!> having the same representation as (), but that won't fall out of any particular size for ! -- instead, that'd be an extension of the optimizations for enum layout that we already have (e.g., Option<&T> having the pointer size, without an added discriminant).

@ubsan

To be specific, lvalue->rvalue conversion for a () is a no-op, whereas lvalue->rvalue conversion for a ! is undefined behavior (I believe I can start using ! now as the Void type, it seems that will go through).

I would say that lvalue -> rvalue conversion for ! assumes that we have a !, so this cannot even happen. That's not UB, that's just impossible -- the UB happened earlier, when we obtained a value of type !. But the consequence is the same, the compiler is free to emit literally any assembly code when anything is done to a !.

@strega-nil
Copy link
Author

strega-nil commented Jun 15, 2016

@RalfJung The issue is raw pointers, where *mut ! is a valid type, despite being a handle of an impossible lvalue.

@RalfJung
Copy link
Member

@ubsan Right, as long as you don't dereference it. Whether &*x is already UB, I am not so sure... I would tend to argue it should not be.

@glaebhoerl
Copy link
Contributor

Quoting my comment from #1216 with respect to using *mut ! for things like FFI types:

Given that the contract of *const T/*mut T is that it shall only be dereferenced when it actually points to valid/live data (the type system can't know this, which is why it's unsafe; it's entirely up to the user to determine this): I think *mut UninhabitedType, with the understanding that it will never be dereferenced, and *mut UnitType, with the understanding that it can be dereferenced whenever but there's no point in doing so, make equal amounts of sense.

(In other words, this is unlike &mut UninhabitedType, which is logically equivalent to UninhabitedType itself, because *mut doesn't imply liveness.)

@arielb1
Copy link
Contributor

arielb1 commented Jun 15, 2016

@RalfJung

I am not sure about &*bot, but a use (aka lvalue-to-rvalue conversion) of a Void is UB.

@RalfJung
Copy link
Member

Agreed. I'd argue that &*x is a noop (when translating to an lvalue-free semantics, it compiles to x). But if an actual memory access happens, we get UB.

@strega-nil
Copy link
Author

strega-nil commented Jun 15, 2016

@RalfJung But having &! is UB, so &*x is undefined behavior, because it creates an &!.

@DemiMarie
Copy link

DemiMarie commented Jun 26, 2016

Here are some of my own thoughts (not an expert, but am working on a garbage collector):

  • The memory model needs to be formal. Otherwise you can't formally verify unsafe Rust code, which is a use case that (at least) Redox definitely wants.
  • We need to avoid falling into the trap that C/C++ fell into with strict aliasing, which as I understand it breaks so much code that major compilers implement -fno-strict-aliasing and major projects rely on this (the OCaml runtime, CPython 2.x, and the Linux kernel, as well as the output of an RPC compiler at a minimum), and yet as I understand it has only minimal performance gains in practice for typical code (i.e. not tight numeric loops).
  • The memory model needs to be easy for people to understand. That means that an equally normative, easy to understand prose description of the memory model needs to be present as well – it is, after all, what most people will be programming to. If the formal model and the prose model diverge, this is a bug in the spec that must be fixed.
  • It needs to be possible to do what needs to be done without unnecessary copies. In C++, the only standards-conforming way to pass the buffer stored in an std::string to a function that takes an unsigned char * is to copy the entire buffer!. This is clearly absurd.
  • The memory model needs to support writing memory allocators, garbage collectors, etc. without too much boilerplate or pitfalls beyond what C would have. If the Rust version of some code is twice as long as the C version (with -fno-strict-aliasing) and is much harder to read, then that is a problem with Rust.
  • Aliasing *mut pointers should be permitted. Furthermore, it should be possible for an &mut or & pointer to alias a *const or *mut pointer, so long as the pointee does not change (in the case of a &-pointer) or is only modified through the reference (in the case of an &mut) for as long as the reference is live. Otherwise, one runs into ugly hacks such as wrapping every element of a struct in a Cell.
  • In multithreaded code, a common type of data race is two threads racing to write the same value to the same address, with no racing reads. Such races should be allowed, as the operation of writing a value to an address is idempotent. Similarly, the case where two threads simultaneously read a value from an address, set and/or clear a bit, and then write the new value back should be allowed – again, no reordering that I can imagine would break the code, and I have read about several algorithms (garbage collectors, if I recall correctly) that rely in this to work.

@nrc
Copy link
Member

nrc commented Aug 25, 2016

Nominating for discussion at lang-team meeting - the unsafe guidelines team exists now, so we should consider moving this PR either somewhere else or just the team tag.

@nikomatsakis
Copy link
Contributor

nikomatsakis commented Aug 31, 2016

I agree with @nrc that, given that we have accepted #1643, we should close this RFC, since its contents are subsumed by the discussion taking place at the unsafe guidelines repo.

I reviewing the thread and have tried to extract out the most notable questions that were debated. It would make sense to move many of these to issues on the rust-memory-model repo, I think:

@rust-lang/lang members, please check off your name to signal agreement. Leave a comment with concerns or objections. Others, please leave comments. Thanks!

@strega-nil
Copy link
Author

I'm just going to close it now, since I owns it :P

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
T-lang Relevant to the language team, which will review and decide on the RFC.
Projects
None yet
Development

Successfully merging this pull request may close these issues.