In C, const pointers are the only way of giving immutable access to a memory buffer. In Rust we have convenient things like references (immutable (shared) and mutable (unique)), so nobody really uses pointers (really rarely and only for FFI and some unsafe focuses). So. My offer is to deprecate *const T and *mut T and replace them with mutable T*. Why?
*const pointers can be safely casted to *mut;
*mut pointers, unlike &mut references, are not unique.
Problems with rewriting low-level legacy.
Make T* an unstable feature and deprecate *const T and *mut T after it's stabilization;
While it's sound to cast a *const T to a *mut T, it's rarely safe to use that pointer mutably. Since *const T is almost always derived from a shared reference, it's almost always undefined behavior to mutate the pointed to value. Additionally, there are variance implications, since *const T is covariant over T while *mut T is invariant (NonNull<T> is also covariant). Generally speaking, *mut T represents a pointer to a mutable object and *const T represents a pointer to an immutable one. Removing one would decrease the clarity of unsafe code, and lead to lots of subtle variance related soundness bugs, and lots of less subtle "Wait I wasn't supposed to mutate with that pointer?" bugs.
While this is true, having a const vs mut distinction is helpful for tracking provenance information around mutability, particularly in cases where there's a low-level (e.g. -sys) API that takes pointers and you're writing a higher-level binding that exposes a safe API and are converting & vs &mut references to pointers in the process.
And as others have mentioned, they differ in variance. You might want to peruse this thread:
Note that that information is partially outdated. At least for now (until all the detailed rules are figured out), *const vs *mut matters on reference to raw-pointer casts. This is UB:
let mut x = 0;
let ptr = &mut x as *const i32; // notice the 'const'!
(ptr as *mut i32).write(42);
It is UB because a raw pointer initially created by as *const _ is read-only, even if it is created from a mutable reference. These might not be the rules we want forever, but it matches how the borrow checker treats reference-to-raw-pointer casts, so these are the rules we are going with for now.
That's a rather confusing rule. If it stays, it would be nice to have a deny lint or error which forbids &mut T as *const T casts entirely. If one wants a *const T, it is much more clear to cast via a &T, i.e. &mut T as &T as *const T. Alternatively, cast &mut T as *mut T as *const T, if mutability is required.
In fact, it would be nice to forbid casting both mutability and pointer at the same time, regardless whether the rule stays, to avoid such ambiguities.
What I meant was why is the example above UB when the below is normal use of NonNull and also goes via a const pointer:
let mut x = 0;
let ptr = NonNull::new_unchecked(&mut x);
However re-reading your comment and NonNull::new_unchecked, it looks like the above is OK because &mut x gets coerced to *mut i32 and only then gets cast to *const i32, so it's not "initially created by as *const _".