Skip to content

Commit 19c0326

Browse files
committed
Auto merge of rust-lang#129392 - compiler-errors:raw-ref-op-doesnt-diverge-but-more, r=<try>
Do not consider match/let/ref of place that evaluates to `!` to diverge, disallow coercions from them too Fixes rust-lang#117288. This PR does two things: ### Don't consider `match`/`let`/`ref`/assignment-LHS of a place expression that evaluates to `!` to diverge. Which fixes this unsoundness: ``` fn make_up_a_value<T>() -> T { unsafe { let x: *const ! = &0 as *const u8 as *const !; let _ = *x; } } ``` Before this PR, it was UB since we consider the `unsafe` block to diverge which means the outer block evalutes to `!`, even though we've never actually *read* a value of type `!`. ### Do not perform coercions of those same place expressions. Which fixes this inadvertent, sneaky unsoundness: ``` unsafe { let x: *const ! = &0 as *const u8 as *const !; let _: () = *x; } ``` which is UB because currently rust emits an *implicit* NeverToAny coercion even though we really shouldn't be, since there's no read of the value pointed by `x`. --- Detecting both of these situations is implemented in a heuristic function called `expr_consitutes_read`. It is tasked with detecting the situations where we have a place expression being passed to some parent expression that would not constitute a read necessarily, like a `let _ = *never_ptr` where `never_ptr: *const !`. --- Specifically, for `let` and `match`, we don't consider it to be a read unless any of the subpatterns (i.e. the LHS of the `let` or the arms of the match) constitute a read. Almost all patterns constitute a read except for `_`, an `|` pattern, or the currently experimental `!` pattern. --- I'm not totally certain that this deserves an FCP, since it's really a bugfix for UB. If it does, I'd be comfortable with it being a T-types FCP since we should be responsible with determining which coercions in the type system are sound (similar to how we adjusted subtyping behavior in rust-lang#118247 to be more sound).
2 parents 17b322f + a0179f5 commit 19c0326

15 files changed

+615
-32
lines changed

compiler/rustc_hir_typeck/src/coercion.rs

+35-11
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@ struct Coerce<'a, 'tcx> {
8282
/// See #47489 and #48598
8383
/// See docs on the "AllowTwoPhase" type for a more detailed discussion
8484
allow_two_phase: AllowTwoPhase,
85+
/// Whether we allow `NeverToAny` coercions. This is unsound if we're
86+
/// coercing a place expression without it counting as a read in the MIR.
87+
/// This is a side-effect of HIR not really having a great distinction
88+
/// between places and values.
89+
coerce_never: bool,
8590
}
8691

