diff --git a/compiler/rustc_borrowck/src/dataflow.rs b/compiler/rustc_borrowck/src/dataflow.rs
index 8c4885770ad..2821677c537 100644
--- a/compiler/rustc_borrowck/src/dataflow.rs
+++ b/compiler/rustc_borrowck/src/dataflow.rs
@@ -393,6 +393,7 @@ impl<'tcx> rustc_mir_dataflow::GenKillAnalysis<'tcx> for Borrows<'_, 'tcx> {
             | mir::StatementKind::AscribeUserType(..)
             | mir::StatementKind::Coverage(..)
             | mir::StatementKind::Intrinsic(..)
+            | mir::StatementKind::ConstEvalCounter
             | mir::StatementKind::Nop => {}
         }
     }
diff --git a/compiler/rustc_borrowck/src/invalidation.rs b/compiler/rustc_borrowck/src/invalidation.rs
index 6fd9290058c..6217676d5c1 100644
--- a/compiler/rustc_borrowck/src/invalidation.rs
+++ b/compiler/rustc_borrowck/src/invalidation.rs
@@ -91,7 +91,8 @@ impl<'cx, 'tcx> Visitor<'tcx> for InvalidationGenerator<'cx, 'tcx> {
                     LocalMutationIsAllowed::Yes,
                 );
             }
-            StatementKind::Nop
+            StatementKind::ConstEvalCounter
+            | StatementKind::Nop
             | StatementKind::Retag { .. }
             | StatementKind::Deinit(..)
             | StatementKind::SetDiscriminant { .. } => {
diff --git a/compiler/rustc_borrowck/src/lib.rs b/compiler/rustc_borrowck/src/lib.rs
index 73ea7314b75..bc81abe4005 100644
--- a/compiler/rustc_borrowck/src/lib.rs
+++ b/compiler/rustc_borrowck/src/lib.rs
@@ -609,7 +609,8 @@ impl<'cx, 'tcx> rustc_mir_dataflow::ResultsVisitor<'cx, 'tcx> for MirBorrowckCtx
             StatementKind::AscribeUserType(..)
             // Doesn't have any language semantics
             | StatementKind::Coverage(..)
-            // Does not actually affect borrowck
+            // These do not actually affect borrowck
+            | StatementKind::ConstEvalCounter
             | StatementKind::StorageLive(..) => {}
             StatementKind::StorageDead(local) => {
                 self.access_place(
diff --git a/compiler/rustc_borrowck/src/type_check/mod.rs b/compiler/rustc_borrowck/src/type_check/mod.rs
index 81bd4c2a783..06087b0c579 100644
--- a/compiler/rustc_borrowck/src/type_check/mod.rs
+++ b/compiler/rustc_borrowck/src/type_check/mod.rs
@@ -1258,6 +1258,7 @@ impl<'a, 'tcx> TypeChecker<'a, 'tcx> {
             | StatementKind::StorageDead(..)
             | StatementKind::Retag { .. }
             | StatementKind::Coverage(..)
+            | StatementKind::ConstEvalCounter
             | StatementKind::Nop => {}
             StatementKind::Deinit(..) | StatementKind::SetDiscriminant { .. } => {
                 bug!("Statement not allowed in this MIR phase")
diff --git a/compiler/rustc_codegen_cranelift/src/base.rs b/compiler/rustc_codegen_cranelift/src/base.rs
index d3a8c10657e..dffb2ed8f4f 100644
--- a/compiler/rustc_codegen_cranelift/src/base.rs
+++ b/compiler/rustc_codegen_cranelift/src/base.rs
@@ -794,6 +794,7 @@ fn codegen_stmt<'tcx>(
         StatementKind::StorageLive(_)
         | StatementKind::StorageDead(_)
         | StatementKind::Deinit(_)
+        | StatementKind::ConstEvalCounter
         | StatementKind::Nop
         | StatementKind::FakeRead(..)
         | StatementKind::Retag { .. }
diff --git a/compiler/rustc_codegen_cranelift/src/constant.rs b/compiler/rustc_codegen_cranelift/src/constant.rs
index 51450897bfc..49c4f1aaaef 100644
--- a/compiler/rustc_codegen_cranelift/src/constant.rs
+++ b/compiler/rustc_codegen_cranelift/src/constant.rs
@@ -530,6 +530,7 @@ pub(crate) fn mir_operand_get_const_val<'tcx>(
                         | StatementKind::Retag(_, _)
                         | StatementKind::AscribeUserType(_, _)
                         | StatementKind::Coverage(_)
+                        | StatementKind::ConstEvalCounter
                         | StatementKind::Nop => {}
                     }
                 }
diff --git a/compiler/rustc_codegen_ssa/src/mir/statement.rs b/compiler/rustc_codegen_ssa/src/mir/statement.rs
index 19452c8cdc8..60fbceb344d 100644
--- a/compiler/rustc_codegen_ssa/src/mir/statement.rs
+++ b/compiler/rustc_codegen_ssa/src/mir/statement.rs
@@ -91,6 +91,7 @@ impl<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>> FunctionCx<'a, 'tcx, Bx> {
             mir::StatementKind::FakeRead(..)
             | mir::StatementKind::Retag { .. }
             | mir::StatementKind::AscribeUserType(..)
+            | mir::StatementKind::ConstEvalCounter
             | mir::StatementKind::Nop => {}
         }
     }
diff --git a/compiler/rustc_const_eval/src/const_eval/machine.rs b/compiler/rustc_const_eval/src/const_eval/machine.rs
index 4709514c82e..a5bc121485d 100644
--- a/compiler/rustc_const_eval/src/const_eval/machine.rs
+++ b/compiler/rustc_const_eval/src/const_eval/machine.rs
@@ -561,8 +561,8 @@ impl<'mir, 'tcx> interpret::Machine<'mir, 'tcx> for CompileTimeInterpreter<'mir,
         throw_unsup_format!("pointer arithmetic or comparison is not supported at compile-time");
     }
 
-    fn before_terminator(ecx: &mut InterpCx<'mir, 'tcx, Self>) -> InterpResult<'tcx> {
-        // The step limit has already been hit in a previous call to `before_terminator`.
+    fn increment_const_eval_counter(ecx: &mut InterpCx<'mir, 'tcx, Self>) -> InterpResult<'tcx> {
+        // The step limit has already been hit in a previous call to `increment_const_eval_counter`.
         if ecx.machine.steps_remaining == 0 {
             return Ok(());
         }
diff --git a/compiler/rustc_const_eval/src/interpret/machine.rs b/compiler/rustc_const_eval/src/interpret/machine.rs
index 248953de867..76ed7b80f8d 100644
--- a/compiler/rustc_const_eval/src/interpret/machine.rs
+++ b/compiler/rustc_const_eval/src/interpret/machine.rs
@@ -244,12 +244,18 @@ pub trait Machine<'mir, 'tcx>: Sized {
     }
 
     /// Called before a basic block terminator is executed.
-    /// You can use this to detect endlessly running programs.
     #[inline]
     fn before_terminator(_ecx: &mut InterpCx<'mir, 'tcx, Self>) -> InterpResult<'tcx> {
         Ok(())
     }
 
+    /// Called when the interpreter encounters a `StatementKind::ConstEvalCounter` instruction.
+    /// You can use this to detect long or endlessly running programs.
+    #[inline]
+    fn increment_const_eval_counter(_ecx: &mut InterpCx<'mir, 'tcx, Self>) -> InterpResult<'tcx> {
+        Ok(())
+    }
+
     /// Called before a global allocation is accessed.
     /// `def_id` is `Some` if this is the "lazy" allocation of a static.
     #[inline]
diff --git a/compiler/rustc_const_eval/src/interpret/step.rs b/compiler/rustc_const_eval/src/interpret/step.rs
index fad4cb06cd6..d101937fd74 100644
--- a/compiler/rustc_const_eval/src/interpret/step.rs
+++ b/compiler/rustc_const_eval/src/interpret/step.rs
@@ -129,6 +129,10 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
             // FIXME(#73156): Handle source code coverage in const eval
             Coverage(..) => {}
 
+            ConstEvalCounter => {
+                M::increment_const_eval_counter(self)?;
+            }
+
             // Defined to do nothing. These are added by optimization passes, to avoid changing the
             // size of MIR constantly.
             Nop => {}
diff --git a/compiler/rustc_const_eval/src/transform/check_consts/check.rs b/compiler/rustc_const_eval/src/transform/check_consts/check.rs
index cc40c2566d2..f47e3e86ebe 100644
--- a/compiler/rustc_const_eval/src/transform/check_consts/check.rs
+++ b/compiler/rustc_const_eval/src/transform/check_consts/check.rs
@@ -693,6 +693,7 @@ impl<'tcx> Visitor<'tcx> for Checker<'_, 'tcx> {
             | StatementKind::AscribeUserType(..)
             | StatementKind::Coverage(..)
             | StatementKind::Intrinsic(..)
+            | StatementKind::ConstEvalCounter
             | StatementKind::Nop => {}
         }
     }
diff --git a/compiler/rustc_const_eval/src/transform/validate.rs b/compiler/rustc_const_eval/src/transform/validate.rs
index fab92f6f6f3..a2f2457487a 100644
--- a/compiler/rustc_const_eval/src/transform/validate.rs
+++ b/compiler/rustc_const_eval/src/transform/validate.rs
@@ -761,6 +761,7 @@ impl<'a, 'tcx> Visitor<'tcx> for TypeChecker<'a, 'tcx> {
             StatementKind::StorageLive(..)
             | StatementKind::StorageDead(..)
             | StatementKind::Coverage(_)
+            | StatementKind::ConstEvalCounter
             | StatementKind::Nop => {}
         }
 
