Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions compiler/rustc_const_eval/src/const_eval/machine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -861,6 +861,36 @@ impl<'tcx> interpret::Machine<'tcx> for CompileTimeMachine<'tcx> {
interp_ok(())
}

fn before_static_ref_eval(
tcx: TyCtxtAt<'tcx>,
machine: &Self,
ptr: Pointer<Self::Provenance>,
static_def_id: DefId,
) -> InterpResult<'tcx> {
// We are only interested in immediate references to statics here.
// Indirect references should not be checked because:
// - they may be references to some other legitimate static reference
// (e.g. via a raw pointer), and
// - if they originate from an illegal static reference, that illegal
// reference must already appear in the body and will be checked there.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't quite understand what you mean by this. However, it all seems an artifact of trying to catch cases where the reference points to a static, rather than just the specific syntactic pattern &STATIC.

if ptr.provenance.shared_ref() {
return interp_ok(());
}

// Check if this is the currently evaluated static.
if Some(static_def_id) == machine.static_root_ids.map(|(_, def_id)| def_id.into()) {
return Err(ConstEvalErrKind::RecursiveStatic).into();
}

if !tcx.is_foreign_item(static_def_id) {
// Fire the query to detect cycles. We cannot restrict this to only when
// evaluating statics, since static reference cycles can also be formed
// through consts, especially promoted ones.
Copy link
Member Author

@ShoyuVanilla ShoyuVanilla Dec 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not skiping firing the query when machine.static_root_ids.is_none() here further prohibits some currently compiled codes such as

static FOO: i32 = {
    let x = &BAR;
    42
};

const BAR: i32 = {
    let x = &FOO;
    42
};

but I guess we might have to refute const cases as well because the following codes slightly modified from the original issue still make problems with it:

enum Never {}

// &&X creates a promoted const
static X: &Never = weird(&&X);

const fn weird(a: &&&Never) -> &'static Never {
    // SAFETY: our argument type has an unsatisfiable
    // library invariant; therefore, this code is unreachable.
    unsafe { std::hint::unreachable_unchecked() };
}
// for privacy
mod mrow {
    pub struct InBoundsIndex<const N: usize>(());

    impl<const N: usize> InBoundsIndex<N> {
        pub const fn new() -> Option<InBoundsIndex<N>> {
            if N < 32 {
                Some(Self(()))
            } else {
                None
            }
        }
    }
}

use mrow::InBoundsIndex;

static IDX: InBoundsIndex<64> = {
    FOO;
    // THIS SHOULD PANIC!!! but we do UB first on the line above
    InBoundsIndex::<64>::new().unwrap()
};

const FOO: () = {
    let _x = index([0; 32], &IDX);
};

const fn index<const N: usize>(arr: [u8; 32], _: &InBoundsIndex<N>) -> u8 {
    // SAFETY: InBoundsIndex can only be created by its new, which ensures N is < 32
    unsafe { arr.as_ptr().add(N).read() }
}

tcx.eval_static_initializer(static_def_id)?;
}
interp_ok(())
}

fn cached_union_data_range<'e>(
ecx: &'e mut InterpCx<'tcx, Self>,
ty: Ty<'tcx>,
Expand Down
14 changes: 14 additions & 0 deletions compiler/rustc_const_eval/src/interpret/machine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,20 @@ pub trait Machine<'tcx>: Sized {
interp_ok(())
}

/// Hook for performing extra checks on any reference to static.
/// This is not invoked for raw pointers such as `unsafe { &*&raw const STATIC }`.
///
/// Used to prevent statics from constructing uninhabitated values by referencing themselves
/// as it is being initialized.
fn before_static_ref_eval(
_tcx: TyCtxtAt<'tcx>,
_machine: &Self,
_ptr: Pointer<Self::Provenance>,
_static_def_id: DefId,
) -> InterpResult<'tcx> {
interp_ok(())
}

/// Hook for performing extra checks on a memory write access.
/// This is not invoked for ZST accesses, as no write actually happens.
/// `ptr` will always be a pointer with the provenance in `prov` pointing to the beginning of
Expand Down
23 changes: 23 additions & 0 deletions compiler/rustc_const_eval/src/interpret/step.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
//! The main entry point is the `step` method.
use std::iter;
use std::ops::Deref;

use either::Either;
use rustc_abi::{FIRST_VARIANT, FieldIdx};
use rustc_data_structures::fx::FxHashSet;
use rustc_index::IndexSlice;
use rustc_middle::mir::interpret::{GlobalAlloc, Provenance, Scalar};
use rustc_middle::ty::{self, Instance, Ty};
use rustc_middle::{bug, mir, span_bug};
use rustc_span::source_map::Spanned;
Expand Down Expand Up @@ -217,9 +219,30 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
}

