Skip to content

Note in no-mutable-refs in consts is confusing #2074

Description

@ehuss

This note is a bit confusing since it does not relate back to what we said above about why these references are disallowed.

It should probably also say that this is sound to allow because it take an unsafe block to create a mutable reference to such a static, and that unsafe block carries the obligation of ensuring that no unsound aliasing will occur, Furthermore, the problem regarding "is there a new instance of the pointee for every use of the const or not" does not come up since unambiguously, these references all point to S.

Originally posted by @RalfJung in #2058

Activity

  1. RalfJung commented on Nov 4, 2025

    @RalfJung
    Member

    Note that there's two of these notes: for mutable statics and extern statics.

  2. RalfJung commented on Nov 7, 2025

    @RalfJung
    Member

    After being probed by @traviscross it occurred to me that this example is actually sound in the sense that surrounding safe code can't do anything wrong:

    #![allow(static_mut_refs)]
    static mut S: &mut u8 = unsafe { static mut I: u8 = 0; &mut I };
    const C: &&mut u8 = unsafe { &S }; // OK.

    This is because &&mut u8 is pretty much equivalent to &&u8.

    Now I wonder whether that is generally the case or not...


    My main point is that it is unclear why "it's separately not allowed to read from a mutable static during constant evaluation" implies that we can allow this const _: &&mut u8.

  3. traviscross commented on Nov 7, 2025

    @traviscross
    Contributor

    Notably, we don't allow this:

    #![allow(static_mut_refs)]
    static S: &mut u8 = unsafe { static mut I: u8 = 0; &mut I };
    const _: &&mut u8 = &S;
    //~^ error[E0080]: constructing invalid value at .<deref>: encountered mutable reference in `const` value

    It seems as though the common thread in allowing the reference to the &mut in the mutable static and the reference to the &mut in the extern static but not the reference to the &mut in the non-mut static is the property mentioned in the Reference, even though I agree that doesn't seem like a particularly good reason as a language matter.

  4. traviscross commented on Nov 7, 2025

    @traviscross
    Contributor

    The language in the Reference, by the way, came from what you had said on Zulip:

    for the last example, apparently we stop checking when encountering a mutable static, because you anyway cant read from mutable statics during const-eval

  5. RalfJung commented on Nov 7, 2025

    @RalfJung
    Member

    Notably, we don't allow this:

    Yeah, we recurse into immutable statics. That is also how we find e.g.

    static S: u8 = 2;
    const C: &bool = unsafe { std::mem::transmute(&S) };

    The original motivation was avoiding a later error if C gets used as pattern, but given that we now allow this, that motivation doesn't really apply any more:

    #![allow(static_mut_refs)]
    static mut S: u8 = 2;
    const C: &bool = unsafe { std::mem::transmute(&S) };

    So it's more "because we can" / "because it finds more UB" -- but this same recursive traversal checks both for UB things like invalid booleans and more fuzzy things like mutable references in consts.

    The language in the Reference, by the way, came from what you had said on Zulip. You had said:

    I figured, but that wasn't meant to be language that can go in a reference. ;) It was just to explain to the people in the thread why rustc behaves this way.

    Cc @rust-lang/wg-const-eval

  6. traviscross commented on Nov 7, 2025

    @traviscross
    Contributor

    I figured, but that wasn't meant to be language that can go in a reference. ;) It was just to explain to the people in the thread why rustc behaves this way.

    We do, I think, in these kind of admonitions want to document the "real" reason, even if it's kind of bogus. I had understood you meant this as the mechanical reason when adding it. As we've talked about, a lot of the rules here are lacking the kind of deeper rationales we'd really like. We could of course add more verbiage to make it more clear this reason is mechanical and not fundamental. That's probably the thing to do.

    And then, of course, let's find more principled rules and lang FCP those stabilizations.

  7. theemathas commented on Nov 7, 2025

    @theemathas
    Contributor

    I think the key thing is: If a const were to be codegenned multiple times, then some promoteds would end up in multiple memory addresses. We should disallow mutable references (that point to 1 or more bytes) from being stored in these potentially duplicated promoteds.

  8. RalfJung commented on Nov 7, 2025

    @RalfJung
    Member

    @theemathas why do you focus on promoteds? A mutable reference directly inside the const has the same issue. The archetype of what we have to prevent is

    const C: &mut i32 = &mut 0;

    There's no promotion involved here; this gets accepted by borrowck based on temporary lifetime extension.

    We don't promote mutable references (except for &mut []) so promoteds are actually no problem I think.

  9. theemathas commented on Nov 7, 2025

    @theemathas
    Contributor

    My bad. Let me correct myself.

    We should disallow mutable references (that point to 1 or more bytes) from being stored in these potentially duplicated promoteds or directly in a const.

    When I talk about promoteds, I have in mind code like this:

    static mut X: i32 = 1;
    
    #[allow(static_mut_refs)]
    const Y: &&i32 = unsafe { &&X };
    
    const Z: &&mut i32 = unsafe { std::mem::transmute(Y) };
  10. theemathas commented on Nov 7, 2025

    @theemathas
    Contributor

    Also disallow &mut references in potentially duplicated things that are lifetime-extended into promoted-like things, whatever you call them.

  11. RalfJung commented on Nov 7, 2025

    @RalfJung
    Member

    When I talk about promoteds, I have in mind code like this:

    I don't think there's a promoted here either, but this has one:

    const Y: &&i32 = &&0; // the inner `&0` gets promoted
    
    const Z: &&mut i32 = unsafe { std::mem::transmute(Y) };

    However, again I think just having those two consts is sound. &&T and &&mut T are basically the same type, it would be sound to have a safe function transmuting between them.

    That said I agree we should reject them. What we currently do is traverse the const value, recursively following references. We also do recurse into immutable statics but not into mutable or extern statics. It seems fairly safe to just stop traversing into statics entirely because at that point there is no ambiguity about place identity any more, the static has already been validity-checked (and if you used unsafe code to access the static at a different type that's on you), and we already deliberately gave up on the property that "every const of eligible type can be used as a pattern".

    The rules around mutability are still quite ad-hoc in other ways (such as the exception for &mut to a zero-sized allocation), but at least the treatment of statics would be more sane.

  12. theemathas commented on Nov 7, 2025

    @theemathas
    Contributor

    @RalfJung I didn't use code like yours because it is currently rejected due to "encountered mutable reference or box pointing to read-only memory".

    Maybe this would be clearer on what I meant:

    static mut X: i32 = 1;
    
    const Y: &*mut i32 = {
        let temp = &const { &raw mut X };
        temp
    };
    
    const Z: &&mut i32 = unsafe { std::mem::transmute(Y) };
  13. RalfJung commented on Nov 7, 2025

    @RalfJung
    Member

    "encountered mutable reference or box pointing to read-only memory"

    FWIW that's also a check where I am not sure we strictly need it. OTOH it also seems unclear why we'd allow this.

    And anyway both the "mutable ref in final value" and "mutable ref to read-only memory" only actually check &mut, they ignore &!Freeze. So it's more of a sanity check than something we can really rely on.

  14. theemathas commented on Nov 7, 2025

    @theemathas
    Contributor

    @RalfJung There is actually a check for !Freeze.

    const A: &std::cell::Cell<i32> = unsafe { std::mem::transmute(&0) };
    error[E0080]: constructing invalid value at .<deref>.value: encountered `UnsafeCell` in read-only memory
     --> src/lib.rs:1:1
      |
    1 | const A: &std::cell::Cell<i32> = unsafe { std::mem::transmute(&0) };
      | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ it is undefined behavior to use this value
      |
      = note: the rules on what exactly is undefined behavior aren't clear, so this check might be overzealous. Please open an issue on the rustc repository if you believe it should not be considered undefined behavior.
      = note: the raw bytes of the constant (size: 8, align: 8) {
                  ╾─────alloc3<imm>─────╼                         │ ╾──────╼
              }
    
    For more information about this error, try `rustc --explain E0080`.
    
  15. RalfJung commented on Nov 7, 2025

    @RalfJung
    Member

    That's not a check for !Freeze, that's a check for UnsafeCell. If, during our traversal, we find an UnsafeCell in an immutable allocation (and that's not the "root" allocation of a const, which is immutable for other reasons), we error. Another somewhat ad-hoc check that however also seems unlikely to fire for any legitimate code.

    If you make this an &Option<Cell> with value None, you can see that we only complain if there is truly an UnsafeCell there, i.e. this is not based on the type of the reference but on the concrete value it points to. Also as a special case we don't complain about UnsafeCell<()> (or other ZST).

  16. traviscross commented on Nov 7, 2025

    @traviscross
    Contributor

    Interestingly, we do accept:

    #![allow(static_mut_refs)]
    use core::cell::UnsafeCell;
    struct W<T>(UnsafeCell<T>);
    unsafe impl<T> Sync for W<T> {}
    
    static S: W<&mut u8> = {
        static mut I: u8 = 0; W(UnsafeCell::new(unsafe { &mut I }))
    };
    
    const _: &W<&mut u8> = &S;

    (I recently posted this elsewhere, but catching up on this thread, it seems relevant here too.)

  17. RalfJung commented on Nov 7, 2025

    @RalfJung
    Member

    As I replied elsewhere -- S is a mutable static, because its backing store is mutable. There's not a big difference between a static mut and an interior mutable static.

  18. added a commit that references this issue on Nov 29, 2025
  19. added a commit that references this issue on Nov 29, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions