Repository navigation
What's the source of immutability for pointers produced by const? #502
Description
If a const (item or expression) contains references and pointers, then what is their mutability (or more generally, their status in the aliasing model)?
It seems fairly clear that we want them to be immutable. It's called "const(ant)" after all, and also constants are values but we deduplicate the underlying storage, which would be bad news if anything is mutable.
However, due to the troublemaker lifetime extension, this is not trivial at all: a single const/static item can generate a whole bunch of allocations, and the user may expect some of them to be mutable. (In fact, for static with interior mutability, the root allocation of the static will indeed be mutable!)
One option is to make this an implementation detail: const-eval tracks the actual mutability of a pointer, and interning bails out if a mutable pointer makes it into the final value. That means all the pointers in the final value are anyway already immutable.
But there's an alternative: we can say that the transition from a const result to a value embedded in other computations (that may themselves be const computations, or they may happen at runtime) makes all pointers (or, equivalently, the memory they point to) immutable.
This has several advantages:
- regression: encountered mutable pointer in final value when "outer scope" rule applies in const/static with interior mutability rust#121610 is resolved, since we could just remove the check that triggers the regression.
- Promotion introduces UB into otherwise well-defined code #493 has a path forward, if we can figure out how to do an edition transition where the problematic promoteds are turned into inline const expressions.
const_allocateresults can more easily be used to create constants without running into issues like this.
It also has several downsides:
- We have a second source of immutability constraints, aside from shared references. So if people don't expect that they may cause UB. (A mitigating factor here is that writing to the memory that contains a const should segfault pretty reliably on most targets, as that memory is typically mapped read-only. So this UB will often be detected.)
- The check we'd have to remove was meant as a safety net for
&mutvalues in consts. If an&mutvalue ends up in the result of a const, the new rules would say that despite it being a mutable reference, it actually is an immutable pointer. We can catch some cases where that happens, but if people get creative and e.g. smuggle out an&mutinside aMaybeUninit, there's a chance to cause UB.
The downsides are why I added these checks in the first place, and they make me hesitant to un-do all that work. OTOH I did not see #493 coming, and it's undeniably useful to be able to do const { &None } even when this is an Option<Cell<T>>.
The current wording in the reference does explicitly call out const values and promoteds as sources of immutability, i.e., it implements the 2nd option. This issue is about whether we are happy with that, or whether we should try to transition to the first option. I don't know if that is even possible, though...
Cc @rust-lang/wg-const-eval, this issue is an the intersection of opsem and const-eval.
Activity
chorman0773 commentedon Apr 1, 2024
My idea is that the
const-AMand runtime-AM are two separate impls that can only pass allocations and values to each other, and any allocation passed from the const-AM to the runtime-AM is read-only in the runtime-AM.RalfJung commentedon Apr 1, 2024
That's definitely not entirely true, as the const-AM also produces
staticallocations with interior mutability that must be mutable in the runtime-AM. So really there's a flag in each allocation that gets passed over that indicates whether it is read-only or not. That's basically what rustc does.But I was hoping that we wouldn't have to talk about this in the spec and could derive it all from shared references being immutable...
joboet commentedon Apr 1, 2024
That's not quite true,
addr_of!also adds an immutability constraint, even though*constpointers don't.RalfJung commentedon Apr 1, 2024
That's a quirk of Stacked Borrows that is not shared by Tree Borrows and that I think we want to get rid of.
For the purpose of SB, we do consider that code to implicitly create a shared reference (
addr_of!(val)becomes basicallyaddr_of!(*&val)), so shared references are still the only source of immutability.joboet commentedon Apr 1, 2024
This is already possible in tree borrows, because it allows
transmute<&T, &mut T>. So you could doto get the exact same effect as
IMHO both should be allowed.
RalfJung commentedon Apr 1, 2024
That text you quote was talking about
This is currently caught by const-checking but the way it works is somewhat indirect and relies on non-local invariants about the structure of MIR, so I'd prefer for it not to be soundness-bearing. Also
-Zunleash-the-miri-inside-of-youmakes const-checking accept that code, and we want that flag to be sound.Right now it's sound because the mutability check at interning time catches this. If we remove this check, we need an argument for why it is UB to write to that reference.
joboet commentedon Apr 1, 2024
How about making a rule that at the end of each initializer scope, an implicit reference is performed to every byte of every constant allocation, be that the main
const, promoted values orconst_allocatevalues, thereby turning every active pointer frozen.RalfJung commentedon Apr 1, 2024
Creating a reference doesn't make that memory immutable for all accesses. It's only memory accesses through that reference (and everything derived from it) that become immutable.
Creating a reference and then immediately discarding it and never using it again is a NOP. (Well, almost: it's a read access, but that's it. It has no lasting effect.)
RalfJung commentedon Apr 1, 2024
FWIW, the reference already contains wording like this:
I always considered that temporary, and intended to remove it once we have figured out our aliasing story. But this means that nominally, "all pointers that come out of
constare immutable" is already the case, we'd just have to re-affirm that yes this is what we want even if the point would be mutable if the const expression was inlined at its use site.joboet commentedon Apr 1, 2024
But all mutable pointers in the
constwould become frozen, which is exactly what we want, right? And it's not possible to create a mutable pointer to the data after that.RalfJung commentedon Apr 1, 2024
No, why would they?
Under Tree Borrows some mutable pointers would get frozen, namely if the access is done with a foreign tag and the pointer is already activated (in the 2-phase sense). But I don't see why this would apply to all pointers.
If we do a magic thing with everything that comes out of a
const, IMO it makes much more sense to directly make it do what we want (in Tree Borrows terms: change all permissions to frozen) rather than trying to be clever and do something indirect that hopefully maybe has the same effect.joboet commentedon Apr 1, 2024
I meant a foreign read like so:
becomes
Phrasing it like this means that we don't need to introduce a new source of immutability. But the specific wording doesn't matter, as you point out.
Related: can this code be made to compile?
RalfJung commentedon Apr 1, 2024
Let's not discuss const_allocate here, that has a too high risk of derailing the discussion. The tracking issue is at rust-lang/rust#79597.
RalfJung commentedon Apr 1, 2024
We do, though. If
PTRis just the value returned byconst_allocate, without ever writing to it, then Tree Borrows would permit writes through PTR even after those reads frommagically_remembered_pointer. That's what I said above.joboet commentedon Apr 1, 2024
Ah yes, whoops. Please think of
magically_remembered_pointer } as *mut i32;in the above as
RalfJung commentedon Apr 1, 2024
Yeah that would work. But I think we went far enough down this rabbit hole to also demonstrate why I don't think that is the most elegant approach. ;)
joboet commentedon Apr 1, 2024
What are you talking about, the above is a totally elegant and clean way to solve this 😄. I just wanted to slightly weaken the downsides you pointed out, cause I'd love to have this. As rust-lang/rust#123254 demonstrated, working around the current limitations just leads to way worse code.
RalfJung commentedon Apr 1, 2024
rust-lang/rust#123254 needs const_heap though so that's anyway somewhat of a different topic. If we remain strict on immutability we could add a
const_make_immutableintrinsic that you must call on heap allocations that reach the final value of a const to explicitly say that you are okay with them becoming immutable.Ddystopia commentedon Jul 15, 2024
Aren't we talking about pointers inside
constitem? If you have mutable pointer without write permissions to some mutable memory, then it will be UB but will not result in a write to read-only memory