diff --git a/compiler/rustc_data_structures/src/graph/dominators/mod.rs b/compiler/rustc_data_structures/src/graph/dominators/mod.rs
index 6398a501983..0a21a4249c8 100644
--- a/compiler/rustc_data_structures/src/graph/dominators/mod.rs
+++ b/compiler/rustc_data_structures/src/graph/dominators/mod.rs
@@ -135,7 +135,47 @@ pub fn dominators<G: ControlFlowGraph>(graph: G) -> Dominators<G::Node> {
         // This loop computes the semi[w] for w.
         semi[w] = w;
         for v in graph.predecessors(pre_order_to_real[w]) {
-            // Reachable vertices may have unreachable predecessors, so ignore any of them
+            // TL;DR: Reachable vertices may have unreachable predecessors, so ignore any of them.
+            //
+            // Ignore blocks which are not connected to the entry block.
+            //
+            // The algorithm that was used to traverse the graph and build the
+            // `pre_order_to_real` and `real_to_pre_order` vectors does so by
+            // starting from the entry block and following the successors.
+            // Therefore, any blocks not reachable from the entry block will be
+            // set to `None` in the `pre_order_to_real` vector.
+            //
+            // For example, in this graph, A and B should be skipped:
+            //
+            //           ┌─────┐
+            //           │     │
+            //           └──┬──┘
+            //              │
+            //           ┌──▼──┐              ┌─────┐
+            //           │     │              │  A  │
+            //           └──┬──┘              └──┬──┘
+            //              │                    │
+            //      ┌───────┴───────┐            │
+            //      │               │            │
+            //   ┌──▼──┐         ┌──▼──┐      ┌──▼──┐
+            //   │     │         │     │      │  B  │
+            //   └──┬──┘         └──┬──┘      └──┬──┘
+            //      │               └──────┬─────┘
+            //   ┌──▼──┐                   │
+            //   │     │                   │
+            //   └──┬──┘                ┌──▼──┐
+            //      │                   │     │
+            //      │                   └─────┘
+            //   ┌──▼──┐
+            //   │     │
+            //   └──┬──┘
+            //      │
+            //   ┌──▼──┐
+            //   │     │
+            //   └─────┘
+            //
+            // ...this may be the case if a MirPass modifies the CFG to remove
+            // or rearrange certain blocks/edges.
             let Some(v) = real_to_pre_order[v] else {
                 continue
             };
@@ -264,13 +304,18 @@ fn compress(
     }
 }
 
+/// Tracks the list of dominators for each node.
 #[derive(Clone, Debug)]
 pub struct Dominators<N: Idx> {
     post_order_rank: IndexVec<N, usize>,
+    // Even though we track only the immediate dominator of each node, it's
+    // possible to get its full list of dominators by looking up the dominator
+    // of each dominator. (See the `impl Iterator for Iter` definition).
     immediate_dominators: IndexVec<N, Option<N>>,
 }
 
 impl<Node: Idx> Dominators<Node> {
+    /// Whether the given Node has an immediate dominator.
     pub fn is_reachable(&self, node: Node) -> bool {
         self.immediate_dominators[node].is_some()
     }
@@ -280,6 +325,8 @@ impl<Node: Idx> Dominators<Node> {
         self.immediate_dominators[node].unwrap()
     }
 
+    /// Provides an iterator over each dominator up the CFG, for the given Node.
+    /// See the `impl Iterator for Iter` definition to understand how this works.
     pub fn dominators(&self, node: Node) -> Iter<'_, Node> {
         assert!(self.is_reachable(node), "node {node:?} is not reachable");
         Iter { dominators: self, node: Some(node) }
diff --git a/compiler/rustc_interface/src/tests.rs b/compiler/rustc_interface/src/tests.rs
index f94bc4d4c66..52a4e0e7418 100644
--- a/compiler/rustc_interface/src/tests.rs
+++ b/compiler/rustc_interface/src/tests.rs
@@ -802,6 +802,7 @@ fn test_unstable_options_tracking_hash() {
     tracked!(teach, true);
     tracked!(thinlto, Some(true));
     tracked!(thir_unsafeck, true);
+    tracked!(tiny_const_eval_limit, true);
     tracked!(tls_model, Some(TlsModel::GeneralDynamic));
     tracked!(trait_solver, TraitSolver::Chalk);
     tracked!(translate_remapped_path_to_local_path, false);
diff --git a/compiler/rustc_middle/src/mir/mod.rs b/compiler/rustc_middle/src/mir/mod.rs
index bc3c38fdb1c..552af589bec 100644
--- a/compiler/rustc_middle/src/mir/mod.rs
+++ b/compiler/rustc_middle/src/mir/mod.rs
@@ -1463,6 +1463,7 @@ impl Debug for Statement<'_> {
             }
             Coverage(box ref coverage) => write!(fmt, "Coverage::{:?}", coverage.kind),
             Intrinsic(box ref intrinsic) => write!(fmt, "{intrinsic}"),
+            ConstEvalCounter => write!(fmt, "ConstEvalCounter"),
             Nop => write!(fmt, "nop"),
         }
     }
diff --git a/compiler/rustc_middle/src/mir/spanview.rs b/compiler/rustc_middle/src/mir/spanview.rs
index fb1e3d233a2..1610ae1ce14 100644
--- a/compiler/rustc_middle/src/mir/spanview.rs
+++ b/compiler/rustc_middle/src/mir/spanview.rs
@@ -250,6 +250,7 @@ pub fn statement_kind_name(statement: &Statement<'_>) -> &'static str {
         AscribeUserType(..) => "AscribeUserType",
         Coverage(..) => "Coverage",
         Intrinsic(..) => "Intrinsic",
+        ConstEvalCounter => "ConstEvalCounter",
         Nop => "Nop",
     }
 }
diff --git a/compiler/rustc_middle/src/mir/syntax.rs b/compiler/rustc_middle/src/mir/syntax.rs
index 52c2b10cbbe..549bc65d6d7 100644
--- a/compiler/rustc_middle/src/mir/syntax.rs
+++ b/compiler/rustc_middle/src/mir/syntax.rs
@@ -355,6 +355,12 @@ pub enum StatementKind<'tcx> {
     /// This avoids adding a new block and a terminator for simple intrinsics.
     Intrinsic(Box<NonDivergingIntrinsic<'tcx>>),
 
+    /// Instructs the const eval interpreter to increment a counter; this counter is used to track
+    /// how many steps the interpreter has taken. It is used to prevent the user from writing const
+    /// code that runs for too long or infinitely. Other than in the const eval interpreter, this
+    /// is a no-op.
+    ConstEvalCounter,
+
     /// No-op. Useful for deleting instructions without affecting statement indices.
     Nop,
 }
diff --git a/compiler/rustc_middle/src/mir/visit.rs b/compiler/rustc_middle/src/mir/visit.rs
index 1a264d2d5af..3ddac5e11fb 100644
--- a/compiler/rustc_middle/src/mir/visit.rs
+++ b/compiler/rustc_middle/src/mir/visit.rs
@@ -427,6 +427,7 @@ macro_rules! make_mir_visitor {
                             }
                         }
                     }
+                    StatementKind::ConstEvalCounter => {}
                     StatementKind::Nop => {}
                 }
             }
diff --git a/compiler/rustc_middle/src/ty/context.rs b/compiler/rustc_middle/src/ty/context.rs
index 526df090a3c..b63b9e754cf 100644
--- a/compiler/rustc_middle/src/ty/context.rs
+++ b/compiler/rustc_middle/src/ty/context.rs
@@ -77,6 +77,8 @@ use std::iter;
 use std::mem;
 use std::ops::{Bound, Deref};
 
