Skip to content

Conversation

@ShoyuVanilla
Copy link
Member

Fixes #143047

@rustbot
Copy link
Collaborator

rustbot commented Dec 14, 2025

Some changes occurred to the CTFE / Miri interpreter

cc @rust-lang/miri

Some changes occurred to the CTFE machinery

cc @RalfJung, @oli-obk, @lcnr

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Dec 14, 2025
@rustbot
Copy link
Collaborator

rustbot commented Dec 14, 2025

r? @mati865

rustbot has assigned @mati865.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

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

@rust-log-analyzer
Copy link
Collaborator

The job aarch64-gnu-llvm-20-1 failed! Check out the build log: (web) (plain enhanced) (plain)

Click to see the possible cause of the failure (guessed by this bot)
   Compiling thiserror-impl v2.0.17
error[E0391]: cycle detected when evaluating initializer of static `scan_expr::POSTFIX`
  --> /cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-2.0.17/src/scan_expr.rs:64:30
   |
64 |     (Keyword("as"), SetState(&[(ExpectType, SetState(&POSTFIX))])),
   |                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
note: ...which requires simplifying constant for the type system `scan_expr::POSTFIX::promoted[0]`...
  --> /cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-2.0.17/src/scan_expr.rs:63:1
   |
63 | static POSTFIX: [(Input, Action); 10] = [
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
note: ...which requires const-evaluating + checking `scan_expr::POSTFIX::promoted[0]`...
  --> /cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-2.0.17/src/scan_expr.rs:63:1
   |
63 | static POSTFIX: [(Input, Action); 10] = [
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   = note: ...which again requires evaluating initializer of static `scan_expr::POSTFIX`, completing the cycle
note: cycle used when evaluating initializer of static `scan_expr::INIT`
  --> /cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-2.0.17/src/scan_expr.rs:32:1
   |
32 | static INIT: [(Input, Action); 28] = [
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   = 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[E0080]: encountered static that tried to access itself during initialization
  --> /cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-2.0.17/src/scan_expr.rs:77:32
   |
77 |     (Keyword("move"), SetState(&ASYNC)),
   |                                ^^^^^^ evaluation of `scan_expr::ASYNC` failed here

error[E0080]: encountered static that tried to access itself during initialization
  --> /cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-2.0.17/src/scan_expr.rs:96:33
   |
96 |     (Keyword("async"), SetState(&CLOSURE)),
   |                                 ^^^^^^^^ evaluation of `scan_expr::CLOSURE` failed here

error[E0080]: encountered static that tried to access itself during initialization
   --> /cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-2.0.17/src/scan_expr.rs:149:33
    |
149 |     (ConsumeDelimiter, SetState(&PATTERN)),
    |                                 ^^^^^^^^ evaluation of `scan_expr::PATTERN` failed here

Some errors have detailed explanations: E0080, E0391.
For more information about an error, try `rustc --explain E0080`.
[RUSTC-TIMING] thiserror_impl test:false 0.472
error: could not compile `thiserror-impl` (lib) due to 4 previous errors

@ShoyuVanilla
Copy link
Member Author

ShoyuVanilla commented Dec 14, 2025

Oh, this already breaks things as hell like in:

static POSTFIX: [(Input, Action); 10] = [
    (Keyword("as"), SetState(&[(ExpectType, SetState(&POSTFIX))])),
...

@traviscross traviscross added the I-lang-radar Items that are on lang's radar and will need eventual work or consideration. label Dec 14, 2025
@ShoyuVanilla
Copy link
Member Author

@bors try

rust-bors bot added a commit that referenced this pull request Dec 14, 2025
Prohibit cycles behind references while static initialization
@rust-bors

This comment has been minimized.

@rust-bors
Copy link

rust-bors bot commented Dec 14, 2025

💔 Test for 352f3f3 failed: CI. Failed jobs:

@rust-log-analyzer
Copy link
Collaborator

The job dist-x86_64-linux failed! Check out the build log: (web) (plain enhanced) (plain)

Click to see the possible cause of the failure (guessed by this bot)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 32.17s
##[endgroup]
[2025-12-14T09:11:02.127Z INFO  opt_dist::timer] Section `Stage 1 (Rustc PGO)` starts
[2025-12-14T09:11:02.127Z INFO  opt_dist::timer] Section `Stage 1 (Rustc PGO) > Build PGO instrumented rustc and LLVM` starts
[2025-12-14T09:11:02.127Z INFO  opt_dist::exec] Executing `RUST_BACKTRACE=full python3 /checkout/x.py build --target x86_64-unknown-linux-gnu --host x86_64-unknown-linux-gnu --stage 2 library/std --set rust.llvm-bitcode-linker=false --set build.extended=false --set rust.codegen-backends=['llvm'] --set rust.deny-warnings=false --rust-profile-generate /tmp/tmp-multistage/opt-artifacts/rustc-pgo --set llvm.thin-lto=false --set llvm.link-shared=true [at /checkout/obj]`
##[group]Building bootstrap
    Finished `dev` profile [unoptimized] target(s) in 0.06s
##[endgroup]
[TIMING:start] compile::Assemble { target_compiler: Compiler { stage: 2, host: x86_64-unknown-linux-gnu, forced_compiler: false } }
[TIMING:start] builder::Libdir { compiler: Compiler { stage: 2, host: x86_64-unknown-linux-gnu, forced_compiler: false }, target: x86_64-unknown-linux-gnu }
---
   Compiling schemars_derive v1.1.0
error[E0391]: cycle detected when evaluating initializer of static `scan_expr::POSTFIX`
  --> /rust/deps/thiserror-impl-2.0.17/src/scan_expr.rs:64:30
   |
64 |     (Keyword("as"), SetState(&[(ExpectType, SetState(&POSTFIX))])),
   |                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
note: ...which requires simplifying constant for the type system `scan_expr::POSTFIX::promoted[0]`...
  --> /rust/deps/thiserror-impl-2.0.17/src/scan_expr.rs:63:1
   |
63 | static POSTFIX: [(Input, Action); 10] = [
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
note: ...which requires const-evaluating + checking `scan_expr::POSTFIX::promoted[0]`...
  --> /rust/deps/thiserror-impl-2.0.17/src/scan_expr.rs:63:1
   |
63 | static POSTFIX: [(Input, Action); 10] = [
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   = note: ...which again requires evaluating initializer of static `scan_expr::POSTFIX`, completing the cycle
note: cycle used when evaluating initializer of static `scan_expr::INIT`
  --> /rust/deps/thiserror-impl-2.0.17/src/scan_expr.rs:32:1
   |
32 | static INIT: [(Input, Action); 28] = [
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   = 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[E0080]: encountered static that tried to access itself during initialization
  --> /rust/deps/thiserror-impl-2.0.17/src/scan_expr.rs:77:32
   |
77 |     (Keyword("move"), SetState(&ASYNC)),
   |                                ^^^^^^ evaluation of `scan_expr::ASYNC` failed here

error[E0080]: encountered static that tried to access itself during initialization
  --> /rust/deps/thiserror-impl-2.0.17/src/scan_expr.rs:96:33
   |
96 |     (Keyword("async"), SetState(&CLOSURE)),
   |                                 ^^^^^^^^ evaluation of `scan_expr::CLOSURE` failed here

error[E0080]: encountered static that tried to access itself during initialization
   --> /rust/deps/thiserror-impl-2.0.17/src/scan_expr.rs:149:33
    |
149 |     (ConsumeDelimiter, SetState(&PATTERN)),
    |                                 ^^^^^^^^ evaluation of `scan_expr::PATTERN` failed here

Some errors have detailed explanations: E0080, E0391.
For more information about an error, try `rustc --explain E0080`.
[RUSTC-TIMING] thiserror_impl test:false 0.474
error: could not compile `thiserror-impl` (lib) due to 4 previous errors
---
warning: rustc_llvm@0.0.0: Inherited flag "-fembed-bitcode=all" is not supported by the currently used CC
warning: rustc_llvm@0.0.0: Inherited flag "-fembed-bitcode=all" is not supported by the currently used CC
warning: rustc_llvm@0.0.0: Inherited flag "-fembed-bitcode=all" is not supported by the currently used CC
warning: rustc_llvm@0.0.0: Inherited flag "-fembed-bitcode=all" is not supported by the currently used CC
Bootstrap failed while executing `build --target x86_64-unknown-linux-gnu --host x86_64-unknown-linux-gnu --stage 2 library/std --set rust.llvm-bitcode-linker=false --set build.extended=false --set rust.codegen-backends=['llvm'] --set rust.deny-warnings=false --rust-profile-generate /tmp/tmp-multistage/opt-artifacts/rustc-pgo --set llvm.thin-lto=false --set llvm.link-shared=true`
Build completed unsuccessfully in 0:11:37
[2025-12-14T09:22:39.231Z INFO  opt_dist::timer] Section `Stage 1 (Rustc PGO) > Build PGO instrumented rustc and LLVM` ended: FAIL (697.10s)`
[2025-12-14T09:22:39.231Z INFO  opt_dist::timer] Section `Stage 1 (Rustc PGO)` ended: FAIL (697.10s)`
[2025-12-14T09:22:39.231Z INFO  opt_dist] Timer results
    -----------------------------------------------------------------
---
[2025-12-14T09:22:39.231Z INFO  opt_dist::utils] Free disk space: 1.24 TiB out of total 2.18 TiB (42.90% used)
Error: Optimized build pipeline has failed

Caused by:
    Command RUST_BACKTRACE=full python3 /checkout/x.py build --target x86_64-unknown-linux-gnu --host x86_64-unknown-linux-gnu --stage 2 library/std --set rust.llvm-bitcode-linker=false --set build.extended=false --set rust.codegen-backends=['llvm'] --set rust.deny-warnings=false --rust-profile-generate /tmp/tmp-multistage/opt-artifacts/rustc-pgo --set llvm.thin-lto=false --set llvm.link-shared=true [at /checkout/obj] has failed with exit code Some(1)

Stack backtrace:
   0: <anyhow::Error>::msg::<alloc::string::String>
             at /rust/deps/anyhow-1.0.100/src/backtrace.rs:27:14
   1: <opt_dist::exec::CmdBuilder>::run
             at /rustc/352f3f317a94f3782e160190a968acd8947f9a21/src/tools/opt-dist/src/exec.rs:80:17
   2: <opt_dist::exec::Bootstrap>::run
             at /rustc/352f3f317a94f3782e160190a968acd8947f9a21/src/tools/opt-dist/src/exec.rs:199:18
   3: opt_dist::execute_pipeline::{closure#1}::{closure#0}
             at /rustc/352f3f317a94f3782e160190a968acd8947f9a21/src/tools/opt-dist/src/main.rs:256:21
   4: <opt_dist::timer::TimerSection>::section::<opt_dist::execute_pipeline::{closure#1}::{closure#0}, ()>
             at /rustc/352f3f317a94f3782e160190a968acd8947f9a21/src/tools/opt-dist/src/timer.rs:111:22
   5: opt_dist::execute_pipeline::{closure#1}
             at /rustc/352f3f317a94f3782e160190a968acd8947f9a21/src/tools/opt-dist/src/main.rs:245:15
   6: <opt_dist::timer::TimerSection>::section::<opt_dist::execute_pipeline::{closure#1}, opt_dist::training::RustcPGOProfile>
             at /rustc/352f3f317a94f3782e160190a968acd8947f9a21/src/tools/opt-dist/src/timer.rs:111:22
   7: opt_dist::execute_pipeline
             at /rustc/352f3f317a94f3782e160190a968acd8947f9a21/src/tools/opt-dist/src/main.rs:242:35
   8: opt_dist::main
             at /rustc/352f3f317a94f3782e160190a968acd8947f9a21/src/tools/opt-dist/src/main.rs:467:18
   9: <fn() -> core::result::Result<(), anyhow::Error> as core::ops::function::FnOnce<()>>::call_once
             at /rustc/3b4dd9bf1410f8da6329baa36ce5e37673cbbd1f/library/core/src/ops/function.rs:250:5
  10: std::sys::backtrace::__rust_begin_short_backtrace::<fn() -> core::result::Result<(), anyhow::Error>, core::result::Result<(), anyhow::Error>>
             at /rustc/3b4dd9bf1410f8da6329baa36ce5e37673cbbd1f/library/std/src/sys/backtrace.rs:158:18
  11: std::rt::lang_start::<core::result::Result<(), anyhow::Error>>::{closure#0}
             at /rustc/3b4dd9bf1410f8da6329baa36ce5e37673cbbd1f/library/std/src/rt.rs:206:18
  12: core::ops::function::impls::<impl core::ops::function::FnOnce<A> for &F>::call_once
             at /rustc/3b4dd9bf1410f8da6329baa36ce5e37673cbbd1f/library/core/src/ops/function.rs:287:21
  13: std::panicking::catch_unwind::do_call
             at /rustc/3b4dd9bf1410f8da6329baa36ce5e37673cbbd1f/library/std/src/panicking.rs:590:40
  14: std::panicking::catch_unwind
             at /rustc/3b4dd9bf1410f8da6329baa36ce5e37673cbbd1f/library/std/src/panicking.rs:553:19

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.

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

@RalfJung
Copy link
Member

Also I should say, thanks for tackling this. :)

@ShoyuVanilla
Copy link
Member Author

ShoyuVanilla commented Dec 15, 2025

Thanks for the detailed feedback!
As I mentioned earlier, I’m not really happy with this implementation either, but since CTFE doesn’t give us much syntactic information, I tried this approach first.
EDIT) Would it be okay to use lint_rootget HIR for this purpose?

Do you think this would be better implemented somewhere else, like in a late lint pass or during MIR construction?

BTW, since it looks like I need to implement the non-denying lint first for FCW - and triggering a query cycle won’t work for that - I’ll have to explore a different approach anyway 😅

@ShoyuVanilla ShoyuVanilla marked this pull request as draft December 15, 2025 05:07
@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Dec 15, 2025
@RalfJung
Copy link
Member

As I mentioned earlier, I’m not really happy with this implementation either, but since CTFE doesn’t give us much syntactic information, I tried this approach first.

CTFE gives you all the syntactic information that exists in MIR. ;)
I guess the problem is that &STATIC gets represented as

        _2 = const {alloc1: &i32};
        _1 = &(*_2);

And strangely, even if the user wrote &raw const STATIC, the _2 variable here will have type &i32.

I think we'll have to change that. To make it easier for later stages of the compiler to distinguish ref-to-static from raw-ptr-to-static, we should adjust the type of the const we generate for that (during MIR building) accordingly.

BTW, since it looks like I need to implement the non-denying lint first for FCW - and triggering a query cycle won’t work for that - I’ll have to explore a different approach anyway 😅

Yes, I was wondering what your plans were here.^^ I honestly don't have any immediately good ideas.
However, "how do we find cycles" is orthogonal to "how do we detect problematic expressions that make up an edge in a potential cycle".

@ShoyuVanilla
Copy link
Member Author

I guess the problem is that &STATIC gets represented as

        _2 = const {alloc1: &i32};
        _1 = &(*_2);

And strangely, even if the user wrote &raw const STATIC, the _2 variable here will have type &i32.

Yeah, I encountered this and most of the fuss above started from trying to discern those two cases.

However, "how do we find cycles" is orthogonal to "how do we detect problematic expressions that make up an edge in a potential cycle".

You're right. I'll try working on both parts 😄

BTW, since it looks like I need to implement the non-denying lint first for FCW - and triggering a query cycle won’t work for that - I’ll have to explore a different approach anyway 😅

Yes, I was wondering what your plans were here.^^ I honestly don't have any immediately good ideas.

I'm thinking of something like the following for now 🤔

  • For the crater run, instead of firing eval_static_initializer query, evaluate the initializers manually with a context, record statics on its way and detect the cycle and lint. But this would regress the perf by a lot
  • So, for the actual FCW to be merged, just check whether the static initializer references itself, and do not evaluate static initializer of referenced statics at all. Though this isn't powerful enough to detect cycles with length > 1, most of the violations would fall into this category so it would be better than nothing 🙃
  • When we are finally making breaking change, fire the query to trigger the cycle errors

But maybe I could find a better way 😅

@theemathas
Copy link
Contributor

theemathas commented Dec 15, 2025

Some other cases that needs to be detected and prohibited, if they aren't handled already:

struct Weird(&'static Weird);
static X: Weird = {
    let ref a = X;
    Weird(a)
};
struct Weird(&'static Weird);
impl Weird {
    const fn by_ref(&self) -> &Self { self }
}
static X: Weird = Weird(X.by_ref());  // autoref
struct Weird(&'static Weird);
static X: (Weird, i32) = (Weird(&X.0), 1);  // reference to part of the static

@ShoyuVanilla
Copy link
Member Author

ShoyuVanilla commented Dec 15, 2025

I'm not sure on adjustments into raw ptr cases:

enum Never {}

static X: &Never = weird(&X);

const fn weird(a: *const &Never) -> &'static Never {
    todo!("Reachable, perhaps?")
}

@RalfJung
Copy link
Member

RalfJung commented Dec 15, 2025 via email

@theemathas
Copy link
Contributor

Another case to be prohibited:

struct Weird(&'static Weird);
static X: Weird = 'a: {
   match X {
       y if break 'a Weird(&y) => {},
       _ => {},
   };
   unreachable!()
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

I-lang-radar Items that are on lang's radar and will need eventual work or consideration. S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

const-eval can construct uninhabited values via recursive static initialization

7 participants