rust-lang / unsafe-code-guidelines

Forum for discussion about what unsafe code can and can't do

Home Page:https://rust-lang.github.io/unsafe-code-guidelines

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

What's the source of immutability for pointers produced by `const`?

RalfJung opened this issue · comments

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.

Currently we're doing our best 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:

  • rust-lang/rust#121610 is resolved, since we could just remove the check that triggers the regression.
  • #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_allocate results 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 &mut values in consts. If an &mut value 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 &mut inside a MaybeUninit, 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>>.

Cc @rust-lang/wg-const-eval, this issue is an the intersection of opsem and const-eval.

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.

My idea is that the const-AM and 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.

That's definitely not entirely true, as the const-AM also produces static allocations 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...

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.)

That's not quite true, addr_of! also adds an immutability constraint, even though *const pointers don't.

That's not quite true, addr_of! also adds an immutability constraint, even though *const pointers don't.

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 basically addr_of!(*&val)), so shared references are still the only source of immutability.

If an &mut value 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.

This is already possible in tree borrows, because it allows transmute<&T, &mut T>. So you could do

const CONSTANT: &i32 = &42;

unsafe { transmute::<&i32, &mut i32>(CONSTANT) }

to get the exact same effect as

const MUTABLE: &mut i32 = unsafe { transmute(&42) };

MUTABLE

IMHO both should be allowed.

That text you quote was talking about

const MUTABLE: &mut i32 = &mut 15;

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-you makes 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.

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 or const_allocate values, thereby turning every active pointer frozen.

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.)

FWIW, the reference already contains wording like this:

Mutating immutable bytes. All bytes inside a const item are immutable.

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 const are 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.

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.)

But all mutable pointers in the const would become frozen, which is exactly what we want, right? And it's not possible to create a mutable pointer to the data after that.

But all mutable pointers in the const would become frozen,

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.

I meant a foreign read like so:

const PTR: *mut i32 = {
    let ptr = const_allocate(4, 4) as *mut i32;
    ptr.write(42);
    ptr
};

becomes

const PTR: *mut i32 = {
    let magically_remembered_pointer: *mut u8;
    let result = {
        let ptr = {
            magically_remembered_pointer = const_allocate(4, 4);
            magically_remembered_pointer
        } as *mut i32;
        ptr.write(42);
        ptr
    }
    // Magic reads:
    &*magically_remembered_pointer.add(0);
    &*magically_remembered_pointer.add(1);
    &*magically_remembered_pointer.add(2);
    &*magically_remembered_pointer.add(3);
    result
}

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?

const VAL: &'static i32 = {
    let val = Box::new_in(42, Constant);
    let val = Box::leak(val);
    &*val
};

Related: can this code be made to compile?

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.

Phrasing it like this means that we don't need to introduce a new source of immutability

We do, though. If PTR is just the value returned by const_allocate, without ever writing to it, then Tree Borrows would permit writes through PTR even after those reads from magically_remembered_pointer. That's what I said above.

Ah yes, whoops. Please think of

            magically_remembered_pointer
        } as *mut i32;

in the above as

            let guarded = (&mut *magically_remembered_pointer) as *mut u8;
            guarded.write(/* undef */);
            guarded
        } as *mut i32;

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. ;)

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.

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_immutable intrinsic 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.

(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.)

Aren't we talking about pointers inside const item? 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

static mut REALLY_MUTABLE: i32 = 0;
const PTR: *mut i32 = &mut REALLY_MUTABLE;

*PTR = 3; // PTR is read-only, but memory is not