+const TINY_CONST_EVAL_LIMIT: Limit = Limit(20);
+
 pub trait OnDiskCache<'tcx>: rustc_data_structures::sync::Sync {
     /// Creates a new `OnDiskCache` instance from the serialized data in `data`.
     fn new(sess: &'tcx Session, data: Mmap, start_pos: usize) -> Self
@@ -1104,7 +1106,11 @@ impl<'tcx> TyCtxt<'tcx> {
     }
 
     pub fn const_eval_limit(self) -> Limit {
-        self.limits(()).const_eval_limit
+        if self.sess.opts.unstable_opts.tiny_const_eval_limit {
+            TINY_CONST_EVAL_LIMIT
+        } else {
+            self.limits(()).const_eval_limit
+        }
     }
 
     pub fn all_traits(self) -> impl Iterator<Item = DefId> + 'tcx {
diff --git a/compiler/rustc_mir_dataflow/src/impls/liveness.rs b/compiler/rustc_mir_dataflow/src/impls/liveness.rs
index 923dc16c11b..2890fa32cc9 100644
--- a/compiler/rustc_mir_dataflow/src/impls/liveness.rs
+++ b/compiler/rustc_mir_dataflow/src/impls/liveness.rs
@@ -271,6 +271,7 @@ impl<'a, 'tcx> Analysis<'tcx> for MaybeTransitiveLiveLocals<'a> {
             | StatementKind::AscribeUserType(..)
             | StatementKind::Coverage(..)
             | StatementKind::Intrinsic(..)
+            | StatementKind::ConstEvalCounter
             | StatementKind::Nop => None,
         };
         if let Some(destination) = destination {
diff --git a/compiler/rustc_mir_dataflow/src/impls/storage_liveness.rs b/compiler/rustc_mir_dataflow/src/impls/storage_liveness.rs
index 8d379b90a86..fcf0ce9d821 100644
--- a/compiler/rustc_mir_dataflow/src/impls/storage_liveness.rs
+++ b/compiler/rustc_mir_dataflow/src/impls/storage_liveness.rs
@@ -141,6 +141,7 @@ impl<'mir, 'tcx> crate::GenKillAnalysis<'tcx> for MaybeRequiresStorage<'mir, 'tc
             StatementKind::AscribeUserType(..)
             | StatementKind::Coverage(..)
             | StatementKind::FakeRead(..)
+            | StatementKind::ConstEvalCounter
             | StatementKind::Nop
             | StatementKind::Retag(..)
             | StatementKind::Intrinsic(..)
diff --git a/compiler/rustc_mir_dataflow/src/move_paths/builder.rs b/compiler/rustc_mir_dataflow/src/move_paths/builder.rs
index f46fd118bde..0195693a7cb 100644
--- a/compiler/rustc_mir_dataflow/src/move_paths/builder.rs
+++ b/compiler/rustc_mir_dataflow/src/move_paths/builder.rs
@@ -331,6 +331,7 @@ impl<'b, 'a, 'tcx> Gatherer<'b, 'a, 'tcx> {
             | StatementKind::AscribeUserType(..)
             | StatementKind::Coverage(..)
             | StatementKind::Intrinsic(..)
+            | StatementKind::ConstEvalCounter
             | StatementKind::Nop => {}
         }
     }
diff --git a/compiler/rustc_mir_dataflow/src/value_analysis.rs b/compiler/rustc_mir_dataflow/src/value_analysis.rs
index 0522c657939..6bdbda909d7 100644
--- a/compiler/rustc_mir_dataflow/src/value_analysis.rs
+++ b/compiler/rustc_mir_dataflow/src/value_analysis.rs
@@ -84,7 +84,8 @@ pub trait ValueAnalysis<'tcx> {
             StatementKind::Retag(..) => {
                 // We don't track references.
             }
-            StatementKind::Nop
+            StatementKind::ConstEvalCounter
+            | StatementKind::Nop
             | StatementKind::FakeRead(..)
             | StatementKind::Coverage(..)
             | StatementKind::AscribeUserType(..) => (),
diff --git a/compiler/rustc_mir_transform/src/check_unsafety.rs b/compiler/rustc_mir_transform/src/check_unsafety.rs
index 9f006a76162..8afa53313fc 100644
--- a/compiler/rustc_mir_transform/src/check_unsafety.rs
+++ b/compiler/rustc_mir_transform/src/check_unsafety.rs
@@ -104,6 +104,7 @@ impl<'tcx> Visitor<'tcx> for UnsafetyChecker<'_, 'tcx> {
             | StatementKind::AscribeUserType(..)
             | StatementKind::Coverage(..)
             | StatementKind::Intrinsic(..)
+            | StatementKind::ConstEvalCounter
             | StatementKind::Nop => {
                 // safe (at least as emitted during MIR construction)
             }
diff --git a/compiler/rustc_mir_transform/src/coverage/spans.rs b/compiler/rustc_mir_transform/src/coverage/spans.rs
index 31d5541a31b..f973c1ed28f 100644
--- a/compiler/rustc_mir_transform/src/coverage/spans.rs
+++ b/compiler/rustc_mir_transform/src/coverage/spans.rs
@@ -802,6 +802,8 @@ pub(super) fn filtered_statement_span(statement: &Statement<'_>) -> Option<Span>
         | StatementKind::StorageDead(_)
         // Coverage should not be encountered, but don't inject coverage coverage
         | StatementKind::Coverage(_)
+        // Ignore `ConstEvalCounter`s
+        | StatementKind::ConstEvalCounter
         // Ignore `Nop`s
         | StatementKind::Nop => None,
 
diff --git a/compiler/rustc_mir_transform/src/ctfe_limit.rs b/compiler/rustc_mir_transform/src/ctfe_limit.rs
new file mode 100644
index 00000000000..7d127032179
--- /dev/null
+++ b/compiler/rustc_mir_transform/src/ctfe_limit.rs
@@ -0,0 +1,59 @@
+//! A pass that inserts the `ConstEvalCounter` instruction into any blocks that have a back edge
+//! (thus indicating there is a loop in the CFG), or whose terminator is a function call.
+use crate::MirPass;
+
+use rustc_data_structures::graph::dominators::Dominators;
+use rustc_middle::mir::{
+    BasicBlock, BasicBlockData, Body, Statement, StatementKind, TerminatorKind,
+};
+use rustc_middle::ty::TyCtxt;
+
+pub struct CtfeLimit;
+
+impl<'tcx> MirPass<'tcx> for CtfeLimit {
+    #[instrument(skip(self, _tcx, body))]
+    fn run_pass(&self, _tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
+        let doms = body.basic_blocks.dominators();
+        let indices: Vec<BasicBlock> = body
+            .basic_blocks
+            .iter_enumerated()
+            .filter_map(|(node, node_data)| {
+                if matches!(node_data.terminator().kind, TerminatorKind::Call { .. })
+                    // Back edges in a CFG indicate loops
+                    || has_back_edge(&doms, node, &node_data)
+                {
+                    Some(node)
+                } else {
+                    None
+                }
+            })
+            .collect();
+        for index in indices {
+            insert_counter(
+                body.basic_blocks_mut()
+                    .get_mut(index)
+                    .expect("basic_blocks index {index} should exist"),
+            );
+        }
+    }
+}
+
+fn has_back_edge(
+    doms: &Dominators<BasicBlock>,
+    node: BasicBlock,
+    node_data: &BasicBlockData<'_>,
+) -> bool {
+    if !doms.is_reachable(node) {
+        return false;
+    }
+    // Check if any of the dominators of the node are also the node's successor.
+    doms.dominators(node)
+        .any(|dom| node_data.terminator().successors().into_iter().any(|succ| succ == dom))
+}
+
+fn insert_counter(basic_block_data: &mut BasicBlockData<'_>) {
+    basic_block_data.statements.push(Statement {
+        source_info: basic_block_data.terminator().source_info,
+        kind: StatementKind::ConstEvalCounter,
+    });
+}
diff --git a/compiler/rustc_mir_transform/src/dead_store_elimination.rs b/compiler/rustc_mir_transform/src/dead_store_elimination.rs
index 09546330cec..9dbfb089dc6 100644
--- a/compiler/rustc_mir_transform/src/dead_store_elimination.rs
+++ b/compiler/rustc_mir_transform/src/dead_store_elimination.rs
@@ -53,6 +53,7 @@ pub fn eliminate<'tcx>(tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>, borrowed: &BitS
                 | StatementKind::StorageDead(_)
                 | StatementKind::Coverage(_)
                 | StatementKind::Intrinsic(_)
+                | StatementKind::ConstEvalCounter
                 | StatementKind::Nop => (),
 
                 StatementKind::FakeRead(_) | StatementKind::AscribeUserType(_, _) => {
diff --git a/compiler/rustc_mir_transform/src/dest_prop.rs b/compiler/rustc_mir_transform/src/dest_prop.rs
index 08e296a8371..20ffb0ab334 100644
--- a/compiler/rustc_mir_transform/src/dest_prop.rs
+++ b/compiler/rustc_mir_transform/src/dest_prop.rs
@@ -577,6 +577,7 @@ impl WriteInfo {
                 self.add_place(**place);
             }
             StatementKind::Intrinsic(_)
+            | StatementKind::ConstEvalCounter
             | StatementKind::Nop
             | StatementKind::Coverage(_)
             | StatementKind::StorageLive(_)
diff --git a/compiler/rustc_mir_transform/src/generator.rs b/compiler/rustc_mir_transform/src/generator.rs
index a9fd95f4541..5624e312da1 100644
--- a/compiler/rustc_mir_transform/src/generator.rs
+++ b/compiler/rustc_mir_transform/src/generator.rs
@@ -1657,6 +1657,7 @@ impl<'tcx> Visitor<'tcx> for EnsureGeneratorFieldAssignmentsNeverAlias<'_> {
             | StatementKind::AscribeUserType(..)
             | StatementKind::Coverage(..)
             | StatementKind::Intrinsic(..)
+            | StatementKind::ConstEvalCounter
             | StatementKind::Nop => {}
         }
     }
diff --git a/compiler/rustc_mir_transform/src/lib.rs b/compiler/rustc_mir_transform/src/lib.rs
index fcb09fa02dd..6858fa8e524 100644
--- a/compiler/rustc_mir_transform/src/lib.rs
+++ b/compiler/rustc_mir_transform/src/lib.rs
@@ -55,6 +55,7 @@ mod const_goto;
 mod const_prop;
 mod const_prop_lint;
 mod coverage;
+mod ctfe_limit;
 mod dataflow_const_prop;
 mod dead_store_elimination;
 mod deaggregator;
@@ -410,6 +411,8 @@ fn inner_mir_for_ctfe(tcx: TyCtxt<'_>, def: ty::WithOptConstParam<LocalDefId>) -
         }
     }
 
+    pm::run_passes(tcx, &mut body, &[&ctfe_limit::CtfeLimit], None);
+
     debug_assert!(!body.has_free_regions(), "Free regions in MIR for CTFE");
 
     body
diff --git a/compiler/rustc_mir_transform/src/remove_noop_landing_pads.rs b/compiler/rustc_mir_transform/src/remove_noop_landing_pads.rs
index f1bbf2ea7e8..e3a03aa08af 100644
--- a/compiler/rustc_mir_transform/src/remove_noop_landing_pads.rs
+++ b/compiler/rustc_mir_transform/src/remove_noop_landing_pads.rs
@@ -35,6 +35,7 @@ impl RemoveNoopLandingPads {
                 | StatementKind::StorageDead(_)
                 | StatementKind::AscribeUserType(..)
                 | StatementKind::Coverage(..)
+                | StatementKind::ConstEvalCounter
                 | StatementKind::Nop => {
                     // These are all noops in a landing pad
                 }
diff --git a/compiler/rustc_mir_transform/src/separate_const_switch.rs b/compiler/rustc_mir_transform/src/separate_const_switch.rs
index 2f116aaa958..a24d2d34d79 100644
--- a/compiler/rustc_mir_transform/src/separate_const_switch.rs
+++ b/compiler/rustc_mir_transform/src/separate_const_switch.rs
@@ -250,6 +250,7 @@ fn is_likely_const<'tcx>(mut tracked_place: Place<'tcx>, block: &BasicBlockData<
             | StatementKind::Coverage(_)
             | StatementKind::StorageDead(_)
             | StatementKind::Intrinsic(_)
+            | StatementKind::ConstEvalCounter
             | StatementKind::Nop => {}
         }
     }
