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

Trait predicate evaluation cache incorrectly handles EvaluatedToOk #80691

Closed
Aaron1011 opened this issue Jan 4, 2021 · 5 comments · Fixed by #83220
Closed

Trait predicate evaluation cache incorrectly handles EvaluatedToOk #80691

Aaron1011 opened this issue Jan 4, 2021 · 5 comments · Fixed by #83220
Labels
A-trait-system Area: Trait system C-bug Category: This is a bug. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.

Comments

@Aaron1011
Copy link
Member

Split out from the discussion in #80336. See #80336 (comment) for the original comments.


The issue is caused by the way that we handle regions in the evaluation cache. When we insert a result into the evaluation cache (either infcx.evaluation_cache or tcx.evaluation_cache, we use a 'freshened' version of the original TraitPredicate as the key. The 'freshened' TraitPredicate has all non-late-bound regions erased.

Unfortunately, this can lead to issues if we try to evaluate the following two predicates:

impl SomeTrait for SomeType<'static>

SomeType<'static> as SomeTrait
SomeType<'#_r> as SomeTrait

When we evaluate SomeType<'static> as SomeTrait, we'll get EvaluatedToOk, since the region parameter in our trait ref is known to match the impl. We will then cache the result as <SomeType<'erased> as SomeTrait> -> EvaluatedToOk.

If we later try to evaluate SomeType<'#_r> as SomeTrait, we will end up matching the evaluation cache entry, giving us a result of EvaluatedToOk. However, we should have gotten a result of EvaluatedToOkModuloRegions, since we don't know that '#_r == 'static holds.

This is really difficult to observe in practice, for a number of reasons:

  • The relevant trait predicates need to get evaluated, not just registered in a FulfillmentContext.
  • Trait evaluation usually goes through the evaluate_obligation query, which canonicalizes the regions in the input trait ref. To end up trying to evaluate SomeType<'static> as SomeTrait, we need to end up calling evaluate_predicate_recursively on it, with a different original trait ref used in the original query.
  • Using EvaluatedToOkModuloRegions instead of EvaluatedToOk only seems to cause an error when it results in the incremental hash changing (I don't know if it's possible to weaponize this into extending a lifetime).

As a result, I haven't been able to minimize this.

@Aaron1011 Aaron1011 added A-trait-system Area: Trait system C-bug Category: This is a bug. labels Jan 4, 2021
@jonas-schievink jonas-schievink added the T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. label Jan 4, 2021
@Aaron1011
Copy link
Member Author

With #83007 landing, this is going to start causing ICEs (since the query result is actually changing across compilatioon sessions). I'm still not sure if this can actually cause unsoundness, but it should be fixed anyway, since it's a bug in a pretty subtle part of the trait system.

cc @eddyb @matthewjasper - I'm not familiar enough with the relevant code to be sure of how to fix this.

@Aaron1011
Copy link
Member Author

This can be reproduced by following the instructions in #82920 after #83007 lands - it turns out that that reproducer also hits this issue. Unfortunately, that reproducer is quite large, and I'm not sure how to minimize it.

@eddyb
Copy link
Member

eddyb commented Mar 15, 2021

I wouldn't know where to look (or rather, I only know the vague shape of all of this, but not enough details) and would defer to @nikomatsakis or @matthewjasper, myself.

The only thing I can think of is we should blanket use EvaluatedToOkModuloRegions any time freshening erased a lifetime.

@KalitaAlexey
Copy link
Contributor

@Aaron1011
Copy link
Member Author

@lqd managed to create a minimized reproducer in #83115 (comment):

pub trait Interner {
    type InternedVariableKinds;
}

trait RustIrDatabase<I: Interner> {
    fn associated_ty_data(&self) -> AssociatedTyDatum<I>;
    fn impl_datum(&self) -> ImplDatum<I>;
}

trait Fold<I: Interner> {
    type Result;
}
impl<T, I: Interner> Fold<I> for Binders<T>
where
    T: HasInterner<Interner = I> + Fold<I>,
    <T as Fold<I>>::Result: HasInterner<Interner = I>,
    I: Interner,
{
    type Result = Binders<T::Result>;
}
impl<I: Interner> Fold<I> for WhereClause<I> {
    type Result = Binders<WhereClause<I>>;
}

trait HasInterner {
    type Interner: Interner;
}
impl<T: HasInterner> HasInterner for Vec<T> {
    type Interner = T::Interner;
}
impl<T: HasInterner + ?Sized> HasInterner for &T {
    type Interner = T::Interner;
}

pub struct VariableKind<I: Interner> {
    _marker: std::marker::PhantomData<I>,
}

struct VariableKinds<I: Interner> {
    _interned: I::InternedVariableKinds,
}

struct WhereClause<I: Interner> {
    _marker: std::marker::PhantomData<I>,
}
impl<I: Interner> HasInterner for WhereClause<I> {
    type Interner = I;
}

struct Binders<T> {
    _marker: std::marker::PhantomData<T>,
}
impl<T: HasInterner> HasInterner for Binders<T> {
    type Interner = T::Interner;
}
impl<T> Binders<&T> {
    fn cloned(self) -> Binders<T> {
        unimplemented!()
    }
}
impl<T: HasInterner> Binders<T> {
    fn map_ref<'a, U, OP>(&'a self, _op: OP) -> Binders<U>
    where
        OP: FnOnce(&'a T) -> U,
        U: HasInterner<Interner = T::Interner>,
    {
        unimplemented!()
    }
}
impl<T, I: Interner> Binders<T>
where
    T: Fold<I> + HasInterner<Interner = I>,
    I: Interner,
{
    fn substitute(self) -> T::Result {
        unimplemented!()
    }
}
impl<V, U> IntoIterator for Binders<V>
where
    V: HasInterner + IntoIterator<Item = U>,
    U: HasInterner<Interner = V::Interner>,
{
    type Item = Binders<U>;
    type IntoIter = BindersIntoIterator<V>;
    fn into_iter(self) -> Self::IntoIter {
        unimplemented!()
    }
}
struct BindersIntoIterator<V: HasInterner> {
    _binders: VariableKinds<V::Interner>,
}
impl<V> Iterator for BindersIntoIterator<V>
where
    V: HasInterner + IntoIterator,
    <V as IntoIterator>::Item: HasInterner<Interner = V::Interner>,
{
    type Item = Binders<<V as IntoIterator>::Item>;
    fn next(&mut self) -> Option<Self::Item> {
        unimplemented!()
    }
}

struct ImplDatum<I: Interner> {
    binders: Binders<ImplDatumBound<I>>,
}
struct ImplDatumBound<I: Interner> {
    where_clauses: Vec<Binders<WhereClause<I>>>,
}
impl<I: Interner> HasInterner for ImplDatumBound<I> {
    type Interner = I;
}

struct AssociatedTyDatum<I: Interner> {
    binders: Binders<AssociatedTyDatumBound<I>>,
}

struct AssociatedTyDatumBound<I: Interner> {
    where_clauses: Vec<Binders<WhereClause<I>>>,
}
impl<I: Interner> HasInterner for AssociatedTyDatumBound<I> {
    type Interner = I;
}

struct ClauseBuilder<'me, I: Interner> {
    db: &'me dyn RustIrDatabase<I>,
}
impl<'me, I: Interner> ClauseBuilder<'me, I> {
    fn new() -> Self {
        unimplemented!()
    }
    fn push_clause(&mut self, _conditions: impl Iterator<Item = Binders<Binders<WhereClause<I>>>>) {
        unimplemented!()
    }
}

