Skip to content

Avoid leaking opaque hidden types via auto trait candidates - #159589

Open
bit-aloo wants to merge 6 commits into
rust-lang:mainfrom
bit-aloo:2026-07-29-opaque-type
Open

Avoid leaking opaque hidden types via auto trait candidates#159589
bit-aloo wants to merge 6 commits into
rust-lang:mainfrom
bit-aloo:2026-07-29-opaque-type

Conversation

@bit-aloo

@bit-aloo bit-aloo commented Jul 20, 2026

Copy link
Copy Markdown
Member

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. WG-trait-system-refactor The Rustc Trait System Refactor Initiative (-Znext-solver) labels Jul 20, 2026
@rustbot

rustbot commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

r? @nnethercote

rustbot has assigned @nnethercote.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

Why was this reviewer chosen?

The reviewer was selected based on:

  • Owners of files modified in this PR: compiler
  • compiler expanded to 74 candidates
  • Random selection from 16 candidates

@bit-aloo

Copy link
Copy Markdown
Member Author

r? @lcnr

@rustbot rustbot assigned lcnr and unassigned nnethercote Jul 20, 2026
return match candidate {
Ok(candidate) if has_only_region_constraints(candidate.result) => Ok(candidate),
Ok(_) => ecx.forced_ambiguity(MaybeInfo::AMBIGUOUS),
Err(NoSolutionOrRerunNonErased::NoSolution(_)) if param_env_may_leak_hidden_ty => {

@bit-aloo bit-aloo Jul 20, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Turns out NoSolution can leak hidden types too, since the failed proof can go through caller bounds and end up comparing the hidden type with something visible to the caller.

For now I went conservative, if the hidden-type proof fails and there are caller bounds around, I return ambiguity. That’s probably way broader than necessary, since totally unrelated bounds would get caught by this too. Curious if on what are your thoughts on this.

View changes since the review

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't get what you mean here 🤔 can you provide an example where NoSolution causes problems?

@bit-aloo bit-aloo Jul 21, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

In both the cases, the fudge result is different depending on whether the hidden type proof returns ambiguity or propagates NoSolution.

This is without the hack, where we return the error as is.

2ms DEBUG rustc_infer::infer::snapshot::fudge return=Ok((ObligationCause { span: /home/gh-Shourya742/rust/tests/ui/impl-trait/auto-trait-leakage/opaque-hidden-ty-inference.rs:25:18: 25:45 (#0), body_def_id: DefId(0:11 ~ opaque_hidden_ty_inference[daf7]::leak), code: ImplDerived(ImplDerivedCause { derived: DerivedCause { parent_trait_pred: Binder { value: TraitPredicate(<dep::WaddupGamers<T, {closure@dep::define<T>::{closure#0}}> as std::marker::Unpin>, polarity:Positive), bound_vars: [] }, parent_code: BuiltinDerived(DerivedCause { parent_trait_pred: Binder { value: TraitPredicate(<impl Sized as std::marker::Unpin>, polarity:Positive), bound_vars: [] }, parent_code: WhereClauseInExpr(DefId(0:5 ~ opaque_hidden_ty_inference[daf7]::require_auto), /home/gh-Shourya742/rust/tests/ui/impl-trait/auto-trait-leakage/opaque-hidden-ty-inference.rs:12:20: 12:25 (#0), HirId(DefId(0:11 ~ opaque_hidden_ty_inference[daf7]::leak).4), 1) }) }, impl_or_alias_def_id: DefId(20:9 ~ opaque_auto_trait_leakage[0084]::{impl#0}), impl_def_predicate_index: Some(0), span: /home/gh-Shourya742/rust/tests/ui/impl-trait/auto-trait-leakage/auxiliary/opaque-auto-trait-leakage.rs:3:14: 3:23 (#0) }) }, Obligation(predicate=Binder { value: ProjectionPredicate(Alias { kind: ProjectionTy { def_id: DefId(20:13 ~ opaque_auto_trait_leakage[0084]::Leak::Assoc) }, args: [T/#0], .. }, Term::Ty(Closure(DefId(20:20 ~ opaque_auto_trait_leakage[0084]::define::{closure#0}), [T/#0, i8, Binder { value: extern "RustCall" fn(()), bound_vars: [] }, ()]))), bound_vars: [] }, depth=2)))

The important part is that the diagnostic cause is now ImplDerived, with the parent obligation:

<dep::WaddupGamers<T, {closure@dep::define<T>::{closure#0}}> as std::marker::Unpin>

and the derived obligation:

<T as Leak>::Assoc = Closure(... define::{closure#0} ...)

So the hidden closure becomes part of the diagnostic obligation.

With the hack, where we turn this hidden type NoSolution into ambiguity, the fudge result stays at the original require_auto obligation:

1ms DEBUG rustc_infer::infer::snapshot::fudge return=Ok((ObligationCause { span: /home/gh-Shourya742/rust/tests/ui/impl-trait/auto-trait-leakage/opaque-hidden-ty-inference.rs:25:18: 25:45 (#0), body_def_id: DefId(0:11 ~ opaque_hidden_ty_inference[daf7]::leak), code: WhereClauseInExpr(DefId(0:5 ~ opaque_hidden_ty_inference[daf7]::require_auto), /home/gh-Shourya742/rust/tests/ui/impl-trait/auto-trait-leakage/opaque-hidden-ty-inference.rs:12:20: 12:25 (#0), HirId(DefId(0:11 ~ opaque_hidden_ty_inference[daf7]::leak).4), 1) }, Obligation(predicate=Binder { value: TraitPredicate(<impl Sized as std::marker::Unpin>, polarity:Positive), bound_vars: [] }, depth=0)))

Here the captured diagnostic obligation is only:

<impl Sized as std::marker::Unpin>

With ambiguity we do not capture the hidden WaddupGamers<T, closure> impl derived obligation as the diag cause. Without the hack, the hard NoSolution lets diag's select that hidden impl derived obligation, which is how the closure leaks into the diag.

@bit-aloo bit-aloo Jul 21, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Not sure this is the right fix, but hard failure here lets diagnostics pick up details the caller shouldn’t see.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

what is the code snippet whose behavior changes?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What I want to know is: if you don't do anything for Err(NoSolution) here, what is an example of a Rust code snippet whose compilation result is undesirable?

Not sure this is the right fix, but hard failure here lets diagnostics pick up details the caller shouldn’t see.

I.e. when is this actually the case

@bit-aloo bit-aloo Jul 23, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Ah!!! so the test itself fails, if we don't do anything for Err(NoSolution). And this is the error log.

 stderr -------------------------------
error[E0271]: type mismatch resolving `<T as Leak>::Assoc == {closure@define<T>::{closure#0}}`
  --> /home/gh-Shourya742/rust/tests/ui/impl-trait/auto-trait-leakage/opaque-hidden-ty-inference.rs:25:31
   |
LL |     let opaque = require_auto(define::<T>());
   |                  ------------ ^^^^^^^^^^^^^ expected closure, found `()`
   |                  |
   |                  required by a bound introduced by this call
   |
  ::: /home/gh-Shourya742/rust/tests/ui/impl-trait/auto-trait-leakage/auxiliary/opaque-auto-trait-leakage.rs:14:29
   |
LL |     WaddupGamers(None::<T>, || ())
   |                             -- the expected closure
   |
   = note: expected closure `{closure@dep::define<T>::{closure#0}}`
            found unit type `()`
   = note: required for `WaddupGamers<T, {closure@dep::define<T>::{closure#0}}>` to implement `Unpin`
note: required because it appears within the type `impl Sized`
  --> /home/gh-Shourya742/rust/tests/ui/impl-trait/auto-trait-leakage/auxiliary/opaque-auto-trait-leakage.rs:13:23
   |
LL | pub fn define<T>() -> impl Sized {
   |                       ^^^^^^^^^^
note: required by a bound in `require_auto`
  --> /home/gh-Shourya742/rust/tests/ui/impl-trait/auto-trait-leakage/opaque-hidden-ty-inference.rs:12:20
   |
LL | fn require_auto<T: Unpin>(x: T) -> T {
   |                    ^^^^^ required by this bound in `require_auto`

error: aborting due to 1 previous error

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

---- [ui] tests/ui/impl-trait/auto-trait-leakage/opaque-hidden-ty-inference.rs stdout end ----

failures:
    [ui] tests/ui/impl-trait/auto-trait-leakage/opaque-hidden-ty-inference.rs

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

We have the test added in PR, same as the one in issue.

@lcnr lcnr Jul 24, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ah, so the problem that errors leak through to diagnostics 🤔

This does result in ambiguity errors while I do think we should emit a proper hard errors here 🤔 this breaks existing supported code.

We should be able to have some Struct<T, U> with impl<T: Send> Struct<T, u32>
and impl<T> Struct<T, i32> where both impls have an inherent method of the same name. If you now call this inherent method with a receiver of type Struct<some_opaque, ?x> where the opaque type does not implement Send we should select the i32 method.

use std::rc::Rc;

struct Struct<T, U>(T, U);

impl<T: Send> Struct<T, u32> {
    fn method(&self) {}
}

impl<T> Struct<T, i32> {
    fn method(&self) {}
}

fn make_opaque() -> impl Sized {
    Rc::new(())
}

fn main() {
    Struct(make_opaque(), Default::default()).method();
}

I think instead we should change the inspect diagnostics visitor to not look into "opaque type auto trait candidates" 🤔 😁

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Was able to limit the diagnostic leak here: c54dd33

Comment thread compiler/rustc_next_trait_solver/src/solve/trait_goals.rs Outdated
Comment thread compiler/rustc_next_trait_solver/src/solve/trait_goals.rs
@bit-aloo
bit-aloo requested a review from lcnr July 21, 2026 07:10
@rust-bors

This comment has been minimized.

@rust-cloud-vms
rust-cloud-vms Bot force-pushed the 2026-07-29-opaque-type branch from 2e546e0 to 7c319d5 Compare July 23, 2026 15:28
Comment thread compiler/rustc_next_trait_solver/src/solve/assembly/structural_traits.rs Outdated
@rust-cloud-vms
rust-cloud-vms Bot force-pushed the 2026-07-29-opaque-type branch from 7c319d5 to c54dd33 Compare July 25, 2026 17:42
@rustbot

rustbot commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator

Some changes occurred to the core trait solver

cc @rust-lang/initiative-trait-system-refactor

@bit-aloo
bit-aloo requested a review from lcnr July 26, 2026 00:12
}
}

pub(in crate::solve) fn consider_auto_trait_candidate_for_opaque_ty<D, I>(

@lcnr lcnr Jul 27, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

please move this function into trait_goals.rs 🤔

it is only used from there and it doesn't feel "purely structural" enough to belong in this file

View changes since the review

return false;
}

let ty::Alias(_, ty::AliasTy { kind: ty::Opaque { def_id, .. }, .. }) =

@lcnr lcnr Jul 27, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
let ty::Alias(_, ty::AliasTy { kind: ty::Opaque { def_id, .. }, .. }) =
let ty::Alias(ty::Rigid::Yes, ty::AliasTy { kind: ty::Opaque { def_id, .. }, .. }) =

View changes since the review

return false;
};

!matches!(tcx.opaque_ty_origin(*def_id), hir::OpaqueTyOrigin::AsyncFn { .. })

@lcnr lcnr Jul 27, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@bit-aloo bit-aloo Jul 30, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

If we remove this we have two failing test:

[ui] tests/ui/traits/error-reporting/leaking-vars-in-cause-code-1.rs
[ui] tests/ui/traits/next-solver/auto-with-drop_tracking_mir.rs#fail

For auto-with-drop_tracking_mir.rs, we lose the cause and .await location:

@@ -1,29 +1,12 @@
-error[E0277]: `dyn AsyncFn<Fut = Pin<Box<dyn Future<Output = ()> + Send>>>` cannot be shared between threads safely
+error[E0277]: `impl Future<Output = ()>` cannot be sent between threads safely
   --> $DIR/leaking-vars-in-cause-code-1.rs:32:17
    |
 LL |     assert_send(cursed_fut());
-   |     ----------- ^^^^^^^^^^^^ `dyn AsyncFn<Fut = Pin<Box<dyn Future<Output = ()> + Send>>>` cannot be shared between threads safely
+   |     ----------- ^^^^^^^^^^^^ `impl Future<Output = ()>` cannot be sent between threads safely
    |     |
    |     required by a bound introduced by this call
    |
-   = help: the trait `Sync` is not implemented for `dyn AsyncFn<Fut = Pin<Box<dyn Future<Output = ()> + Send>>>`
-   = note: required for `&dyn AsyncFn<Fut = Pin<Box<dyn Future<Output = ()> + Send>>>` to implement `Send`
-note: required because it's used within this `async` fn body
-  --> $DIR/leaking-vars-in-cause-code-1.rs:19:53
-   |
-LL |   async fn wrap_call<P: AsyncFn + ?Sized>(filter: &P) {
-   |  _____________________________________________________^
-LL | |     filter.call().await;
-LL | | }
-   | |_^
-note: required because it's used within this `async` fn body
-  --> $DIR/leaking-vars-in-cause-code-1.rs:27:23
-   |
-LL |   async fn cursed_fut() {
-   |  _______________________^
-LL | |     wrap_call(get_boxed_fn().as_ref()).await;
-LL | | }
-   | |_^
+   = help: the trait `Send` is not implemented for `impl Future<Output = ()>`
 note: required by a bound in `assert_send`
   --> $DIR/leaking-vars-in-cause-code-1.rs:36:19
    |

For leaking-vars-in-cause-code-1.rs, we also lose the nested async-function context:

@@ -1,21 +1,12 @@
-error: future cannot be sent between threads safely
+error[E0277]: `impl Future<Output = ()>` cannot be sent between threads safely
   --> $DIR/auto-with-drop_tracking_mir.rs:25:13
    |
 LL |     is_send(foo());
-   |             ^^^^^ future returned by `foo` is not `Send`
+   |     ------- ^^^^^ `impl Future<Output = ()>` cannot be sent between threads safely
+   |     |
+   |     required by a bound introduced by this call
    |
-help: the trait `Sync` is not implemented for `NotSync`
-  --> $DIR/auto-with-drop_tracking_mir.rs:8:1
-   |
-LL | struct NotSync;
-   | ^^^^^^^^^^^^^^
-note: future is not `Send` as this value is used across an await
-  --> $DIR/auto-with-drop_tracking_mir.rs:16:11
-   |
-LL |     let x = &NotSync;
-   |         - has type `&NotSync` which is not `Send`
-LL |     bar().await;
-   |           ^^^^^ await occurs here, with `x` maybe used later
+   = help: the trait `Send` is not implemented for `impl Future<Output = ()>`
 note: required by a bound in `is_send`
   --> $DIR/auto-with-drop_tracking_mir.rs:24:24
    |
@@ -24,3 +15,4 @@
 
 error: aborting due to 1 previous error
 
+For more information about this error, try `rustc --explain E0277`.

It seems like we should continue diagnostic traversal for AsyncFn; otherwise, the resulting diagnostic loses most of its useful context. So I’m not sure we should remove this exception. 🤔 Wanna vibe check on what you think?

//@ ignore-compare-mode-next-solver
//@ compile-flags: -Znext-solver
//@ aux-build:opaque-auto-trait-leakage.rs

@lcnr lcnr Jul 27, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

comment explaining this test please, the relevant issue and a short explanation of what it does

View changes since the review

@lcnr lcnr added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Jul 28, 2026
@rust-cloud-vms
rust-cloud-vms Bot force-pushed the 2026-07-29-opaque-type branch from c54dd33 to 1557821 Compare July 30, 2026 10:09
@bit-aloo
bit-aloo requested a review from lcnr July 30, 2026 10:34
@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels Jul 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. WG-trait-system-refactor The Rustc Trait System Refactor Initiative (-Znext-solver)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

auto trait leakage can be used to leak arbitrary types

4 participants