8792
impl<'a, 'tcx> Deref for Coerce<'a, 'tcx> {
@@ -125,8 +130,9 @@ impl<'f, 'tcx> Coerce<'f, 'tcx> {
125130
fcx: &'f FnCtxt<'f, 'tcx>,
126131
cause: ObligationCause<'tcx>,
127132
allow_two_phase: AllowTwoPhase,
133+
coerce_never: bool,
128134
) -> Self {
129-
Coerce { fcx, cause, allow_two_phase, use_lub: false }
135+
Coerce { fcx, cause, allow_two_phase, use_lub: false, coerce_never }
130136
}
131137

132138
fn unify(&self, a: Ty<'tcx>, b: Ty<'tcx>) -> InferResult<'tcx, Ty<'tcx>> {
@@ -177,7 +183,12 @@ impl<'f, 'tcx> Coerce<'f, 'tcx> {
177183

178184
// Coercing from `!` to any type is allowed:
179185
if a.is_never() {
180-
return success(simple(Adjust::NeverToAny)(b), b, vec![]);
186+
if self.coerce_never {
187+
return success(simple(Adjust::NeverToAny)(b), b, vec![]);
188+
} else {
189+
// Otherwise the only coercion we can do is unification.
190+
return self.unify_and(a, b, identity);
191+
}
181192
}
182193

183194
// Coercing *from* an unresolved inference variable means that
@@ -979,7 +990,7 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
979990
/// The expressions *must not* have any preexisting adjustments.
980991
pub(crate) fn coerce(
981992
&self,
982-
expr: &hir::Expr<'_>,
993+
expr: &'tcx hir::Expr<'tcx>,
983994
expr_ty: Ty<'tcx>,
984995
mut target: Ty<'tcx>,
985996
allow_two_phase: AllowTwoPhase,
@@ -996,7 +1007,12 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
9961007

9971008
let cause =
9981009
cause.unwrap_or_else(|| self.cause(expr.span, ObligationCauseCode::ExprAssignable));
999-
let coerce = Coerce::new(self, cause, allow_two_phase);
1010+
let coerce = Coerce::new(
1011+
self,
1012+
cause,
1013+
allow_two_phase,
1014+
self.expr_guaranteed_to_constitute_read_for_never(expr),
1015+
);
10001016
let ok = self.commit_if_ok(|_| coerce.coerce(source, target))?;
10011017

10021018
let (adjustments, _) = self.register_infer_ok_obligations(ok);
@@ -1018,8 +1034,9 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
10181034
debug!("coercion::can_with_predicates({:?} -> {:?})", source, target);
10191035

10201036
let cause = self.cause(DUMMY_SP, ObligationCauseCode::ExprAssignable);
1021-
// We don't ever need two-phase here since we throw out the result of the coercion
1022-
let coerce = Coerce::new(self, cause, AllowTwoPhase::No);
1037+
// We don't ever need two-phase here since we throw out the result of the coercion.
1038+
// We also just always set `coerce_never` to true, since this is a heuristic.
1039+
let coerce = Coerce::new(self, cause, AllowTwoPhase::No, true);
10231040
self.probe(|_| {
10241041
let Ok(ok) = coerce.coerce(source, target) else {
10251042
return false;
@@ -1031,12 +1048,16 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
10311048
}
10321049

10331050
/// Given a type and a target type, this function will calculate and return
1034-
/// how many dereference steps needed to achieve `expr_ty <: target`. If
1051+
/// how many dereference steps needed to coerce `expr_ty` to `target`. If
10351052
/// it's not possible, return `None`.
1036-
pub(crate) fn deref_steps(&self, expr_ty: Ty<'tcx>, target: Ty<'tcx>) -> Option<usize> {
1053+
pub(crate) fn deref_steps_for_suggestion(
1054+
&self,
1055+
expr_ty: Ty<'tcx>,
1056+
target: Ty<'tcx>,
1057+
) -> Option<usize> {
10371058
let cause = self.cause(DUMMY_SP, ObligationCauseCode::ExprAssignable);
1038-
// We don't ever need two-phase here since we throw out the result of the coercion
1039-
let coerce = Coerce::new(self, cause, AllowTwoPhase::No);
1059+
// We don't ever need two-phase here since we throw out the result of the coercion.
1060+
let coerce = Coerce::new(self, cause, AllowTwoPhase::No, true);
10401061
coerce
10411062
.autoderef(DUMMY_SP, expr_ty)
10421063
.find_map(|(ty, steps)| self.probe(|_| coerce.unify(ty, target)).ok().map(|_| steps))
@@ -1193,7 +1214,10 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
11931214
// probably aren't processing function arguments here and even if we were,
11941215
// they're going to get autorefed again anyway and we can apply 2-phase borrows
11951216
// at that time.
1196-
let mut coerce = Coerce::new(self, cause.clone(), AllowTwoPhase::No);
1217+
//
1218+
// NOTE: we set `coerce_never` to `true` here because coercion LUBs only
1219+
// operate on values and not places, so a never coercion is valid.
1220+
let mut coerce = Coerce::new(self, cause.clone(), AllowTwoPhase::No, true);
11971221
coerce.use_lub = true;
11981222

11991223
// First try to coerce the new expression to the type of the previous ones,

compiler/rustc_hir_typeck/src/expr.rs

+187-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
// ignore-tidy-filelength
2+
// FIXME: we should move the field error reporting code somewhere else.
3+
14
//! Type checking expressions.
25
//!
36
//! See [`rustc_hir_analysis::check`] for more context on type checking in general.
@@ -62,7 +65,7 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
6265

6366
// While we don't allow *arbitrary* coercions here, we *do* allow
6467
// coercions from ! to `expected`.
65-
if ty.is_never() {
68+
if ty.is_never() && self.expr_guaranteed_to_constitute_read_for_never(expr) {
6669
if let Some(_) = self.typeck_results.borrow().adjustments().get(expr.hir_id) {
6770
let reported = self.dcx().span_delayed_bug(
6871
expr.span,
@@ -238,8 +241,11 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
238241
_ => self.warn_if_unreachable(expr.hir_id, expr.span, "expression"),
239242
}
240243

241-
// Any expression that produces a value of type `!` must have diverged
242-
if ty.is_never() {
244+
// Any expression that produces a value of type `!` must have diverged,
245+
// unless it's a place expression that isn't being read from, in which case
246+
// diverging would be unsound since we may never actually read the `!`.
247+
// e.g. `let _ = *never_ptr;` with `never_ptr: *const !`.
248+
if ty.is_never() && self.expr_guaranteed_to_constitute_read_for_never(expr) {
243249
self.diverges.set(self.diverges.get() | Diverges::always(expr.span));
244250
}
245251

@@ -257,6 +263,184 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
257263
ty
258264
}
259265

266+
/// Whether this expression constitutes a read of value of the type that
267+
/// it evaluates to.
268+
///
269+
/// This is used to determine if we should consider the block to diverge
270+
/// if the expression evaluates to `!`, and if we should insert a `NeverToAny`
271+
/// coercion for values of type `!`.
272+
///
273+
/// This function generally returns `false` if the expression is a place
274+
/// expression and the *parent* expression is the scrutinee of a match or
275+
/// the pointee of an `&` addr-of expression, since both of those parent
276+
/// expressions take a *place* and not a value.
277+
pub(super) fn expr_guaranteed_to_constitute_read_for_never(
278+
&self,
279+
expr: &'tcx hir::Expr<'tcx>,
280+
) -> bool {
281+
// We only care about place exprs. Anything else returns an immediate
282+
// which would constitute a read. We don't care about distinguishing
283+
// "syntactic" place exprs since if the base of a field projection is
284+
// not a place then it would've been UB to read from it anyways since
285+
// that constitutes a read.
286+
if !expr.is_syntactic_place_expr() {
287+
return true;
288+
}
289+
290+
let parent_node = self.tcx.parent_hir_node(expr.hir_id);
291+
match parent_node {
292+
hir::Node::Expr(parent_expr) => {
293+
match parent_expr.kind {
294+
// Addr-of, field projections, and LHS of assignment don't constitute reads.
295+
// Assignment does call `drop_in_place`, though, but its safety
296+
// requirements are not the same.
297+
ExprKind::AddrOf(..) | hir::ExprKind::Field(..) => false,
298+
ExprKind::Assign(lhs, _, _) => {
299+
// Only the LHS does not constitute a read
300+
expr.hir_id != lhs.hir_id
301+
}
302+
303+
// See note on `PatKind::Or` below for why this is `all`.
304+
ExprKind::Match(scrutinee, arms, _) => {
305+
assert_eq!(scrutinee.hir_id, expr.hir_id);
306+
arms.iter()
307+
.all(|arm| self.pat_guaranteed_to_constitute_read_for_never(arm.pat))
308+
}
309+
ExprKind::Let(hir::LetExpr { init, pat, .. }) => {
310+
assert_eq!(init.hir_id, expr.hir_id);
311+
self.pat_guaranteed_to_constitute_read_for_never(*pat)
312+
}
313+
314+
// Any expression child of these expressions constitute reads.
315+
ExprKind::Array(_)
316+
| ExprKind::Call(_, _)
317+
| ExprKind::MethodCall(_, _, _, _)
318+
| ExprKind::Tup(_)
319+
| ExprKind::Binary(_, _, _)
320+
| ExprKind::Unary(_, _)
321+
| ExprKind::Cast(_, _)
322+
| ExprKind::Type(_, _)
323+
| ExprKind::DropTemps(_)
324+
| ExprKind::If(_, _, _)
325+
| ExprKind::Closure(_)
326+
| ExprKind::Block(_, _)
327+
| ExprKind::AssignOp(_, _, _)
328+
| ExprKind::Index(_, _, _)
329+
| ExprKind::Break(_, _)
330+
| ExprKind::Ret(_)
331+
| ExprKind::Become(_)
332+
| ExprKind::InlineAsm(_)
333+
| ExprKind::Struct(_, _, _)
334+
| ExprKind::Repeat(_, _)
335+
| ExprKind::Yield(_, _) => true,
336+
337+
// These expressions have no (direct) sub-exprs.
338+
ExprKind::ConstBlock(_)
339+
| ExprKind::Loop(_, _, _, _)
340+
| ExprKind::Lit(_)
341+
| ExprKind::Path(_)
342+
| ExprKind::Continue(_)
343+
| ExprKind::OffsetOf(_, _)
344+
| ExprKind::Err(_) => unreachable!("no sub-expr expected for {:?}", expr.kind),
345+
}
346+
}
347+
348+
// If we have a subpattern that performs a read, we want to consider this
349+
// to diverge for compatibility to support something like `let x: () = *never_ptr;`.
350+
hir::Node::LetStmt(hir::LetStmt { init: Some(target), pat, .. }) => {
351+
assert_eq!(target.hir_id, expr.hir_id);
352+
self.pat_guaranteed_to_constitute_read_for_never(*pat)
353+
}
354+
355+
// These nodes (if they have a sub-expr) do constitute a read.
356+
hir::Node::Block(_)
357+
| hir::Node::Arm(_)
358+
| hir::Node::ExprField(_)
359+
| hir::Node::AnonConst(_)
360+
| hir::Node::ConstBlock(_)
361+
| hir::Node::ConstArg(_)
362+
| hir::Node::Stmt(_)
363+
| hir::Node::Item(hir::Item {
364+
kind: hir::ItemKind::Const(..) | hir::ItemKind::Static(..),
365+
..
366+
})
367+
| hir::Node::TraitItem(hir::TraitItem {
368+
kind: hir::TraitItemKind::Const(..), ..
369+
})
370+
| hir::Node::ImplItem(hir::ImplItem { kind: hir::ImplItemKind::Const(..), .. }) => true,
371+
372+
// These nodes do not have direct sub-exprs.
373+
hir::Node::Param(_)
374+
| hir::Node::Item(_)
375+
| hir::Node::ForeignItem(_)
376+
| hir::Node::TraitItem(_)
377+
| hir::Node::ImplItem(_)
378+
| hir::Node::Variant(_)
379+
| hir::Node::Field(_)
380+
| hir::Node::PathSegment(_)
381+
| hir::Node::Ty(_)
382+
| hir::Node::AssocItemConstraint(_)
383+
| hir::Node::TraitRef(_)
384+
| hir::Node::Pat(_)
385+
| hir::Node::PatField(_)
386+
| hir::Node::LetStmt(_)
387+
| hir::Node::Synthetic
388+
| hir::Node::Err(_)
389+
| hir::Node::Ctor(_)
390+
| hir::Node::Lifetime(_)
391+
| hir::Node::GenericParam(_)
392+
| hir::Node::Crate(_)
393+
| hir::Node::Infer(_)
394+
| hir::Node::WhereBoundPredicate(_)
395+
| hir::Node::ArrayLenInfer(_)
396+
| hir::Node::PreciseCapturingNonLifetimeArg(_) => {
397+
unreachable!("no sub-expr expected for {parent_node:?}")
398+
}
399+
}
400+
}
401+
402+
/// Whether this pattern constitutes a read of value of the scrutinee that
403+
/// it is matching against. This is used to determine whether we should
404+
/// perform `NeverToAny` coercions
405+
///
406+
/// See above for the nuances of what happens when this returns true.
407+
pub(super) fn pat_guaranteed_to_constitute_read_for_never(&self, pat: &hir::Pat<'_>) -> bool {
408+
match pat.kind {
409+
// Does not constitute a read.
410+
hir::PatKind::Wild => false,
411+
412+
// This is unnecessarily restrictive when the pattern that doesn't
413+
// constitute a read is unreachable.
414+
//
415+
// For example `match *never_ptr { value => {}, _ => {} }` or
416+
// `match *never_ptr { _ if false => {}, value => {} }`.
417+
//
418+
// It is however fine to be restrictive here; only returning `true`
419+
// can lead to unsoundness.
420+
hir::PatKind::Or(subpats) => {
421+
subpats.iter().all(|pat| self.pat_guaranteed_to_constitute_read_for_never(pat))
422+
}
423+
424+
// Does constitute a read, since it is equivalent to a discriminant read.
425+
hir::PatKind::Never => true,
426+
427+
// All of these constitute a read, or match on something that isn't `!`,
428+
// which would require a `NeverToAny` coercion.
429+
hir::PatKind::Binding(_, _, _, _)
430+
| hir::PatKind::Struct(_, _, _)
431+
| hir::PatKind::TupleStruct(_, _, _)
432+
| hir::PatKind::Path(_)
433+
| hir::PatKind::Tuple(_, _)
434+
| hir::PatKind::Box(_)
435+
| hir::PatKind::Ref(_, _)
436+
| hir::PatKind::Deref(_)
437+
| hir::PatKind::Lit(_)
438+
| hir::PatKind::Range(_, _, _)
439+
| hir::PatKind::Slice(_, _, _)
440+
| hir::PatKind::Err(_) => true,
441+
}
442+
}
443+
260444
#[instrument(skip(self, expr), level = "debug")]
261445
fn check_expr_kind(
262446
&self,

compiler/rustc_hir_typeck/src/fn_ctxt/suggestions.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -2585,7 +2585,7 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
25852585
}
25862586

25872587
if let hir::ExprKind::Unary(hir::UnOp::Deref, inner) = expr.kind
2588-
&& let Some(1) = self.deref_steps(expected, checked_ty)
2588+
&& let Some(1) = self.deref_steps_for_suggestion(expected, checked_ty)
25892589
{
25902590
// We have `*&T`, check if what was expected was `&T`.
25912591
// If so, we may want to suggest removing a `*`.
@@ -2715,7 +2715,7 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
27152715
}
27162716
}
27172717
(_, &ty::RawPtr(ty_b, mutbl_b), &ty::Ref(_, ty_a, mutbl_a)) => {
2718-
if let Some(steps) = self.deref_steps(ty_a, ty_b)
2718+
if let Some(steps) = self.deref_steps_for_suggestion(ty_a, ty_b)
27192719
// Only suggest valid if dereferencing needed.
27202720
&& steps > 0
27212721
// The pointer type implements `Copy` trait so the suggestion is always valid.
@@ -2759,7 +2759,7 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
27592759
}
27602760
}
27612761
_ if sp == expr.span => {
2762-
if let Some(mut steps) = self.deref_steps(checked_ty, expected) {
2762+
if let Some(mut steps) = self.deref_steps_for_suggestion(checked_ty, expected) {
27632763
let mut expr = expr.peel_blocks();
27642764
let mut prefix_span = expr.span.shrink_to_lo();
27652765
let mut remove = String::new();

0 commit comments

Comments
 (0)