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

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

Open
RalfJung opened this issue Apr 1, 2024 · 18 comments
Open

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

RalfJung opened this issue Apr 1, 2024 · 18 comments
Labels
A-aliasing-model Topic: Related to the aliasing model (e.g. Stacked/Tree Borrows)

Comments

@RalfJung
Copy link
Member

RalfJung commented Apr 1, 2024

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:

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.

@RalfJung RalfJung added the A-aliasing-model Topic: Related to the aliasing model (e.g. Stacked/Tree Borrows) label Apr 1, 2024
@chorman0773
Copy link
Contributor

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.

@RalfJung
Copy link
Member Author

RalfJung commented Apr 1, 2024

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

@joboet
Copy link

joboet commented Apr 1, 2024

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.

@RalfJung
Copy link
Member Author

RalfJung commented Apr 1, 2024

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.

@joboet
Copy link

joboet commented Apr 1, 2024

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.

@RalfJung
Copy link
Member Author

RalfJung commented Apr 1, 2024

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.

@joboet
Copy link

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

@RalfJung
Copy link
Member Author

RalfJung commented 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
Copy link
Member Author

RalfJung commented Apr 1, 2024

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.

@joboet
Copy link

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

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.

@RalfJung
Copy link
Member Author

RalfJung commented Apr 1, 2024

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.

@joboet
Copy link

joboet commented Apr 1, 2024

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
};

@RalfJung
Copy link
Member Author

RalfJung commented Apr 1, 2024

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.

@RalfJung
Copy link
Member Author

RalfJung commented Apr 1, 2024

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.

@joboet
Copy link

joboet commented Apr 1, 2024

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;

@RalfJung
Copy link
Member Author

RalfJung commented 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
Copy link

joboet commented 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
Copy link
Member Author

RalfJung commented 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_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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-aliasing-model Topic: Related to the aliasing model (e.g. Stacked/Tree Borrows)
Projects
None yet
Development

No branches or pull requests

3 participants