Skip to content

Commit 5d2375f

Browse files
authored
Rollup merge of #139042 - compiler-errors:do-not-optimize-switchint, r=saethlin
Do not remove trivial `SwitchInt` in analysis MIR This PR ensures that we don't prematurely remove trivial `SwitchInt` terminators which affects both the borrow-checking and runtime semantics (i.e. UB) of the code. Previously the `SimplifyCfg` optimization was removing `SwitchInt` terminators when they was "trivial", i.e. when all arms branched to the same basic block, even if that `SwitchInt` terminator had the side-effect of reading an operand which (for example) may not be initialized or may point to an invalid place in memory. This behavior is unlike all other optimizations, which are only applied after "analysis" (i.e. borrow-checking) is finished, and which Miri disables to make sure the compiler doesn't silently remove UB. Fixing this code "breaks" (i.e. unmasks) code that used to borrow-check but no longer does, like: ```rust fn foo() { let x; let (0 | _) = x; } ``` This match expression should perform a read because `_` does not shadow the `0` literal pattern, and the compiler should have to read the match scrutinee to compare it to 0. I've checked that this behavior does not actually manifest in practice via a crater run which came back clean: #139042 (comment) As a side-note, it may be tempting to suggest that this is actually a good thing or that we should preserve this behavior. If we wanted to make this work (i.e. trivially optimize out reads from matches that are redundant like `0 | _`), then we should be enabling this behavior *after* fixing this. However, I think it's kinda unprincipled, and for example other variations of the code don't even work today, e.g.: ```rust fn foo() { let x; let (0.. | _) = x; } ```
2 parents 077cedc + 3ee62a9 commit 5d2375f

18 files changed

+197
-51
lines changed

Diff for: compiler/rustc_interface/src/tests.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -818,8 +818,8 @@ fn test_unstable_options_tracking_hash() {
818818
tracked!(min_function_alignment, Some(Align::EIGHT));
819819
tracked!(mir_emit_retag, true);
820820
tracked!(mir_enable_passes, vec![("DestProp".to_string(), false)]);
821-
tracked!(mir_keep_place_mention, true);
822821
tracked!(mir_opt_level, Some(4));
822+
tracked!(mir_preserve_ub, true);
823823
tracked!(move_size_limit, Some(4096));
824824
tracked!(mutable_noalias, false);
825825
tracked!(next_solver, NextSolverConfig { coherence: true, globally: true });

Diff for: compiler/rustc_mir_transform/src/early_otherwise_branch.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ impl<'tcx> crate::MirPass<'tcx> for EarlyOtherwiseBranch {
223223
// Since this optimization adds new basic blocks and invalidates others,
224224
// clean up the cfg to make it nicer for other passes
225225
if should_cleanup {
226-
simplify_cfg(body);
226+
simplify_cfg(tcx, body);
227227
}
228228
}
229229

Diff for: compiler/rustc_mir_transform/src/inline.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ impl<'tcx> crate::MirPass<'tcx> for Inline {
6363
let _guard = span.enter();
6464
if inline::<NormalInliner<'tcx>>(tcx, body) {
6565
debug!("running simplify cfg on {:?}", body.source);
66-
simplify_cfg(body);
66+
simplify_cfg(tcx, body);
6767
deref_finder(tcx, body);
6868
}
6969
}
@@ -99,7 +99,7 @@ impl<'tcx> crate::MirPass<'tcx> for ForceInline {
9999
let _guard = span.enter();
100100
if inline::<ForceInliner<'tcx>>(tcx, body) {
101101
debug!("running simplify cfg on {:?}", body.source);
102-
simplify_cfg(body);
102+
simplify_cfg(tcx, body);
103103
deref_finder(tcx, body);
104104
}
105105
}

Diff for: compiler/rustc_mir_transform/src/match_branches.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ impl<'tcx> crate::MirPass<'tcx> for MatchBranchSimplification {
4343
}
4444