Ref(_, borrow_kind, place) => {
let is_reborrow_of_ref = if let Some(local) = place.local_or_deref_local()
&& place.is_indirect()
{
self.layout_of_local(self.frame(), local, None)?.ty.is_ref()
} else {
false
};
Copy link
Member

@RalfJung RalfJung Dec 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like it will be false for &(*local_ref).field, which is not what we want.

Generally, I am not convinced by the implementation approach. This adds a very syntactic check to one specific operation that constructs references, but it's far from clear that this is enough. For instance, the code could take a raw pointer to a static, store it in a [*const T; 1], and transmute that to [&T; 1] -- then you would never see a (re)borrow of a reference but we still have the same underlying unsoundness.

Copy link
Member

@RalfJung RalfJung Dec 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For instance,

I guess we'd say that the unsafe code needed to construct references this way is unsound... hm. Maybe that is fair but it needs good documentation.

Also, what you implemented is somewhat different from what I sketched, not sure if that is deliberate. You seem to be trying to catch all &expr where expr evaluates to a pointer that points to a static. The original plan was just to catch all &STATIC, i.e. it really syntactically has to have that shape.

In particular, your approach would seem to reject this:

static X: &Never = weird(unsafe { &*&raw const X });

I'm not sure that's a good idea. It's unprincipled: it's still possible to write unsafe code that evades your checks, so yo haven't gained any new guarantees. I would rather have a very clear line for which code gets accepted and which gets rejected.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I’m not a fan of this implementation either (I’ll elaborate a bit more in my next comment 😅).
But this doesn't reject static X: &Never = weird(unsafe { &*&raw const X }); because is_reborrow_of_ref is false for that case.

But everything else is exactly as you described - e.g., field projections - so I’ll try the other approach.


let src = self.eval_place(place)?;
let place = self.force_allocation(&src)?;
let val = ImmTy::from_immediate(place.to_ref(self), dest.layout);

// Check whether this forms a cycle involving a static reference.
// Ensure the place is not a reborrow from a raw pointer, since we do not want to
// forbid static reference cycles that go through raw pointers.
if is_reborrow_of_ref
&& let Immediate::Scalar(Scalar::Ptr(ptr, _)) = val.deref()
&& let Some(alloc_id) = ptr.provenance.get_alloc_id()
&& let Some(GlobalAlloc::Static(def_id)) =
self.tcx.try_get_global_alloc(alloc_id)
{
M::before_static_ref_eval(self.tcx, &self.machine, *ptr, def_id)?;
}

// A fresh reference was created, make sure it gets retagged.
let val = M::retag_ptr_value(
self,
Expand Down
3 changes: 1 addition & 2 deletions tests/ui/consts/const-eval/issue-47971.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
//@ check-pass

struct S(pub &'static u32, pub u32);

const fn g(ss: &S) -> &u32 { &ss.1 }

static T: S = S(g(&T), 0);
//~^ ERROR: encountered static that tried to access itself during initialization

fn main () { }
9 changes: 9 additions & 0 deletions tests/ui/consts/const-eval/issue-47971.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
error[E0080]: encountered static that tried to access itself during initialization
--> $DIR/issue-47971.rs:5:19
|
LL | static T: S = S(g(&T), 0);
| ^^ evaluation of `T` failed here

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0080`.
3 changes: 1 addition & 2 deletions tests/ui/consts/cycle-static-promoted.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
//@ check-pass

struct Value {
values: &'static [&'static Value],
}

// This `static` recursively points to itself through a promoted (the slice).
static VALUE: Value = Value {
values: &[&VALUE],
//~^ ERROR: cycle detected when evaluating initializer of static `VALUE`
};

fn main() {}
23 changes: 23 additions & 0 deletions tests/ui/consts/cycle-static-promoted.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
error[E0391]: cycle detected when evaluating initializer of static `VALUE`
--> $DIR/cycle-static-promoted.rs:7:13
|
LL | values: &[&VALUE],
| ^^^^^^^^^
|
note: ...which requires simplifying constant for the type system `VALUE::promoted[0]`...
--> $DIR/cycle-static-promoted.rs:6:1
|
LL | static VALUE: Value = Value {
| ^^^^^^^^^^^^^^^^^^^
note: ...which requires const-evaluating + checking `VALUE::promoted[0]`...
--> $DIR/cycle-static-promoted.rs:6:1
|
LL | static VALUE: Value = Value {
| ^^^^^^^^^^^^^^^^^^^
= note: ...which again requires evaluating initializer of static `VALUE`, completing the cycle
= note: cycle used when running analysis passes on crate `cycle_static_promoted`
= note: see https://rustc-dev-guide.rust-lang.org/overview.html#queries and https://rustc-dev-guide.rust-lang.org/query.html for more information

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0391`.
3 changes: 1 addition & 2 deletions tests/ui/consts/static-cycle-error.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
//@ check-pass

struct Foo {
foo: Option<&'static Foo>
}

static FOO: Foo = Foo {
foo: Some(&FOO),
//~^ ERROR: encountered static that tried to access itself during initialization
};

fn main() {}
9 changes: 9 additions & 0 deletions tests/ui/consts/static-cycle-error.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
error[E0080]: encountered static that tried to access itself during initialization
--> $DIR/static-cycle-error.rs:6:15
|
LL | foo: Some(&FOO),
| ^^^^ evaluation of `FOO` failed here

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0080`.
2 changes: 1 addition & 1 deletion tests/ui/statics/recursive_interior_mut.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ unsafe impl Sync for EmptyChunkFooter {}

static EMPTY_CHUNK: EmptyChunkFooter = EmptyChunkFooter(ChunkFooter {
prev: Cell::new(unsafe {
NonNull::new_unchecked(&EMPTY_CHUNK as *const EmptyChunkFooter as *mut ChunkFooter)
NonNull::new_unchecked(&raw const EMPTY_CHUNK as *mut ChunkFooter)
}),
});

Expand Down
7 changes: 2 additions & 5 deletions tests/ui/statics/static-recursive.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
//@ run-pass

static mut S: *const u8 = unsafe { &S as *const *const u8 as *const u8 };
//~^ WARN shared reference to mutable static [static_mut_refs]
static mut S: *const u8 = &raw const S as *const u8;

struct StaticDoubleLinked {
prev: &'static StaticDoubleLinked,
Expand All @@ -11,13 +8,13 @@ struct StaticDoubleLinked {
}

static L1: StaticDoubleLinked = StaticDoubleLinked { prev: &L3, next: &L2, data: 1, head: true };
//~^ ERROR: cycle detected when evaluating initializer of static `L1`
static L2: StaticDoubleLinked = StaticDoubleLinked { prev: &L1, next: &L3, data: 2, head: false };
static L3: StaticDoubleLinked = StaticDoubleLinked { prev: &L2, next: &L1, data: 3, head: false };

pub fn main() {
unsafe {
assert_eq!(S, *(S as *const *const u8));
//~^ WARN creating a shared reference to mutable static [static_mut_refs]
}

let mut test_vec = Vec::new();
Expand Down
36 changes: 17 additions & 19 deletions tests/ui/statics/static-recursive.stderr
Original file line number Diff line number Diff line change
@@ -1,25 +1,23 @@
warning: creating a shared reference to mutable static
--> $DIR/static-recursive.rs:3:36
error[E0391]: cycle detected when evaluating initializer of static `L1`
--> $DIR/static-recursive.rs:10:1
|
LL | static mut S: *const u8 = unsafe { &S as *const *const u8 as *const u8 };
| ^^ shared reference to mutable static
LL | static L1: StaticDoubleLinked = StaticDoubleLinked { prev: &L3, next: &L2, data: 1, head: true };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: for more information, see <https://doc.rust-lang.org/edition-guide/rust-2024/static-mut-references.html>
= note: shared references to mutable statics are dangerous; it's undefined behavior if the static is mutated or if a mutable reference is created for it while the shared reference lives
= note: `#[warn(static_mut_refs)]` (part of `#[warn(rust_2024_compatibility)]`) on by default
help: use `&raw const` instead to create a raw pointer
note: ...which requires evaluating initializer of static `L3`...
--> $DIR/static-recursive.rs:13:1
|
LL | static mut S: *const u8 = unsafe { &raw const S as *const *const u8 as *const u8 };
| +++++++++

warning: creating a shared reference to mutable static
--> $DIR/static-recursive.rs:19:20
|
LL | assert_eq!(S, *(S as *const *const u8));
| ^ shared reference to mutable static
LL | static L3: StaticDoubleLinked = StaticDoubleLinked { prev: &L2, next: &L1, data: 3, head: false };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
note: ...which requires evaluating initializer of static `L2`...
--> $DIR/static-recursive.rs:12:1
|
= note: for more information, see <https://doc.rust-lang.org/edition-guide/rust-2024/static-mut-references.html>
= note: shared references to mutable statics are dangerous; it's undefined behavior if the static is mutated or if a mutable reference is created for it while the shared reference lives
LL | static L2: StaticDoubleLinked = StaticDoubleLinked { prev: &L1, next: &L3, data: 2, head: false };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
= note: ...which again requires evaluating initializer of static `L1`, completing the cycle
= note: cycle used when running analysis passes on crate `static_recursive`
= note: see https://rustc-dev-guide.rust-lang.org/overview.html#queries and https://rustc-dev-guide.rust-lang.org/query.html for more information

warning: 2 warnings emitted
error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0391`.
48 changes: 48 additions & 0 deletions tests/ui/statics/static-ref-cycle-issue-143047.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Regression test for https://github.com/rust-lang/rust/issues/143047

#[allow(static_mut_refs)]

static FOO: u8 = {
let x = &FOO;
//~^ ERROR: encountered static that tried to access itself during initialization
0
};

static mut BAR: u8 = {
let x = unsafe { &mut BAR };
//~^ ERROR: encountered static that tried to access itself during initialization
0
};

static mut BAZ: u8 = {
let x: &u8 = unsafe { &*&raw const BAR };
let y = &raw mut BAR;
let z: &mut u8 = unsafe { &mut *y };
let w = &z;
let v = &w;
0
};

static QUX: u8 = {
//~^ ERROR: cycle detected when evaluating initializer of static `QUX`
let x = &QUUX;
0
};

static QUUX: u8 = {
let x = &QUUUX;
0
};

static QUUUX: u8 = {
let x = &QUX;
0
};

static PROMOTED: u8 = {
let x = &&&PROMOTED;
//~^ ERROR: cycle detected when evaluating initializer of static `PROMOTED`
0
};

fn main() {}
56 changes: 56 additions & 0 deletions tests/ui/statics/static-ref-cycle-issue-143047.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
error[E0080]: encountered static that tried to access itself during initialization
--> $DIR/static-ref-cycle-issue-143047.rs:6:13
|
LL | let x = &FOO;
| ^^^^ evaluation of `FOO` failed here

error[E0080]: encountered static that tried to access itself during initialization
--> $DIR/static-ref-cycle-issue-143047.rs:12:22
|
LL | let x = unsafe { &mut BAR };
| ^^^^^^^^ evaluation of `BAR` failed here

error[E0391]: cycle detected when evaluating initializer of static `QUX`
--> $DIR/static-ref-cycle-issue-143047.rs:26:1
|
LL | static QUX: u8 = {
| ^^^^^^^^^^^^^^
|
note: ...which requires evaluating initializer of static `QUUX`...
--> $DIR/static-ref-cycle-issue-143047.rs:32:1
|
LL | static QUUX: u8 = {
| ^^^^^^^^^^^^^^^
note: ...which requires evaluating initializer of static `QUUUX`...
--> $DIR/static-ref-cycle-issue-143047.rs:37:1
|
LL | static QUUUX: u8 = {
| ^^^^^^^^^^^^^^^^
= note: ...which again requires evaluating initializer of static `QUX`, completing the cycle
= note: cycle used when running analysis passes on crate `static_ref_cycle_issue_143047`
= note: see https://rustc-dev-guide.rust-lang.org/overview.html#queries and https://rustc-dev-guide.rust-lang.org/query.html for more information

error[E0391]: cycle detected when evaluating initializer of static `PROMOTED`
--> $DIR/static-ref-cycle-issue-143047.rs:43:13
|
LL | let x = &&&PROMOTED;
| ^^^^^^^^^^^
|
note: ...which requires simplifying constant for the type system `PROMOTED::promoted[0]`...
--> $DIR/static-ref-cycle-issue-143047.rs:42:1
|
LL | static PROMOTED: u8 = {
| ^^^^^^^^^^^^^^^^^^^
note: ...which requires const-evaluating + checking `PROMOTED::promoted[0]`...
--> $DIR/static-ref-cycle-issue-143047.rs:42:1
|
LL | static PROMOTED: u8 = {
| ^^^^^^^^^^^^^^^^^^^
= note: ...which again requires evaluating initializer of static `PROMOTED`, completing the cycle
= note: cycle used when running analysis passes on crate `static_ref_cycle_issue_143047`
= note: see https://rustc-dev-guide.rust-lang.org/overview.html#queries and https://rustc-dev-guide.rust-lang.org/query.html for more information

error: aborting due to 4 previous errors

Some errors have detailed explanations: E0080, E0391.
For more information about an error, try `rustc --explain E0080`.
28 changes: 28 additions & 0 deletions tests/ui/statics/static-ref-cycle-lib-invariants-issue-143047.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Regression test for https://github.com/rust-lang/rust/issues/143047

mod mrow {
pub struct InBoundsIndex<const N: usize>(());

impl<const N: usize> InBoundsIndex<N> {
pub const fn new() -> Option<InBoundsIndex<N>> {
if N < 32 { Some(Self(())) } else { None }
}
}
}

use mrow::InBoundsIndex;

static IDX: InBoundsIndex<64> = {
// The following line should cause a compilation error, otherwise it results in an
// undefined behavior.
index([0; 32], &IDX);
//~^ ERROR: encountered static that tried to access itself during initialization
InBoundsIndex::<64>::new().unwrap()
};

const fn index<const N: usize>(arr: [u8; 32], _: &InBoundsIndex<N>) -> u8 {
// SAFETY: InBoundsIndex can only be created by its new, which ensures N is < 32
unsafe { arr.as_ptr().add(N).read() }
}

fn main() {}
Loading
Loading