Repository navigation
Differences between *const T and *mut T. Initially *const T pointers are forever read-only? #257
Description
I hadn't seen this but it's very surprising and should be documented better. Apparently, *mut T and *const T aren't equivalent — if the raw pointer starts out as a *const T it will always be illegal to write to, even nothing beyond the pointer's "memory" of its initial state is the reason for this.
See: rust-lang/rust-clippy#4774 (comment)
This is not entirely correct... something like &mut foo as *mut T as *const T as *mut T is entirely harmless. What is relevant is the initial cast, when a reference is turned to a raw pointer. I think of the pointer as "crossing into another domain", that of uncontrolled raw accesses. If that initial transition is a *const, then the entire "domain" gets marked as read-only (modulo UnsafeCell). The raw ptrs basically "remember" the way that the first raw ptr got created.
This is extremely surprising, as lots of documentation and common wisdom indicates that *const T vs *mut T are identical except as a sort of lint, and that the variance is different.
In fact, often having correct variance in your types often forces using *const even for mutable data (hence NonNull uses const). The prevalence of this certainly helps contribute to programmers belief that there's no meaningful difference, you just have to be sure the data you write to is legal for you to write to.
A common case where this happens is if you write a helper method to return a pointer, you might write this just once for the *const T case, and use it even if you're a &mut self and need a *mut T result. I wouldn't think twice about this, mostly because the myth they're equivalent is so widespread
Ralf's comment here even further propagates this myth, in a thread explicitly asking about the differences here... https://internals.rust-lang.org/t/what-is-the-real-difference-between-const-t-and-mut-t-raw-pointers/6127/18 :
I agree. *const T and *mut T are equivalent in terms of UB.
More broadly, nowhere in the thread does the 'once *const, always *const' behavior come up, just that you need to make sure that you maintain the normal rust rules (e.g. the rules which would apply had you started out with a *mut T).
I looked and in none of Rust's reference material could I find any mention of behavior like this. This is very surprising, and I had been under the impression that optimising accesses to raw pointers wasn't beneficial enough for Rust to care strongly about them.
I also think this breaks a lot of existing unsafe code given how widespread the belief that they are equivalent is, and makes non-const-correct C libraries much thornier to bind to :(
Activity
elichai commentedon Nov 11, 2020
These statements aren't true. the only thing that matters is how did you get the pointer, for example this is 100% correct rust code:
So even though it started as a
*const u8you're still allowed to write into it, because you got the pointer from a unique(mut) reference and not a shared referenceRalfJung commentedon Nov 11, 2020
This is, I think, a duplicate of #227. I agree it is a problem. I just do not know a good solution.
The subtle aspect of this is that
x as *const _is basically the same as&*x as *const _, i.e.,as *const _always goes through a shared reference.elichai commentedon Nov 11, 2020
Ohhh that's what he was talking about, I'm sorry I misunderstood you @thomcc
mversic commentedon Nov 17, 2020
I hope I'm not off topic. I don't think it is, since
NonNullis a wrapper over*constNonNulldocumentation states:I've also found this post which basically asserts that it is entirely ok to use
NonNullin FFI. If pointer is nullable then I see no special benefit in usingOption<NonNull<T>>, I would just use*mut T. However, I'm interested to useNonNull<T>for non nullable pointers(i.e pointers for which C documentation explicitly states null value must not be provided) as it would provide additional type safety. This is just for the scenario where Rust code is calling C code, not vice versa.And, now I'm confused :)
mversic commentedon Nov 17, 2020
hm, we could say that this isn't a
NonNullrelated issue. We can still have the same issue in FFI in this scenario:this is said to be a UB as well
therefore I find that using
NonNull<T>is as good as using*mut Tconsidering the risk of UB. Maybe it's a little better since documentation states the risk of UBDiggsey commentedon Nov 18, 2020
AIUI, this actually has little to do with
*constvs*mutand is about whether the pointer provenance is a&or a&mut(or no provenance).The only tricky part is the point @RalfJung mentioned when casting directly from a
&mutto a*constwhere a reference is implicitly created.One option would be to warn on this direct cast (
&mut->*const) (in the next edition if that would be too noisy) and require that the&mut->*mut->*constvs&mut->&->*constpath is explicitly distinguished.Then you can be safe in treating
*constand*mutthe same.RustyYato commentedon Nov 29, 2020
Could we change
&mut T as *const Tto not go through a shared reference? Getting rid of the implicit footgun.RalfJung commentedon Nov 29, 2020
The tricky bit would be to keep this code working:
Currently this works because
x as *const _is considered a read-only access.OTOH, we do reject the
as *mutversion of this. If we want to treatas *mutandas *constthe same, accepting one and rejecting the other makes little sense.GoldsteinE commentedon Jul 26, 2022
How does
addr_of!()affects this? This code:creates a
*const i32without creating&i32first and currently triggers Miri.addr_of!()documentation doesn’t mention that resulting pointer can’t be casted to*mut Tand used for writes though.RalfJung commentedon Jul 26, 2022
Indeed, that's how it currently affects addr_of.
True. It also doesn't say that you can do that. The docs are not exhaustive for what you cannot do. (That would require infinitely large docs.)
There has not been a decision on what the semantics should be here, and that's why the docs basically don't talk about this. It's not great, but absent a decision it's also not clear what else to do. And making the decision without having an entire aliasing model for all the context isn't really a good idea either.
GoldsteinE commentedon Jul 27, 2022
It’s true. The way “validity” is currently defined in the standard library docs doesn’t guarantee that any use of pointers from
addr_of!()(oraddr_of_mut!(), for that matter) is valid.Is there a rationale for making
addr_of!()-produced pointers invalid for writes? I think it’s kind of confusing and doesn’t match the general intuition that*const _and*mut _raw pointers are interchangeable.bjorn3 commentedon Jul 27, 2022
If you want to write through the pointer you would use
addr_of_mut!(), right? Otherwise what is the point of having two separate macros?It actually does under the examples section of the
addr_of!()documentation:RalfJung commentedon Jul 30, 2022
I feel quoted out of context here -- for the question raised in that particular thread, my statement holds true. But specifically when converting a reference to a raw pointer, there is a difference.
Basically, because it matches what the borrow checker does -- see here further up this thread.
Rollup merge of rust-lang#107954 - RalfJung:tree-borrows-fix, r=m-ou-se
Rollup merge of rust-lang#107954 - RalfJung:tree-borrows-fix, r=m-ou-se
10 remaining items
RalfJung commentedon May 14, 2024
Closure capture is an interesting one. The consistent capture mode would be
&mut, but that's probably also surprising.But really the main point to me is that
&raw const *raw_mut_pointer(andraw_mut_ptr as *const _, which compiles to the same MIR) should not lose an existing write permission -- I assume we have consensus on that? Having&raw constdo different things to the permission depending on the shape of the place expression that follows is a non-compositional nightmare (and I've had to spend my share of time just dealing with that nightmare in Miri; it's particularly bad forBox).celinval commentedon Jun 28, 2024
I'm curious... what is the point of having two types
*const Tand*mut Tif they behave the same way?As a developer, if I call a function that takes
*const T, I expect that function to never change the value of that variable, even if my original variable is mutable.chorman0773 commentedon Jun 28, 2024
Well, it's intended as an indicator to programmers mostly.
chorman0773 commentedon Jun 28, 2024
Using a
&mutcapture would also alter well-formedness (degrade from Fn() to FnMut(), and also borrow the type mutably).RalfJung commentedon Jun 29, 2024
We also have
*const i32and*const u32even though they behave in the same way -- or rather, opsem doesn't make a difference between them. Both the pointee type and mutability are hints for the intended use of this pointer, but not hard guarantees/constraints.Auto merge of rust-lang#127679 - RalfJung:raw_ref_op, r=<try>
Auto merge of rust-lang#127679 - RalfJung:raw_ref_op, r=<try>
Rollup merge of rust-lang#127679 - RalfJung:raw_ref_op, r=traviscross…
Rollup merge of rust-lang#127679 - RalfJung:raw_ref_op, r=jieyouxu
Unrolled build for rust-lang#127679
Rollup merge of #127679 - RalfJung:raw_ref_op, r=jieyouxu
Rollup merge of #127679 - RalfJung:raw_ref_op, r=jieyouxu
Rollup merge of #127679 - RalfJung:raw_ref_op, r=jieyouxu