@@ -318,6 +319,7 @@ fn find_determining_place<'tcx>(
             | StatementKind::AscribeUserType(_, _)
             | StatementKind::Coverage(_)
             | StatementKind::Intrinsic(_)
+            | StatementKind::ConstEvalCounter
             | StatementKind::Nop => {}
 
             // If the discriminant is set, it is always set
diff --git a/compiler/rustc_mir_transform/src/simplify.rs b/compiler/rustc_mir_transform/src/simplify.rs
index 8f6abe7a912..7b6fa2baf2f 100644
--- a/compiler/rustc_mir_transform/src/simplify.rs
+++ b/compiler/rustc_mir_transform/src/simplify.rs
@@ -517,7 +517,7 @@ impl<'tcx> Visitor<'tcx> for UsedLocals {
                 self.super_statement(statement, location);
             }
 
-            StatementKind::Nop => {}
+            StatementKind::ConstEvalCounter | StatementKind::Nop => {}
 
             StatementKind::StorageLive(_local) | StatementKind::StorageDead(_local) => {}
 
diff --git a/compiler/rustc_session/src/options.rs b/compiler/rustc_session/src/options.rs
index 66b100c103e..0db4d85ff4b 100644
--- a/compiler/rustc_session/src/options.rs
+++ b/compiler/rustc_session/src/options.rs
@@ -1618,6 +1618,8 @@ options! {
         "measure time of each LLVM pass (default: no)"),
     time_passes: bool = (false, parse_bool, [UNTRACKED],
         "measure time of each rustc pass (default: no)"),
