Skip to content

Commit 4a9d536

Browse files
committed
Auto merge of rust-lang#156977 - RalfJung:interpret-opsem-inhabited, r=WaffleLapkin
interpret: properly check for inhabitedness of nested references This implements the opsem from the ongoing FCP in rust-lang/unsafe-code-guidelines#413. The bit we were previously missing is that transmuting a `&&!` into existence was not caught as being immediate UB -- only the `&!` case behaved as expected. I did not adjust the layout computation because when we compute the layout of `&T`, we cannot know the layout of `T` (as that might be recursive). r? @oli-obk
2 parents 3d50c25 + d65ad07 commit 4a9d536

16 files changed

Lines changed: 307 additions & 49 deletions

File tree

compiler/rustc_const_eval/src/interpret/validity.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ use super::{
3535
format_interp_error,
3636
};
3737
use crate::enter_trace_span;
38+
use crate::interpret::ensure_monomorphic_enough;
3839

3940
// for the validation errors
4041
#[rustfmt::skip]
@@ -686,11 +687,11 @@ impl<'rt, 'tcx, M: Machine<'tcx>> ValidityVisitor<'rt, 'tcx, M> {
686687
)
687688
}
688689
// Do not allow references to uninhabited types.
689-
if place.layout.is_uninhabited() {
690+
if !place.layout.ty.is_opsem_inhabited(*self.ecx.tcx, self.ecx.typing_env) {
690691
let ty = place.layout.ty;
691692
throw_validation_failure!(
692693
self.path,
693-
format!("encountered a {ptr_kind} pointing to uninhabited type {ty}")
694+
format!("encountered a {ptr_kind} pointing to uninhabited type `{ty}`")
694695
)
695696
}
696697

@@ -1524,8 +1525,9 @@ impl<'rt, 'tcx, M: Machine<'tcx>> ValueVisitor<'tcx, M> for ValidityVisitor<'rt,
15241525
}
15251526

15261527
// Assert that we checked everything there is to check about this type.
1528+
// `is_opsem_inhabited` implies that the layout is inhabited (checked by layout invariants).
15271529
assert!(
1528-
!val.layout.is_uninhabited(),
1530+
val.layout.ty.is_opsem_inhabited(*self.ecx.tcx, self.ecx.typing_env),
15291531
"a value of type `{}` passed validation but that type is uninhabited",
15301532
val.layout.ty
15311533
);
@@ -1583,6 +1585,9 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
15831585
) -> InterpResult<'tcx> {
15841586
trace!("validate_operand_internal: {:?}, {:?}", *val, val.layout.ty);
15851587

1588+
// We can't check validity if there are any generics left.
1589+
ensure_monomorphic_enough(*self.tcx, val.layout.ty)?;
1590+
15861591
// Run the visitor.
15871592
self.run_for_validation_mut(|ecx| {
15881593
let reset_padding = reset_provenance_and_padding && {

compiler/rustc_middle/src/queries.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2179,6 +2179,11 @@ rustc_queries! {
21792179
desc { "computing the uninhabited predicate of `{}`", key }
21802180
}
21812181

2182+
/// Do not call this query directly: invoke `Ty::is_opsem_inhabited` instead.
2183+
query is_opsem_inhabited_raw(env: ty::PseudoCanonicalInput<'tcx, Ty<'tcx>>) -> bool {
2184+
desc { "computing whether `{}` is inhabited on the opsem level", env.value }
2185+
}
2186+
21822187
query crate_dep_kind(_: CrateNum) -> CrateDepKind {
21832188
eval_always
21842189
desc { "fetching what a dependency looks like" }

compiler/rustc_middle/src/ty/inhabitedness/mod.rs

Lines changed: 182 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@
4343
//! This code should only compile in modules where the uninhabitedness of `Foo`
4444
//! is visible.
4545
46+
use std::assert_matches;
47+
48+
use rustc_data_structures::fx::FxHashSet;
4649
use rustc_span::def_id::LocalModId;
4750
use rustc_type_ir::TyKind::*;
4851
use tracing::instrument;
@@ -55,7 +58,12 @@ pub mod inhabited_predicate;
5558
pub use inhabited_predicate::InhabitedPredicate;
5659

5760
pub(crate) fn provide(providers: &mut Providers) {
58-
*providers = Providers { inhabited_predicate_adt, inhabited_predicate_type, ..*providers };
61+
*providers = Providers {
62+
inhabited_predicate_adt,
63+
inhabited_predicate_type,
64+
is_opsem_inhabited_raw,
65+
..*providers
66+
};
5967
}
6068

6169
/// Returns an `InhabitedPredicate` that is generic over type parameters and
@@ -191,14 +199,33 @@ impl<'tcx> Ty<'tcx> {
191199
self.inhabited_predicate(tcx).apply(tcx, typing_env, module)
192200
}
193201

194-
/// Returns true if the type is uninhabited without regard to visibility
202+
/// Returns true if the type is uninhabited without regard to visibility.
203+
///
204+
/// This is still conservative; for instance, a `#[non_exhaustive]` enum *in another crate*
205+
/// is always considered inhabited.
195206
pub fn is_privately_uninhabited(
196207
self,
197208
tcx: TyCtxt<'tcx>,
198209
typing_env: ty::TypingEnv<'tcx>,
199210
) -> bool {
200211
!self.inhabited_predicate(tcx).apply_ignore_module(tcx, typing_env)
201212
}
213+
214+
/// Returns whether `self` is considered inhabited on the opsem level, i.e., its validity
215+
/// invariant might be satisfiable. `self` is expected to be monomorphic and normalized.
216+
///
217+
/// Key constraints are:
218+
/// - if a type's validity invariant is satisfiable, it must be opsem-inhabited.
219+
/// - if a type's layout is marked uninhabited, it must be opsem-uninhabited.
220+
///
221+
/// Beyond that, the value returned by this function is not a stable guarantee.
222+
pub fn is_opsem_inhabited(self, tcx: TyCtxt<'tcx>, typing_env: ty::TypingEnv<'tcx>) -> bool {
223+
// Handle simple cases directly, use the query with its cache for the rest.
224+
is_opsem_inhabited_recursor(self, tcx, &mut (), /* stop_at_ref */ false, &|ty, _, _| {
225+
// ADT handler: stop recursing, invoke the query.
226+
tcx.is_opsem_inhabited_raw(typing_env.as_query_input(ty))
227+
})
228+
}
202229
}
203230

204231
/// N.B. this query should only be called through `Ty::inhabited_predicate`
@@ -221,3 +248,156 @@ fn inhabited_predicate_type<'tcx>(tcx: TyCtxt<'tcx>, ty: Ty<'tcx>) -> InhabitedP
221248
_ => bug!("unexpected TyKind, use `Ty::inhabited_predicate`"),
222249
}
223250
}
251+
252+
/// Recurse over a type to determine whether it is inhabited on the opsem level.
253+
/// See `is_opsem_inhabited` above for the spec of what we compute.
254+
///
255+
/// When we encounter an ADT, we call `adt_handler`, giving it as its last argument a closure that
256+
/// it can invoke to continue the recursion. This lets us share the logic for "simple" cases
257+
/// (i.e., everything except for ADTs) between `Ty::is_opsem_inhabited` and the query.
258+
///
259+
/// `seen` is used to detect infinite recursion: the set contains all ADTs that we encountered
260+
/// on our path to the current type.
261+
/// If `stop_at_ref` is true, we stop recursing at the next reference we encounter.
262+
fn is_opsem_inhabited_recursor<'tcx, SEEN>(
263+
ty: Ty<'tcx>,
264+
tcx: TyCtxt<'tcx>,
265+
seen: &mut SEEN,
266+
stop_at_ref: bool,
267+
adt_handler: &impl Fn(
268+
Ty<'tcx>,
269+
&mut SEEN,
270+
&dyn Fn(Ty<'tcx>, &mut SEEN, /* stop_at_ref */ bool) -> bool,
271+
) -> bool,
272+
) -> bool {
273+
match *ty.kind() {
274+
// Trivially (un)inhabited types
275+
ty::Int(_)
276+
| ty::Uint(_)
277+
| ty::Float(_)
278+
| ty::Bool
279+
| ty::Char
280+
| ty::Str
281+
| ty::Foreign(..)
282+
| ty::RawPtr(..)
283+
| ty::FnPtr(..)
284+
| ty::FnDef(..) => true,
285+
ty::Dynamic(..) => true, // We can't reason about traits, assume they are inhabited
286+
ty::Slice(..) => true, // Slices can always be empty
287+
ty::Never => false,
288+
289+
// Types where we recurse
290+
ty::Ref(_, pointee, _) => {
291+
if stop_at_ref {
292+
// Bailing out here is safe as the layout code always considers references
293+
// inhabited, so the implication ("layout uninhabited => opsem uninhabited")
294+
// is upheld.
295+
return true;
296+
}
297+
is_opsem_inhabited_recursor(pointee, tcx, seen, stop_at_ref, adt_handler)
298+
}
299+
ty::Tuple(tys) => tys
300+
.iter()
301+
.all(|ty| is_opsem_inhabited_recursor(ty, tcx, seen, stop_at_ref, adt_handler)),
302+
ty::Array(elem, len) => {
303+
len.try_to_target_usize(tcx).unwrap() == 0
304+
|| is_opsem_inhabited_recursor(elem, tcx, seen, stop_at_ref, adt_handler)
305+
}
306+
ty::Pat(inner, _pat) => {
307+
is_opsem_inhabited_recursor(inner, tcx, seen, stop_at_ref, adt_handler)
308+
}
309+
ty::Closure(_def, args) => {
310+
let args = args.as_closure();
311+
args.upvar_tys()
312+
.iter()
313+
.all(|ty| is_opsem_inhabited_recursor(ty, tcx, seen, stop_at_ref, adt_handler))
314+
}
315+
ty::Coroutine(_def, args) => {
316+
let args = args.as_coroutine();
317+
args.upvar_tys()
318+
.iter()
319+
.all(|ty| is_opsem_inhabited_recursor(ty, tcx, seen, stop_at_ref, adt_handler))
320+
}
321+
ty::CoroutineClosure(_def, args) => {
322+
let args = args.as_coroutine_closure();
323+
args.upvar_tys()
324+
.iter()
325+
.all(|ty| is_opsem_inhabited_recursor(ty, tcx, seen, stop_at_ref, adt_handler))
326+
}
327+
ty::UnsafeBinder(base) => {
328+
let base = tcx.instantiate_bound_regions_with_erased((*base).into());
329+
is_opsem_inhabited_recursor(base, tcx, seen, stop_at_ref, adt_handler)
330+
}
331+
ty::Adt(..) => {
332+
// ADTs need a special handler to avoid infinite recursion. That handler is meant to
333+
// call back into the recursor. Ideally it'd just call `is_opsem_inhabited_recursor` but
334+
// then it would have to pass itself as the adt_handler argument which is not possible
335+
// in Rust... so we provide the handler with a callback that it can use to continue the
336+
// recursion with the same `adt_handler`.
337+
adt_handler(ty, seen, &|ty, seen, stop_at_ref| {
338+
is_opsem_inhabited_recursor(ty, tcx, seen, stop_at_ref, adt_handler)
339+
})
340+
}
341+
342+
ty::Error(_)
343+
| ty::Infer(..)
344+
| ty::Placeholder(..)
345+
| ty::Bound(..)
346+
| ty::Param(..)
347+
| ty::Alias(..)
348+
| ty::CoroutineWitness(..) => {
349+
bug!("non-normalized type in `is_opsem_uninhabited`: `{ty}`")
350+
}
351+
}
352+
}
353+
354+
fn is_opsem_inhabited_raw<'tcx>(
355+
tcx: TyCtxt<'tcx>,
356+
env: ty::PseudoCanonicalInput<'tcx, Ty<'tcx>>,
357+
) -> bool {
358+
let (ty, typing_env) = (env.value, env.typing_env);
359+
assert_matches!(
360+
ty.kind(),
361+
ty::Adt(..),
362+
"the query should only be invoked by `Ty::is_opsem_inhabited`"
363+
);
364+
365+
is_opsem_inhabited_recursor(
366+
ty,
367+
tcx,
368+
&mut FxHashSet::<DefId>::default(),
369+
/* stop_at_ref */ false,
370+
&|ty, seen, rec| {
371+
let ty::Adt(adt_def, adt_args) = *ty.kind() else {
372+
unreachable! {}
373+
};
374+
if adt_def.is_union() {
375+
// Unions are always inhabited.
376+
return true;
377+
}
378+
379+
let new_adt = seen.insert(adt_def.did());
380+
// If we have seen this ADT before, stop at the next reference to avoid infinite
381+
// recursion. We can't stop here since we have to ensure that "layout uninhabited"
382+
// implies "opsem uninhabited". References are always layout-inhabited so the
383+
// implication is vacuously true.
384+
let stop_at_ref = !new_adt;
385+
386+
// We are inhabited if in some variant all fields are inhabited.
387+
let inhabited = adt_def.variants().iter().any(|variant| {
388+
variant.fields.iter().all(|field| {
389+
let ty = field.ty(tcx, adt_args);
390+
let ty = tcx.normalize_erasing_regions(typing_env, ty);
391+
rec(ty, seen, stop_at_ref)
392+
})
393+
});
394+
395+
// Remove the type again so that we allow it to appear on other branches.
396+
if new_adt {
397+
seen.remove(&adt_def.did());
398+
}
399+
400+
inhabited
401+
},
402+
)
403+
}

compiler/rustc_ty_utils/src/layout/invariant.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::assert_matches;
22

33
use rustc_abi::{BackendRepr, FieldsShape, Scalar, Size, TagEncoding, Variants};
4+
use rustc_middle::ty::TypeVisitableExt;
45
use rustc_middle::ty::layout::{HasTyCtxt, LayoutCx, TyAndLayout};
56
use rustc_middle::{bug, ty};
67

@@ -34,6 +35,15 @@ pub(super) fn layout_sanity_check<'tcx>(cx: &LayoutCx<'tcx>, layout: &TyAndLayou
3435
layout.ty
3536
);
3637
}
38+
// ABI uninhabitedness should imply opsem uninhabitedness. However, we can only check that if
39+
// the type is really monomorphic (while we can compute a layout for some generic types).
40+
if layout.is_uninhabited() && !layout.ty.has_param() {
41+
assert!(
42+
!layout.ty.is_opsem_inhabited(tcx, cx.typing_env),
43+
"{:?} is ABI-uninhabited but not opsem-uninhabited?",
44+
layout.ty
45+
);
46+
}
3747

3848
/// Yields non-ZST fields of the type
3949
fn non_zst_fields<'tcx, 'a>(

src/tools/miri/tests/fail/validity/ref_to_uninhabited1.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::mem::{forget, transmute};
33

44
fn main() {
55
unsafe {
6-
let x: Box<!> = transmute(&mut 42); //~ERROR: encountered a box pointing to uninhabited type !
6+
let x: Box<!> = transmute(&mut 42); //~ERROR: encountered a box pointing to uninhabited type `!`
77
forget(x);
88
}
99
}

src/tools/miri/tests/fail/validity/ref_to_uninhabited1.stderr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
error: Undefined Behavior: constructing invalid value of type std::boxed::Box<!>: encountered a box pointing to uninhabited type !
1+
error: Undefined Behavior: constructing invalid value of type std::boxed::Box<!>: encountered a box pointing to uninhabited type `!`
22
--> tests/fail/validity/ref_to_uninhabited1.rs:LL:CC
33
|
44
LL | let x: Box<!> = transmute(&mut 42);

src/tools/miri/tests/fail/validity/ref_to_uninhabited2.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ enum Void {}
44

55
fn main() {
66
unsafe {
7-
let _x: &(i32, Void) = transmute(&42); //~ERROR: encountered a reference pointing to uninhabited type (i32, Void)
7+
let _x: &&(i32, Void) = transmute(&&42); //~ERROR: encountered a reference pointing to uninhabited type `&(i32, Void)`
88
}
99
}

src/tools/miri/tests/fail/validity/ref_to_uninhabited2.stderr

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
error: Undefined Behavior: constructing invalid value of type &(i32, Void): encountered a reference pointing to uninhabited type (i32, Void)
1+
error: Undefined Behavior: constructing invalid value of type &&(i32, Void): encountered a reference pointing to uninhabited type `&(i32, Void)`
22
--> tests/fail/validity/ref_to_uninhabited2.rs:LL:CC
33
|
4-
LL | let _x: &(i32, Void) = transmute(&42);
5-
| ^^^^^^^^^^^^^^ Undefined Behavior occurred here
4+
LL | let _x: &&(i32, Void) = transmute(&&42);
5+
| ^^^^^^^^^^^^^^^ Undefined Behavior occurred here
66
|
77
= help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
88
= help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information

tests/crashes/150296.rs

Lines changed: 0 additions & 14 deletions
This file was deleted.

tests/ui/consts/const-eval/raw-bytes.32bit.stderr

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ LL | const DATA_FN_PTR: fn() = unsafe { mem::transmute(&13) };
218218
╾ALLOC$ID╼ │ ╾──╼
219219
}
220220

221-
error[E0080]: constructing invalid value of type &Bar: encountered a reference pointing to uninhabited type Bar
221+
error[E0080]: constructing invalid value of type &Bar: encountered a reference pointing to uninhabited type `Bar`
222222
--> $DIR/raw-bytes.rs:109:1
223223
|
224224
LL | const BAD_BAD_REF: &Bar = unsafe { mem::transmute(1usize) };
@@ -458,7 +458,7 @@ LL | const RAW_TRAIT_OBJ_VTABLE_INVALID: *const dyn Trait = unsafe { mem::transm
458458
╾ALLOC$ID╼ ╾ALLOC$ID╼ │ ╾──╼╾──╼
459459
}
460460

461-
error[E0080]: constructing invalid value of type &[!; 1]: encountered a reference pointing to uninhabited type [!; 1]
461+
error[E0080]: constructing invalid value of type &[!; 1]: encountered a reference pointing to uninhabited type `[!; 1]`
462462
--> $DIR/raw-bytes.rs:187:1
463463
|
464464
LL | const _: &[!; 1] = unsafe { &*(1_usize as *const [!; 1]) };

0 commit comments

Comments
 (0)