4545
if should_cleanup {
46-
simplify_cfg(body);
46+
simplify_cfg(tcx, body);
4747
}
4848
}
4949

Diff for: compiler/rustc_mir_transform/src/remove_place_mention.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ pub(super) struct RemovePlaceMention;
88

99
impl<'tcx> crate::MirPass<'tcx> for RemovePlaceMention {
1010
fn is_enabled(&self, sess: &rustc_session::Session) -> bool {
11-
!sess.opts.unstable_opts.mir_keep_place_mention
11+
!sess.opts.unstable_opts.mir_preserve_ub
1212
}
1313

1414
fn run_pass(&self, _: TyCtxt<'tcx>, body: &mut Body<'tcx>) {

Diff for: compiler/rustc_mir_transform/src/remove_unneeded_drops.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ impl<'tcx> crate::MirPass<'tcx> for RemoveUnneededDrops {
3535
// if we applied optimizations, we potentially have some cfg to cleanup to
3636
// make it easier for further passes
3737
if should_simplify {
38-
simplify_cfg(body);
38+
simplify_cfg(tcx, body);
3939
}
4040
}
4141

Diff for: compiler/rustc_mir_transform/src/simplify.rs

+26-9
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@
2626
//! Here the block (`{ return; }`) has the return type `char`, rather than `()`, but the MIR we
2727
//! naively generate still contains the `_a = ()` write in the unreachable block "after" the
2828
//! return.
29+
//!
30+
//! **WARNING**: This is one of the few optimizations that runs on built and analysis MIR, and
31+
//! so its effects may affect the type-checking, borrow-checking, and other analysis of MIR.
32+
//! We must be extremely careful to only apply optimizations that preserve UB and all
33+
//! non-determinism, since changes here can affect which programs compile in an insta-stable way.
34+
//! The normal logic that a program with UB can be changed to do anything does not apply to
35+
//! pre-"runtime" MIR!
2936
3037
use rustc_index::{Idx, IndexSlice, IndexVec};
3138
use rustc_middle::mir::visit::{MutVisitor, MutatingUseContext, PlaceContext, Visitor};
@@ -66,8 +73,8 @@ impl SimplifyCfg {
6673
}
6774
}
6875

69-
pub(super) fn simplify_cfg(body: &mut Body<'_>) {
70-
CfgSimplifier::new(body).simplify();
76+
pub(super) fn simplify_cfg<'tcx>(tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
77+
CfgSimplifier::new(tcx, body).simplify();
7178
remove_dead_blocks(body);
7279

7380
// FIXME: Should probably be moved into some kind of pass manager
@@ -79,9 +86,9 @@ impl<'tcx> crate::MirPass<'tcx> for SimplifyCfg {
7986
self.name()
8087
}
8188

82-
fn run_pass(&self, _: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
89+
fn run_pass(&self, tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
8390
debug!("SimplifyCfg({:?}) - simplifying {:?}", self.name(), body.source);
84-
simplify_cfg(body);
91+
simplify_cfg(tcx, body);
8592
}
8693

8794
fn is_required(&self) -> bool {
@@ -90,12 +97,13 @@ impl<'tcx> crate::MirPass<'tcx> for SimplifyCfg {
9097
}
9198

9299
struct CfgSimplifier<'a, 'tcx> {
100+
preserve_switch_reads: bool,
93101
basic_blocks: &'a mut IndexSlice<BasicBlock, BasicBlockData<'tcx>>,
94102
pred_count: IndexVec<BasicBlock, u32>,
95103
}
96104

97105
impl<'a, 'tcx> CfgSimplifier<'a, 'tcx> {
98-
fn new(body: &'a mut Body<'tcx>) -> Self {
106+
fn new(tcx: TyCtxt<'tcx>, body: &'a mut Body<'tcx>) -> Self {
99107
let mut pred_count = IndexVec::from_elem(0u32, &body.basic_blocks);
100108

101109
// we can't use mir.predecessors() here because that counts
@@ -110,9 +118,12 @@ impl<'a, 'tcx> CfgSimplifier<'a, 'tcx> {
110118
}
111119
}
112120

121+
// Preserve `SwitchInt` reads on built and analysis MIR, or if `-Zmir-preserve-ub`.
122+
let preserve_switch_reads = matches!(body.phase, MirPhase::Built | MirPhase::Analysis(_))
123+
|| tcx.sess.opts.unstable_opts.mir_preserve_ub;
113124
let basic_blocks = body.basic_blocks_mut();
114125

115-
CfgSimplifier { basic_blocks, pred_count }
126+
CfgSimplifier { preserve_switch_reads, basic_blocks, pred_count }
116127
}
117128

118129
fn simplify(mut self) {
@@ -253,9 +264,15 @@ impl<'a, 'tcx> CfgSimplifier<'a, 'tcx> {
253264

254265
// turn a branch with all successors identical to a goto
255266
fn simplify_branch(&mut self, terminator: &mut Terminator<'tcx>) -> bool {
256-
match terminator.kind {
257-
TerminatorKind::SwitchInt { .. } => {}
258-
_ => return false,
267+
// Removing a `SwitchInt` terminator may remove reads that result in UB,
268+
// so we must not apply this optimization before borrowck or when
269+
// `-Zmir-preserve-ub` is set.
270+
if self.preserve_switch_reads {
271+
return false;
272+
}
273+
274+
let TerminatorKind::SwitchInt { .. } = terminator.kind else {
275+
return false;
259276
};
260277

261278
let first_succ = {

Diff for: compiler/rustc_session/src/options.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -2322,12 +2322,12 @@ options! {
23222322
mir_include_spans: MirIncludeSpans = (MirIncludeSpans::default(), parse_mir_include_spans, [UNTRACKED],
23232323
"include extra comments in mir pretty printing, like line numbers and statement indices, \
23242324
details about types, etc. (boolean for all passes, 'nll' to enable in NLL MIR only, default: 'nll')"),
2325-
mir_keep_place_mention: bool = (false, parse_bool, [TRACKED],
2326-
"keep place mention MIR statements, interpreted e.g., by miri; implies -Zmir-opt-level=0 \
2327-
(default: no)"),
23282325
#[rustc_lint_opt_deny_field_access("use `Session::mir_opt_level` instead of this field")]
23292326
mir_opt_level: Option<usize> = (None, parse_opt_number, [TRACKED],
23302327
"MIR optimization level (0-4; default: 1 in non optimized builds and 2 in optimized builds)"),
2328+
mir_preserve_ub: bool = (false, parse_bool, [TRACKED],
2329+
"keep place mention statements and reads in trivial SwitchInt terminators, which are interpreted \
2330+
e.g., by miri; implies -Zmir-opt-level=0 (default: no)"),
23312331
mir_strip_debuginfo: MirStripDebugInfo = (MirStripDebugInfo::None, parse_mir_strip_debuginfo, [TRACKED],
23322332
"Whether to remove some of the MIR debug info from methods. Default: None"),
23332333
move_size_limit: Option<usize> = (None, parse_opt_number, [TRACKED],

Diff for: src/tools/miri/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ pub const MIRI_DEFAULT_ARGS: &[&str] = &[
169169
"-Zalways-encode-mir",
170170
"-Zextra-const-ub-checks",
171171
"-Zmir-emit-retag",
172-
"-Zmir-keep-place-mention",
172+
"-Zmir-preserve-ub",
173173
"-Zmir-opt-level=0",
174174
"-Zmir-enable-passes=-CheckAlignment,-CheckNull",
175175
// Deduplicating diagnostics means we miss events when tracking what happens during an
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Ensure that we don't optimize out `SwitchInt` reads even if that terminator
2+
// branches to the same basic block on every target, since the operand may have
3+
// side-effects that affect analysis of the MIR.
4+
//
5+
// See <https://github.com/rust-lang/miri/issues/4237>.
6+
7+
use std::mem::MaybeUninit;
8+
9+
fn main() {
10+
let uninit: MaybeUninit<i32> = MaybeUninit::uninit();
11+
let bad_ref: &i32 = unsafe { uninit.assume_init_ref() };
12+
let &(0 | _) = bad_ref;
13+
//~^ ERROR: Undefined Behavior: using uninitialized data, but this operation requires initialized memory
14+
}
+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
error: Undefined Behavior: using uninitialized data, but this operation requires initialized memory
2+
--> tests/fail/read_from_trivial_switch.rs:LL:CC
3+
|
4+
LL | let &(0 | _) = bad_ref;
5+
| ^^^^^^^^ using uninitialized data, but this operation requires initialized memory
6+
|
7+
= help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
8+
= help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
9+
= note: BACKTRACE:
10+
= note: inside `main` at tests/fail/read_from_trivial_switch.rs:LL:CC
11+
12+
note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
13+
14+
error: aborting due to 1 previous error
15+

Diff for: tests/mir-opt/building/match/exponential_or.match_tuple.SimplifyCfg-initial.after.mir

+19-15
Original file line numberDiff line numberDiff line change
@@ -24,43 +24,47 @@ fn match_tuple(_1: (u32, bool, Option<i32>, u32)) -> u32 {
2424

2525
bb1: {
2626
_0 = const 0_u32;
27-
goto -> bb10;
27+
goto -> bb11;
2828
}
2929

3030
bb2: {
31-
_2 = discriminant((_1.2: std::option::Option<i32>));
32-
switchInt(move _2) -> [0: bb4, 1: bb3, otherwise: bb1];
31+
switchInt(copy (_1.1: bool)) -> [0: bb3, otherwise: bb3];
3332
}
3433

3534
bb3: {
36-
switchInt(copy (((_1.2: std::option::Option<i32>) as Some).0: i32)) -> [1: bb4, 8: bb4, otherwise: bb1];
35+
_2 = discriminant((_1.2: std::option::Option<i32>));
36+
switchInt(move _2) -> [0: bb5, 1: bb4, otherwise: bb1];
3737
}
3838

3939
bb4: {
40-
_5 = Le(const 6_u32, copy (_1.3: u32));
41-
switchInt(move _5) -> [0: bb5, otherwise: bb7];
40+
switchInt(copy (((_1.2: std::option::Option<i32>) as Some).0: i32)) -> [1: bb5, 8: bb5, otherwise: bb1];
4241
}
4342

4443
bb5: {
45-
_3 = Le(const 13_u32, copy (_1.3: u32));
46-
switchInt(move _3) -> [0: bb1, otherwise: bb6];
44+
_5 = Le(const 6_u32, copy (_1.3: u32));
45+
switchInt(move _5) -> [0: bb6, otherwise: bb8];
4746
}
4847

4948
bb6: {
50-
_4 = Le(copy (_1.3: u32), const 16_u32);
51-
switchInt(move _4) -> [0: bb1, otherwise: bb8];
49+
_3 = Le(const 13_u32, copy (_1.3: u32));
50+
switchInt(move _3) -> [0: bb1, otherwise: bb7];
5251
}
5352

5453
bb7: {
55-
_6 = Le(copy (_1.3: u32), const 9_u32);
56-
switchInt(move _6) -> [0: bb5, otherwise: bb8];
54+
_4 = Le(copy (_1.3: u32), const 16_u32);
55+
switchInt(move _4) -> [0: bb1, otherwise: bb9];
5756
}
5857

5958
bb8: {
60-
falseEdge -> [real: bb9, imaginary: bb1];
59+
_6 = Le(copy (_1.3: u32), const 9_u32);
60+
switchInt(move _6) -> [0: bb6, otherwise: bb9];
6161
}
6262

6363
bb9: {
64+
falseEdge -> [real: bb10, imaginary: bb1];
65+
}
66+
67+
bb10: {
6468
StorageLive(_7);
6569
_7 = copy (_1.0: u32);
6670
StorageLive(_8);
@@ -74,10 +78,10 @@ fn match_tuple(_1: (u32, bool, Option<i32>, u32)) -> u32 {
7478
StorageDead(_9);
7579
StorageDead(_8);
7680
StorageDead(_7);
77-
goto -> bb10;
81+
goto -> bb11;
7882
}
7983

80-
bb10: {
84+
bb11: {
8185
return;
8286
}
8387
}

Diff for: tests/mir-opt/dead-store-elimination/place_mention.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// and don't remove it as a dead store.
33
//
44
//@ test-mir-pass: DeadStoreElimination-initial
5-
//@ compile-flags: -Zmir-keep-place-mention
5+
//@ compile-flags: -Zmir-preserve-ub
66

77
// EMIT_MIR place_mention.main.DeadStoreElimination-initial.diff
88
fn main() {

Diff for: tests/mir-opt/or_pattern.single_switchint.SimplifyCfg-initial.after.mir

+23-15
Original file line numberDiff line numberDiff line change
@@ -14,59 +14,67 @@ fn single_switchint() -> () {
1414
}
1515

1616
bb1: {
17-
switchInt(copy (_2.0: i32)) -> [3: bb8, 4: bb8, otherwise: bb7];
17+
switchInt(copy (_2.0: i32)) -> [3: bb9, 4: bb9, otherwise: bb8];
1818
}
1919

2020
bb2: {
2121
switchInt(copy (_2.1: bool)) -> [0: bb6, otherwise: bb3];
2222
}
2323

2424
bb3: {
25-
falseEdge -> [real: bb12, imaginary: bb4];
25+
falseEdge -> [real: bb14, imaginary: bb4];
2626
}
2727

2828
bb4: {
2929
switchInt(copy (_2.1: bool)) -> [0: bb5, otherwise: bb6];
3030
}
3131

3232
bb5: {
33-
falseEdge -> [real: bb11, imaginary: bb6];
33+
falseEdge -> [real: bb13, imaginary: bb6];
3434
}
3535

3636
bb6: {
37-
falseEdge -> [real: bb10, imaginary: bb1];
37+
switchInt(copy (_2.1: bool)) -> [0: bb7, otherwise: bb7];
3838
}
3939

4040
bb7: {
41-
_1 = const 5_i32;
42-
goto -> bb13;
41+
falseEdge -> [real: bb12, imaginary: bb1];
4342
}
4443

4544
bb8: {
46-
falseEdge -> [real: bb9, imaginary: bb7];
45+
_1 = const 5_i32;
46+
goto -> bb15;
4747
}
4848

4949
bb9: {
50-
_1 = const 4_i32;
51-
goto -> bb13;
50+
switchInt(copy (_2.1: bool)) -> [0: bb10, otherwise: bb10];
5251
}
5352

5453
bb10: {
55-
_1 = const 3_i32;
56-
goto -> bb13;
54+
falseEdge -> [real: bb11, imaginary: bb8];
5755
}
5856

5957
bb11: {
60-
_1 = const 2_i32;
61-
goto -> bb13;
58+
_1 = const 4_i32;
59+
goto -> bb15;
6260
}
6361

6462
bb12: {
65-
_1 = const 1_i32;
66-
goto -> bb13;
63+
_1 = const 3_i32;
64+
goto -> bb15;
6765
}
6866

6967
bb13: {
68+
_1 = const 2_i32;
69+
goto -> bb15;
70+
}
71+
72+
bb14: {
73+
_1 = const 1_i32;
74+
goto -> bb15;
75+
}
76+
77+
bb15: {
7078
StorageDead(_2);
7179
StorageDead(_1);
7280
_0 = const ();

0 commit comments

Comments
 (0)