+    tiny_const_eval_limit: bool = (false, parse_bool, [TRACKED],
+        "sets a tiny, non-configurable limit for const eval; useful for compiler tests"),
     #[rustc_lint_opt_deny_field_access("use `Session::tls_model` instead of this field")]
     tls_model: Option<TlsModel> = (None, parse_tls_model, [TRACKED],
         "choose the TLS model to use (`rustc --print tls-models` for details)"),
diff --git a/src/doc/unstable-book/src/compiler-flags/tiny-const-eval-limit.md b/src/doc/unstable-book/src/compiler-flags/tiny-const-eval-limit.md
new file mode 100644
index 00000000000..51c5fd69c63
--- /dev/null
+++ b/src/doc/unstable-book/src/compiler-flags/tiny-const-eval-limit.md
@@ -0,0 +1,6 @@
+# `tiny-const-eval-limit`
+
+--------------------
+
+The `-Ztiny-const-eval-limit` compiler flag sets a tiny, non-configurable limit for const eval.
+This flag should only be used by const eval tests in the rustc test suite.
diff --git a/src/tools/clippy/clippy_utils/src/qualify_min_const_fn.rs b/src/tools/clippy/clippy_utils/src/qualify_min_const_fn.rs
index 13de780b710..72705878075 100644
--- a/src/tools/clippy/clippy_utils/src/qualify_min_const_fn.rs
+++ b/src/tools/clippy/clippy_utils/src/qualify_min_const_fn.rs
@@ -240,6 +240,7 @@ fn check_statement<'tcx>(
         | StatementKind::Retag { .. }
         | StatementKind::AscribeUserType(..)
         | StatementKind::Coverage(..)
+        | StatementKind::ConstEvalCounter
         | StatementKind::Nop => Ok(()),
     }
 }
diff --git a/tests/rustdoc-ui/z-help.stdout b/tests/rustdoc-ui/z-help.stdout
index 546df947c0f..4f07fca82d1 100644
--- a/tests/rustdoc-ui/z-help.stdout
+++ b/tests/rustdoc-ui/z-help.stdout
@@ -173,6 +173,7 @@
     -Z                                 threads=val -- use a thread pool with N threads
     -Z                        time-llvm-passes=val -- measure time of each LLVM pass (default: no)
     -Z                             time-passes=val -- measure time of each rustc pass (default: no)
+    -Z                   tiny-const-eval-limit=val -- sets a tiny, non-configurable limit for const eval; useful for compiler tests
     -Z                               tls-model=val -- choose the TLS model to use (`rustc --print tls-models` for details)
     -Z                            trace-macros=val -- for every macro invocation, print its name and arguments (default: no)
     -Z                       track-diagnostics=val -- tracks where in rustc a diagnostic was emitted
diff --git a/tests/ui/consts/const-eval/infinite_loop.stderr b/tests/ui/consts/const-eval/infinite_loop.stderr
index 8b58cb279f3..f30bfaf3f95 100644
--- a/tests/ui/consts/const-eval/infinite_loop.stderr
+++ b/tests/ui/consts/const-eval/infinite_loop.stderr
@@ -1,8 +1,11 @@
 error[E0080]: evaluation of constant value failed
-  --> $DIR/infinite_loop.rs:6:15
+  --> $DIR/infinite_loop.rs:6:9
    |
-LL |         while n != 0 {
-   |               ^^^^^^ exceeded interpreter step limit (see `#[const_eval_limit]`)
+LL | /         while n != 0 {
+LL | |
+LL | |             n = if n % 2 == 0 { n/2 } else { 3*n + 1 };
+LL | |         }
+   | |_________^ exceeded interpreter step limit (see `#[const_eval_limit]`)
 
 error: aborting due to previous error
 
diff --git a/tests/ui/consts/const-eval/issue-52475.rs b/tests/ui/consts/const-eval/issue-52475.rs
index ce65407bbab..307c1a66834 100644
--- a/tests/ui/consts/const-eval/issue-52475.rs
+++ b/tests/ui/consts/const-eval/issue-52475.rs
@@ -2,8 +2,8 @@ fn main() {
     let _ = [(); {
         let mut x = &0;
         let mut n = 0;
-        while n < 5 {
-            n = (n + 1) % 5; //~ ERROR evaluation of constant value failed
+        while n < 5 { //~ ERROR evaluation of constant value failed [E0080]
+            n = (n + 1) % 5;
             x = &0; // Materialize a new AllocId
         }
         0
diff --git a/tests/ui/consts/const-eval/issue-52475.stderr b/tests/ui/consts/const-eval/issue-52475.stderr
index 8536ff02c6d..3aa6bd277dd 100644
--- a/tests/ui/consts/const-eval/issue-52475.stderr
+++ b/tests/ui/consts/const-eval/issue-52475.stderr
@@ -1,8 +1,11 @@
 error[E0080]: evaluation of constant value failed
-  --> $DIR/issue-52475.rs:6:17
+  --> $DIR/issue-52475.rs:5:9
    |
-LL |             n = (n + 1) % 5;
-   |                 ^^^^^^^^^^^ exceeded interpreter step limit (see `#[const_eval_limit]`)
+LL | /         while n < 5 {
+LL | |             n = (n + 1) % 5;
+LL | |             x = &0; // Materialize a new AllocId
+LL | |         }
+   | |_________^ exceeded interpreter step limit (see `#[const_eval_limit]`)
 
 error: aborting due to previous error
 
diff --git a/tests/ui/consts/const-eval/stable-metric/ctfe-fn-call.rs b/tests/ui/consts/const-eval/stable-metric/ctfe-fn-call.rs
new file mode 100644
index 00000000000..c59596238e1
--- /dev/null
+++ b/tests/ui/consts/const-eval/stable-metric/ctfe-fn-call.rs
@@ -0,0 +1,36 @@
+// check-fail
+// compile-flags: -Z tiny-const-eval-limit
+
+const fn foo() {}
+
+const fn call_foo() -> u32 {
+    foo();
+    foo();
+    foo();
+    foo();
+    foo();
+
+    foo();
+    foo();
+    foo();
+    foo();
+    foo();
+
+    foo();
+    foo();
+    foo();
+    foo();
+    foo();
+
+    foo();
+    foo();
+    foo();
+    foo(); //~ ERROR evaluation of constant value failed [E0080]
+    0
+}
+
+const X: u32 = call_foo();
+
+fn main() {
+    println!("{X}");
+}
diff --git a/tests/ui/consts/const-eval/stable-metric/ctfe-fn-call.stderr b/tests/ui/consts/const-eval/stable-metric/ctfe-fn-call.stderr
new file mode 100644
index 00000000000..ed70975af34
--- /dev/null
+++ b/tests/ui/consts/const-eval/stable-metric/ctfe-fn-call.stderr
@@ -0,0 +1,20 @@
+error[E0080]: evaluation of constant value failed
+  --> $DIR/ctfe-fn-call.rs:28:5
+   |
+LL |     foo();
+   |     ^^^^^ exceeded interpreter step limit (see `#[const_eval_limit]`)
+   |
+note: inside `call_foo`
+  --> $DIR/ctfe-fn-call.rs:28:5
+   |
+LL |     foo();
+   |     ^^^^^
+note: inside `X`
+  --> $DIR/ctfe-fn-call.rs:32:16
+   |
+LL | const X: u32 = call_foo();
+   |                ^^^^^^^^^^
+
+error: aborting due to previous error
+
+For more information about this error, try `rustc --explain E0080`.
diff --git a/tests/ui/consts/const-eval/stable-metric/ctfe-labelled-loop.rs b/tests/ui/consts/const-eval/stable-metric/ctfe-labelled-loop.rs
new file mode 100644
index 00000000000..c10b8d83791
--- /dev/null
+++ b/tests/ui/consts/const-eval/stable-metric/ctfe-labelled-loop.rs
@@ -0,0 +1,19 @@
+// check-fail
+// compile-flags: -Z tiny-const-eval-limit
+
+const fn labelled_loop(n: u32) -> u32 {
+    let mut i = 0;
+    'mylabel: loop { //~ ERROR evaluation of constant value failed [E0080]
+        if i > n {
+            break 'mylabel
+        }
+        i += 1;
+    }
+    0
+}
+
+const X: u32 = labelled_loop(19);
+
+fn main() {
+    println!("{X}");
+}
diff --git a/tests/ui/consts/const-eval/stable-metric/ctfe-labelled-loop.stderr b/tests/ui/consts/const-eval/stable-metric/ctfe-labelled-loop.stderr
new file mode 100644
index 00000000000..d9404edd5b1
--- /dev/null
+++ b/tests/ui/consts/const-eval/stable-metric/ctfe-labelled-loop.stderr
@@ -0,0 +1,30 @@
+error[E0080]: evaluation of constant value failed
+  --> $DIR/ctfe-labelled-loop.rs:6:5
+   |
+LL | /     'mylabel: loop {
+LL | |         if i > n {
+LL | |             break 'mylabel
+LL | |         }
+LL | |         i += 1;
+LL | |     }
+   | |_____^ exceeded interpreter step limit (see `#[const_eval_limit]`)
+   |
+note: inside `labelled_loop`
+  --> $DIR/ctfe-labelled-loop.rs:6:5
+   |
+LL | /     'mylabel: loop {
+LL | |         if i > n {
+LL | |             break 'mylabel
+LL | |         }
+LL | |         i += 1;
+LL | |     }
+   | |_____^
+note: inside `X`
+  --> $DIR/ctfe-labelled-loop.rs:15:16
+   |
+LL | const X: u32 = labelled_loop(19);
+   |                ^^^^^^^^^^^^^^^^^
+
+error: aborting due to previous error
+
+For more information about this error, try `rustc --explain E0080`.
diff --git a/tests/ui/consts/const-eval/stable-metric/ctfe-recursion.rs b/tests/ui/consts/const-eval/stable-metric/ctfe-recursion.rs
new file mode 100644
index 00000000000..80ff835f3e8
--- /dev/null
+++ b/tests/ui/consts/const-eval/stable-metric/ctfe-recursion.rs
@@ -0,0 +1,16 @@
+// check-fail
+// compile-flags: -Z tiny-const-eval-limit
+
+const fn recurse(n: u32) -> u32 {
+    if n == 0 {
+        n
+    } else {
+        recurse(n - 1) //~ ERROR evaluation of constant value failed [E0080]
+    }
+}
+
+const X: u32 = recurse(19);
+
+fn main() {
+    println!("{X}");
+}
diff --git a/tests/ui/consts/const-eval/stable-metric/ctfe-recursion.stderr b/tests/ui/consts/const-eval/stable-metric/ctfe-recursion.stderr
new file mode 100644
index 00000000000..ed9a3111942
--- /dev/null
+++ b/tests/ui/consts/const-eval/stable-metric/ctfe-recursion.stderr
@@ -0,0 +1,25 @@
+error[E0080]: evaluation of constant value failed
+  --> $DIR/ctfe-recursion.rs:8:9
+   |
+LL |         recurse(n - 1)
+   |         ^^^^^^^^^^^^^^ exceeded interpreter step limit (see `#[const_eval_limit]`)
+   |
+note: inside `recurse`
+  --> $DIR/ctfe-recursion.rs:8:9
+   |
+LL |         recurse(n - 1)
+   |         ^^^^^^^^^^^^^^
+note: [... 18 additional calls inside `recurse` ...]
+  --> $DIR/ctfe-recursion.rs:8:9
+   |
+LL |         recurse(n - 1)
+   |         ^^^^^^^^^^^^^^
+note: inside `X`
+  --> $DIR/ctfe-recursion.rs:12:16
+   |
+LL | const X: u32 = recurse(19);
+   |                ^^^^^^^^^^^
+
+error: aborting due to previous error
+
+For more information about this error, try `rustc --explain E0080`.
diff --git a/tests/ui/consts/const-eval/stable-metric/ctfe-simple-loop.rs b/tests/ui/consts/const-eval/stable-metric/ctfe-simple-loop.rs
new file mode 100644
index 00000000000..ca0eec93c5d
--- /dev/null
+++ b/tests/ui/consts/const-eval/stable-metric/ctfe-simple-loop.rs
@@ -0,0 +1,15 @@
+// check-fail
+// compile-flags: -Z tiny-const-eval-limit
+const fn simple_loop(n: u32) -> u32 {
+    let mut index = 0;
+    while index < n { //~ ERROR evaluation of constant value failed [E0080]
+        index = index + 1;
+    }
+    0
+}
+
+const X: u32 = simple_loop(19);
+
+fn main() {
+    println!("{X}");
+}
diff --git a/tests/ui/consts/const-eval/stable-metric/ctfe-simple-loop.stderr b/tests/ui/consts/const-eval/stable-metric/ctfe-simple-loop.stderr
new file mode 100644
index 00000000000..83ff275de70
--- /dev/null
+++ b/tests/ui/consts/const-eval/stable-metric/ctfe-simple-loop.stderr
@@ -0,0 +1,24 @@
+error[E0080]: evaluation of constant value failed
+  --> $DIR/ctfe-simple-loop.rs:5:5
+   |
+LL | /     while index < n {
+LL | |         index = index + 1;
+LL | |     }
+   | |_____^ exceeded interpreter step limit (see `#[const_eval_limit]`)
+   |
+note: inside `simple_loop`
+  --> $DIR/ctfe-simple-loop.rs:5:5
+   |
+LL | /     while index < n {
+LL | |         index = index + 1;
+LL | |     }
+   | |_____^
+note: inside `X`
+  --> $DIR/ctfe-simple-loop.rs:11:16
+   |
+LL | const X: u32 = simple_loop(19);
+   |                ^^^^^^^^^^^^^^^
+
+error: aborting due to previous error
+
+For more information about this error, try `rustc --explain E0080`.
diff --git a/tests/ui/consts/const-eval/stable-metric/dominators-edge-case.rs b/tests/ui/consts/const-eval/stable-metric/dominators-edge-case.rs
new file mode 100644
index 00000000000..0b0f361809f
--- /dev/null
+++ b/tests/ui/consts/const-eval/stable-metric/dominators-edge-case.rs
@@ -0,0 +1,19 @@
+// check-pass
+//
+// Exercising an edge case which was found during Stage 2 compilation.
+// Compilation would fail for this code when running the `CtfeLimit`
+// MirPass (specifically when looking up the dominators).
+#![crate_type="lib"]
+
+const DUMMY: Expr = Expr::Path(ExprPath {
+    attrs: Vec::new(),
+    path: Vec::new(),
+});
+
+pub enum Expr {
+    Path(ExprPath),
+}
+pub struct ExprPath {
+    pub attrs: Vec<()>,
+    pub path: Vec<()>,
+}
diff --git a/tests/ui/consts/const_limit/const_eval_limit_reached.stderr b/tests/ui/consts/const_limit/const_eval_limit_reached.stderr
index 850aebdfb2a..a8e8ae9bb08 100644
--- a/tests/ui/consts/const_limit/const_eval_limit_reached.stderr
+++ b/tests/ui/consts/const_limit/const_eval_limit_reached.stderr
@@ -1,8 +1,11 @@
 error[E0080]: evaluation of constant value failed
-  --> $DIR/const_eval_limit_reached.rs:6:11
+  --> $DIR/const_eval_limit_reached.rs:6:5
    |
-LL |     while x != 1000 {
-   |           ^^^^^^^^^ exceeded interpreter step limit (see `#[const_eval_limit]`)
+LL | /     while x != 1000 {
+LL | |
+LL | |         x += 1;
+LL | |     }
+   | |_____^ exceeded interpreter step limit (see `#[const_eval_limit]`)
 
 error: aborting due to previous error