pub(crate) struct Forest<I: Interner> {
    _marker: std::marker::PhantomData<I>,
}

impl<I: Interner> Forest<I> {
    fn iter_answers<'f>(&'f self) {
        let builder = &mut ClauseBuilder::<I>::new();
        let impl_datum = builder.db.impl_datum();
        let impl_where_clauses = impl_datum
            .binders
            .map_ref(|b| &b.where_clauses)
            .into_iter()
            .map(|wc| wc.cloned().substitute());
        let associated_ty = builder.db.associated_ty_data();
        let assoc_ty_where_clauses = associated_ty
            .binders
            .map_ref(|b| &b.where_clauses)
            .into_iter()
            .map(|wc| wc.cloned().substitute());
        builder.push_clause(impl_where_clauses.chain(assoc_ty_where_clauses));
    }
}

pub struct SLGSolver {
    pub(crate) forest: Forest<ChalkIr>,
}
impl SLGSolver {
    fn new() -> Self {
        unimplemented!()
    }
    fn solve_multiple(&self) {
        let _answers = self.forest.iter_answers();
    }
}

pub struct ChalkIr;
impl Interner for ChalkIr {
    type InternedVariableKinds = Vec<VariableKind<ChalkIr>>;
}

fn main() {
    let solver = SLGSolver::new();
    solver.solve_multiple();
}

bors added a commit to rust-lang-ci/rust that referenced this issue Mar 24, 2021
…komatsakis

Use `EvaluatedToOkModuloRegions` whenever we erase regions

Fixes rust-lang#80691

When we evaluate a trait predicate, we convert an
`EvaluatedToOk` result to `EvaluatedToOkModuloRegions` if we erased any
regions. We cache the result under a region-erased 'freshened'
predicate, so `EvaluatedToOk` may not be correct for other predicates
that have the same cache key.
@bors bors closed this as completed in 102b578 Mar 24, 2021
Dessix added a commit to microsoft/snocat that referenced this issue Apr 17, 2021
Also removes expect_none usages as it has been removed and recommended to be replaced with asserts by the compiler devs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-trait-system Area: Trait system C-bug Category: This is a bug. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants