Skip to content

Polonius (nightly default) loses the borrow captured by an impl Trait return value when the callee has a projection-typed parameter and is called through a closure #164337

Open

Description

@leeewee

Minimal reproducer (edition 2024, where return-position impl Trait captures all lifetimes):

trait Id { type Out: ?Sized; }
impl<X: ?Sized> Id for X { type Out = X; }

fn f<T: std::fmt::Display>(t: <&T as Id>::Out) -> impl std::fmt::Display {
    t
}

fn main() {
    let mut x = 0;
    let a = (|| f(&x))();
    x = 1; // should be rejected: `a` borrows `x`
    println!("{a}");
}

This compiles on current nightly and has undefined behaviour under Miri (an aliasing violation). It is rejected by NLL (-Zpolonius=off: E0506), by 1.100.0-beta.5 and by 1.99.0 stable (edition 2024). Writing the parameter as &T, or calling f(&x) directly, are both correctly rejected.

The same happens with an async fn, whose return type is an opaque impl Future. This is how it was first found:

use std::future::Future;
use std::pin::pin;
use std::task::{Context, Poll, Waker};

trait Id { type Out: ?Sized; }
impl<X: ?Sized> Id for X { type Out = X; }

async fn multiple_named_lifetimes<'a, 'b>(a: <&'a u8 as Id>::Out, _: &u8) -> &'a u8 {
    a
}

fn main() {
    let mut a = 22u8;
    let b = 44u8;
    let future = (|| multiple_named_lifetimes(&a, &b))();
    a += 1; // should be rejected: `future` still borrows `a`
    let mut fut = pin!(future);
    let mut cx = Context::from_waker(Waker::noop());
    if let Poll::Ready(r) = fut.as_mut().poll(&mut cx) {
        println!("{}", *r);
    }
}

Expected: error[E0506]: cannot assign to a because it is borrowed, as with NLL.

Actual: compiles. Under Miri:

error: Undefined Behavior: trying to retag from <505> for SharedReadOnly permission at alloc206[0x0], but that tag does not exist in the borrow stack for this location
 --> src/main.rs:8:43
  |
8 | async fn multiple_named_lifetimes<'a, 'b>(a: <&'a u8 as Id>::Out, _: &u8) -> &'a u8 {
  |                                           ^ this error occurs as part of retag at alloc206[0x0..0x1]

Both ingredients are required. Each of these is correctly rejected:

  • the same program with a: &'a u8 (no projection);
  • the direct call multiple_named_lifetimes(&a, &b) (no closure).

Versions / bisection:

Configuration Result
nightly-2026-10-13 (e9bbadf68), default accepted
nightly-2026-10-13, -Zpolonius=off / -Zpolonius=legacy rejected
nightly-2026-10-13, -Zpolonius=next accepted
nightly-2026-10-13, -Znext-solver=coherence and =globally accepted with both trait solvers
-Zpolonius=next on nightly-2025-01-01 rejected
-Zpolonius=next on nightly-2025-07-01 and later accepted
default nightly, 2026-08-05 (1ed2df61a19) rejected
default nightly, 2026-08-06 (7608eb7b07e, the merge of #159343 "Enable polonius alpha on nightly") accepted
1.100.0-beta.5, 1.99.0 stable rejected

So the bug has been present in Polonius alpha since the first half of 2025. #159343 made it reachable on default nightly.

Possibly related: #160979 ("A possible regression with -Zpolonius=next", A-impl-trait) and #161038 ("Unsoundness in Polonius involving impl Trait"). The async fn return type is an opaque impl Future, so this may share their root cause. The trigger here (a projection-typed parameter plus a closure call) is different.

LLM disclosure
The bug was found by an automated testing tool developed with the assistance of an LLM (Claude). This report text was drafted by an LLM and reviewed/edited by me.

Meta

rustc --version --verbose:

rustc 1.101.0-nightly (e9bbadf68 2026-10-12)
commit-hash: e9bbadf6824e65b3583c2dda3a0926ec76df9143

Activity

  1. added
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Oct 15, 2026
  2. added
    I-unsoundIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness
    A-impl-traitArea: `impl Trait`. Universally / existentially quantified anonymous types with static dispatch.
    NLL-poloniusIssues related for using Polonius in the borrow checker
    T-typesRelevant to the types team, which will review and decide on the PR/issue.
    and removed
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Oct 15, 2026
  3. added
    I-prioritizeIssue needs a team member to assess the impact. Will be replaced by P-{low,medium,high,critical}
    on Oct 15, 2026
  4. added
    A-closuresArea: Closures (`|…| { … }`)
    A-associated-itemsArea: Associated items (types, constants & functions)
    on Oct 15, 2026
  5. added
    S-found-by-fuzzingStatus: This issue was found using fuzzing. It may not arise in practical cases.
    on Nov 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-associated-itemsArea: Associated items (types, constants & functions)A-borrow-checkerArea: The borrow checkerA-closuresArea: Closures (`|…| { … }`)A-impl-traitArea: `impl Trait`. Universally / existentially quantified anonymous types with static dispatch.C-bugCategory: This is a bug.I-prioritizeIssue needs a team member to assess the impact. Will be replaced by P-{low,medium,high,critical}I-unsoundIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/SoundnessNLL-poloniusIssues related for using Polonius in the borrow checkerS-found-by-fuzzingStatus: This issue was found using fuzzing. It may not arise in practical cases.T-typesRelevant to the types team, which will review and decide on the PR/issue.regression-from-stable-to-nightlyPerformance or correctness regression from stable to nightly.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions