Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simple validation for unsize coercion in MIR validation #130735

Merged
merged 2 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion compiler/rustc_codegen_cranelift/src/unsize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,22 @@ pub(crate) fn unsized_info<'tcx>(
let old_info =
old_info.expect("unsized_info: missing old info for trait upcasting coercion");
if data_a.principal_def_id() == data_b.principal_def_id() {
// A NOP cast that doesn't actually change anything, should be allowed even with invalid vtables.
// Codegen takes advantage of the additional assumption, where if the
// principal trait def id of what's being casted doesn't change,
// then we don't need to adjust the vtable at all. This
// corresponds to the fact that `dyn Tr<A>: Unsize<dyn Tr<B>>`
// requires that `A = B`; we don't allow *upcasting* objects
// between the same trait with different args. If we, for
// some reason, were to relax the `Unsize` trait, it could become
// unsound, so let's assert here that the trait refs are *equal*.
//
// We can use `assert_eq` because the binders should have been anonymized,
// and because higher-ranked equality now requires the binders are equal.
debug_assert_eq!(
data_a.principal(),
data_b.principal(),
"NOP unsize vtable changed principal trait ref: {data_a} -> {data_b}"
);
return old_info;
}

Expand Down
24 changes: 22 additions & 2 deletions compiler/rustc_codegen_ssa/src/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,28 @@ fn unsized_info<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>>(
let old_info =
old_info.expect("unsized_info: missing old info for trait upcasting coercion");
if data_a.principal_def_id() == data_b.principal_def_id() {
// A NOP cast that doesn't actually change anything, should be allowed even with
// invalid vtables.
// Codegen takes advantage of the additional assumption, where if the
// principal trait def id of what's being casted doesn't change,
// then we don't need to adjust the vtable at all. This
// corresponds to the fact that `dyn Tr<A>: Unsize<dyn Tr<B>>`
// requires that `A = B`; we don't allow *upcasting* objects
// between the same trait with different args. If we, for
// some reason, were to relax the `Unsize` trait, it could become
// unsound, so let's assert here that the trait refs are *equal*.
//
// We can use `assert_eq` because the binders should have been anonymized,
// and because higher-ranked equality now requires the binders are equal.
debug_assert_eq!(
data_a.principal(),
data_b.principal(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That doesn't check that the projections are equal though, does it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No but there is not an upcast for which this is valid. That would literally be unsound in the type system.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like, checking projection validity seems more appropriate as an assertion on the Unsize trait goal in the trait system or something, but even that seems excessive.

Like, the only reason miri needs to check projections are compatible is because of transmutes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And also because I don't trust the library/ and would prefer this to be double-checked in the compiler. ;)

But maybe that's overly paranoid, and anyway it's a different PR. I just wanted to understand what is and is not being checked here, and the comments should reflect that.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And also because I don't trust the library/ and would prefer this to be double-checked in the compiler. ;)

It's not really library/, it's just the soundness of the Unsize goal, and the fact that you can't impl impl Foo for Bar { type Assoc = A; } and impl Foo for Bar { type Assoc = B; }. That is, normalization is a function.

"NOP unsize vtable changed principal trait ref: {data_a} -> {data_b}"
);

// A NOP cast that doesn't actually change anything, let's avoid any
// unnecessary work. This relies on the assumption that if the principal
// traits are equal, then the associated type bounds (`dyn Trait<Assoc=T>`)
// are also equal, which is ensured by the fact that normalization is
// a function and we do not allow overlapping impls.
return old_info;
}

Expand Down
35 changes: 32 additions & 3 deletions compiler/rustc_mir_transform/src/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ use rustc_data_structures::fx::{FxHashMap, FxHashSet};
use rustc_hir::LangItem;
use rustc_index::IndexVec;
use rustc_index::bit_set::BitSet;
use rustc_infer::traits::Reveal;
use rustc_infer::infer::TyCtxtInferExt;
use rustc_infer::traits::{Obligation, ObligationCause, Reveal};
use rustc_middle::mir::coverage::CoverageKind;
use rustc_middle::mir::visit::{NonUseContext, PlaceContext, Visitor};
use rustc_middle::mir::*;
Expand All @@ -16,6 +17,8 @@ use rustc_middle::ty::{
use rustc_middle::{bug, span_bug};
use rustc_target::abi::{FIRST_VARIANT, Size};
use rustc_target::spec::abi::Abi;
use rustc_trait_selection::traits::ObligationCtxt;
use rustc_type_ir::Upcast;

use crate::util::{is_within_packed, relate_types};

Expand Down Expand Up @@ -586,6 +589,22 @@ impl<'a, 'tcx> TypeChecker<'a, 'tcx> {

crate::util::relate_types(self.tcx, self.param_env, variance, src, dest)
}

/// Check that the given predicate definitely holds in the param-env of this MIR body.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we already have a "test this predicate holds" function somewhere in the validator? I couldn't find one when doing a simple search.

fn predicate_must_hold_modulo_regions(
&self,
pred: impl Upcast<TyCtxt<'tcx>, ty::Predicate<'tcx>>,
) -> bool {
let infcx = self.tcx.infer_ctxt().build();
let ocx = ObligationCtxt::new(&infcx);
ocx.register_obligation(Obligation::new(
self.tcx,
ObligationCause::dummy(),
self.param_env,
pred,
));
ocx.select_all_or_error().is_empty()
}
}

impl<'a, 'tcx> Visitor<'tcx> for TypeChecker<'a, 'tcx> {
Expand Down Expand Up @@ -1202,8 +1221,18 @@ impl<'a, 'tcx> Visitor<'tcx> for TypeChecker<'a, 'tcx> {
}
}
CastKind::PointerCoercion(PointerCoercion::Unsize, _) => {
// This is used for all `CoerceUnsized` types,
// not just pointers/references, so is hard to check.
// Pointers being unsize coerced should at least implement
// `CoerceUnsized`.
if !self.predicate_must_hold_modulo_regions(ty::TraitRef::new(
self.tcx,
self.tcx.require_lang_item(
LangItem::CoerceUnsized,
Some(self.body.source_info(location).span),
),
[op_ty, *target_type],
)) {
self.fail(location, format!("Unsize coercion, but `{op_ty}` isn't coercible to `{target_type}`"));
}
}
CastKind::PointerCoercion(PointerCoercion::DynStar, _) => {
// FIXME(dyn-star): make sure nothing needs to be done here.
Expand Down
26 changes: 0 additions & 26 deletions tests/crashes/129219.rs

This file was deleted.

33 changes: 33 additions & 0 deletions tests/ui/mir/validate/validate-unsize-cast.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//@ compile-flags: -Zmir-opt-level=0 -Zmir-enable-passes=+Inline,+GVN -Zvalidate-mir

#![feature(unsize)]

use std::marker::Unsize;

pub trait CastTo<U: ?Sized>: Unsize<U> {}

// Not well-formed!
impl<T: ?Sized, U: ?Sized> CastTo<U> for T {}
//~^ ERROR the trait bound `T: Unsize<U>` is not satisfied

pub trait Cast {
fn cast<U: ?Sized>(&self)
where
Self: CastTo<U>;
}
impl<T: ?Sized> Cast for T {
#[inline(always)]
fn cast<U: ?Sized>(&self)
where
Self: CastTo<U>,
{
let x: &U = self;
}
}

fn main() {
// When we inline this call, then we run GVN, then
// GVN tries to evaluate the `() -> [i32]` unsize.
// That's invalid!
().cast::<[i32]>();
}
20 changes: 20 additions & 0 deletions tests/ui/mir/validate/validate-unsize-cast.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
error[E0277]: the trait bound `T: Unsize<U>` is not satisfied
--> $DIR/validate-unsize-cast.rs:10:42
|
LL | impl<T: ?Sized, U: ?Sized> CastTo<U> for T {}
| ^ the trait `Unsize<U>` is not implemented for `T`
|
= note: all implementations of `Unsize` are provided automatically by the compiler, see <https://doc.rust-lang.org/stable/std/marker/trait.Unsize.html> for more information
note: required by a bound in `CastTo`
--> $DIR/validate-unsize-cast.rs:7:30
|
LL | pub trait CastTo<U: ?Sized>: Unsize<U> {}
| ^^^^^^^^^ required by this bound in `CastTo`
help: consider further restricting this bound
|
LL | impl<T: ?Sized + std::marker::Unsize<U>, U: ?Sized> CastTo<U> for T {}
| ++++++++++++++++++++++++

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0277`.
Loading