Skip to content

Use the entire type of a dropped local to compute variance (edge direction) for Polonius alpha - #161305

Merged
rust-bors[bot] merged 1 commit into
rust-lang:mainfrom
amandasystems:issue-160670
Sep 17, 2026
Merged

rust-bors[bot] merged 1 commit into
rust-lang:mainfrom
amandasystems:issue-160670

Conversation

@amandasystems

@amandasystems amandasystems commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

View all comments

Fixes: #160670

The soundness issue is caused by (as suggested by the text extruder) the incorrect variance for a region, which is supposed to be bidirectional (invariant) but is registered as contravariant.

Starting with this example (from the issue):

use std::fmt::Debug;

struct D<T: HasArg>(T::Arg);

trait HasArg {
    type Arg: Debug;
}
impl<'a, T: Debug> HasArg for fn(&'a T) {
    type Arg = &'a T;
}
impl<T: HasArg> Drop for D<T> {
    fn drop(&mut self) {
        println!("{:?}", self.0);
    }
}
fn mk<'a, T: Debug>(r: &'a T) -> D<fn(&'a T)> {
    D(r)
}

fn main() {
    let b = Box::new(vec![vec![1]]);
    let d;
    d = mk(&*b);
    drop(b); // ERROR: move out of borrowed...
}

This generates a path through MIR on the way to a drop that looks like this:

_1 = move _2

/// ...

drop(_1)

In this instance, the types of _1 and _2 are D<fn(&'?1 Vec<...>) and D<fn(&'?2 Vec<...>) respectively.

During liveness computation (in liveness::trace) region liveness is computed from drop liveness and use liveness. Additionally, for each live region (drop-live or use-live), region variance is computed for Polonius' loan propagation. Variance determines the direction of propagation across program flow.

For drop-live locals (variables), the types reported in DropckOutlivesResult::kinds are used to register drop live regions and compute their variances. However, instead of using the full type D<...> for the left-hand side of this assignment statement, kinds starts with a Binder {...} and the function type inside of it. From that it finds region '?1 and records it as contravariant (backwards propagated).

This PR addresses the issue by using the entire type of the drop-live local to compute the variance of any regions referenced inside it, at the cost of potentially doing unnecessary extra work, either when iteration continues over DropckOutlivesResult::kinds (which should be redundant with it in most cases), or if the local contains a region whose variance is actually not needed for computation or in regard to drop liveness (assuming that ever happens).

It also adds some debug statements that helped me debug the issue, and a ui test for the soundness issue.

@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. labels Aug 18, 2026
@rustbot

rustbot commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

r? @mejrs

rustbot has assigned @mejrs.
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: borrowck, compiler
  • borrowck, compiler expanded to 75 candidates
  • Random selection from 16 candidates

@lqd lqd assigned lqd and jackh726 and unassigned mejrs Aug 18, 2026
@amandasystems amandasystems changed the title Issue 160670 Use the entire type of a dropped local to compute variance (edge direction) for Polonius alpha Aug 18, 2026
Comment thread compiler/rustc_borrowck/src/type_check/liveness/trace.rs
/// points `live_at`.
fn add_use_live_facts_for(&mut self, value: Ty<'tcx>, live_at: &IntervalSet<PointIndex>) {
debug!("add_use_live_facts_for(value={:?})", value);
Self::record_polonius_region_variance_from_type(self.typeck, value);

@jackh726 jackh726 Aug 18, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm surprised we don't have a check elsewhere that asserts that we've recorded the variance for all regions.

(For the issue I was looking, I was thinking that we might want to ensure that liveness for every region var is recorded.)

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We have the exact opposite in fact! An unwrap-or-default for when we haven’t!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What's the fallout for just an expect there?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

(That would be the fallback case that activates if I removed some type from the drop liveness, and it’s fail safe into bidirectional edges)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

that we've recorded the variance for all regions.

we don't propagate loans to dead regions throughout the CFG so I may be misunderstanding what you mean

@lqd lqd Aug 18, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

And the default case for a region whose variance we haven't recorded is to create bidir edges, so it wouldn't have helped to record the variance for all regions, depending on what you mean by "all". This case has to be because the variance is different in different types/drop kinds with some shared free region, and we're not recording all of these different contexts, not missing regions.

So the fun thing here looks to be the local is either not drop-live but some of its """drop kinds""" are, or it is and we needed to also, or only, record the variance of free regions in its type instead of just of the ones the actual drop-live drop kinds. Remember: NLL only records the regions in the drop kinds as live. The fact that the variance changes between these and the local's type is just the cherry on top.

@amandasystems amandasystems Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

the local is either not drop-live but some of its """drop kinds""" are

That's possibly what's happening, though I'm not sure because all the parameter names seem to suggest the local is indeed drop live. The local becomes (drop) live in the next statement (I'm pretty sure, haven't fully checked), but for some reason, possibly due to assignment, its own type isn't among the drop-live kinds (only one layer in, for some reason). This might well be a bug or (more likely) an undocumented optimisation in drop live kind computation, but I don't know what it's supposed to do so I didn't dare touch it.

Before this apparently nobody needed the entire type of the local being dropped in all cases, but we do for the variance.

If the drop kinds never add anything to the variance, this PR is correct and may in fact be a slight optimisation, who knows, since it doesn't use the kinds to compute variance. If they may contain regions not in the type of the dropped local, I may lose variance information for them. This is a potential soundness issue if they appear somewhere else with an edge in only one direction, but otherwise just a risk of a compile failure (because we fail safe to a bidir edge). This can be trivially fixed by doing the variance computation on all the live kinds, at the risk of doing duplicate work.

I intentionally don't touch liveness, since it should be correct (tm).

@jackh726

Copy link
Copy Markdown
Member

@lqd and I were looking at this a bit today

First, this example is a bit more minimal, which should help to understand the MIR a bit more.

struct D<T: HasArg>(T::Arg);

trait HasArg {
    type Arg;
}
impl<'a, T> HasArg for fn(&'a T) {
    type Arg = &'a T;
}
impl<T: HasArg> Drop for D<T> {
    fn drop(&mut self) {}
}
fn mk<'a, T>(r: &'a T) -> D<fn(&'a T)> {
    D(r)
}

fn main() {
    let b = Box::new(0u8);
    let d;
    d = mk(&*b);
    drop(b); // ERROR: move out of borrowed...
}

One thing we noticed was that the let d; d = mk(&*b); split is actually load-bearing for this. Splitting that essentially makes a new local with a single move into the dropped local (d). It was surprising that just that addition causes a problem.

@rust-log-analyzer

This comment has been minimized.

Comment thread compiler/rustc_borrowck/src/polonius/mod.rs Outdated
/// `live_kind` is the type of a use-live of drop-live local.
/// Record the variance of any region(s) appearing in it for
/// Polonius. Does nothing if Polonius is not active.
fn record_polonius_region_variance_from_type(

@lqd lqd Aug 31, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We can name this something simpler like record_variance or something, and we shouldn't to need it to be generic and can use a Ty.

View changes since the review

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This was resolved but looks incomplete.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I did change the name (I picked record_variance()), but I couldn't change the type because of make_all_regions_live(), and then there's a cascade. In principle they should all be GenericArgs or Tys, but the problem is the "or". However, I could make it into a GenericArg by converting a bunch of Tys and I guess that's as good as it's going to get.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

(I'm scared of unwrapping a Ty out of them in case there's a weird edge case)

Comment thread compiler/rustc_borrowck/src/polonius/constraints.rs Outdated
@rust-bors

This comment has been minimized.

@amandasystems
amandasystems force-pushed the issue-160670 branch 3 times, most recently from 7679b11 to 92e4034 Compare September 2, 2026 14:55
@rustbot

rustbot commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

This PR was rebased onto a different main commit. Here's a range-diff highlighting what actually changed.

Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers.

@jackh726

jackh726 commented Sep 15, 2026

Copy link
Copy Markdown
Member

One thing we noticed was that the let d; d = mk(&*b); split is actually load-bearing for this. Splitting that essentially makes a new local with a single move into the dropped local (d). It was surprising that just that addition causes a problem.

Btw, the reason that this is the case:

let d = mk(&*b);

introduces a FakeRead for the local, but

let d;
d = mk(&*b);

does not.

That FakeRead is treated a use-live.

This also compiles with alpha:

let (d,) = (mk(&*b),);

(see #53695)

@amandasystems

Copy link
Copy Markdown
Contributor Author

Thanks for looking into that! I’ve been badly swamped with teaching and admin stuff the last two weeks and haven’t had time to do much of anything really.

Is there anything else we need to investigate to determine if we merge this?

@jackh726 jackh726 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

A couple minor debugging changes, but r=me after

View changes since this review

Comment thread compiler/rustc_borrowck/src/polonius/constraints.rs Outdated
Comment thread compiler/rustc_borrowck/src/polonius/constraints.rs Outdated
Comment thread compiler/rustc_borrowck/src/polonius/constraints.rs Outdated
@lqd

lqd commented Sep 15, 2026

Copy link
Copy Markdown
Member

I had also left a couple of comments which I don't think were completed yet.

@jackh726

Copy link
Copy Markdown
Member

I had also left a couple of comments which I don't think were completed yet.

which?

@lqd

lqd commented Sep 15, 2026

Copy link
Copy Markdown
Member

I've unresolved the two.

@amandasystems

Copy link
Copy Markdown
Contributor Author

Should be fixed now! I left the commits unsquashed in case anyone wants to check I didn't mess up.

@lqd

lqd commented Sep 16, 2026

Copy link
Copy Markdown
Member

Thanks!

@bors squash message="Use the entire type of the live variable to compute region variance"

@lqd

lqd commented Sep 16, 2026

Copy link
Copy Markdown
Member

Things are squashed (GH had added the coauthor for one of the suggestions that were applied, but ok ..., if you don't want that please force push it away), let's :shipit:.

@lqd

lqd commented Sep 16, 2026

Copy link
Copy Markdown
Member

Confirmed the coauthorship is fine on zulip.

@bors r=lqd,jackh726 rollup

@rust-bors

rust-bors Bot commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

📌 Commit 8265b6f has been approved by lqd,jackh726

It is now in the queue for this repository.

@rust-bors rust-bors Bot added S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Sep 16, 2026
JonathanBrouwer added a commit to JonathanBrouwer/rust that referenced this pull request Sep 16, 2026
…jackh726

Use the entire type of a dropped local to compute variance (edge direction) for Polonius alpha

Fixes: rust-lang#160670

The soundness issue is caused by (as suggested by the text extruder) the incorrect variance for a region, which is supposed to be bidirectional (invariant) but is registered as contravariant.

Starting with this example (from the issue):
```rust
use std::fmt::Debug;

struct D<T: HasArg>(T::Arg);

trait HasArg {
    type Arg: Debug;
}
impl<'a, T: Debug> HasArg for fn(&'a T) {
    type Arg = &'a T;
}
impl<T: HasArg> Drop for D<T> {
    fn drop(&mut self) {
        println!("{:?}", self.0);
    }
}
fn mk<'a, T: Debug>(r: &'a T) -> D<fn(&'a T)> {
    D(r)
}

fn main() {
    let b = Box::new(vec![vec![1]]);
    let d;
    d = mk(&*b);
    drop(b); // ERROR: move out of borrowed...
}
```

This generates a path through MIR on the way to a drop that looks like this:

```MIR
_1 = move _2

/// ...

drop(_1)
```
In this instance, the types of `_1` and `_2` are `D<fn(&'?1 Vec<...>)` and `D<fn(&'?2 Vec<...>)` respectively.

During liveness computation (in `liveness::trace`) region liveness is computed from drop liveness and use liveness. Additionally, for each live region (drop-live or use-live), region variance is computed for Polonius' loan propagation. Variance determines the direction of propagation across program flow.

For drop-live locals (variables), the types reported in ` DropckOutlivesResult::kinds` are used to register drop live regions and compute their variances. However, instead of using the full type `D<...>` for the left-hand side of this assignment statement, `kinds` starts with a `Binder {...}` and the function type inside of it. From that it finds region `'?1` and records it as contravariant (backwards propagated).

This PR addresses the issue by using the entire type of the drop-live local to compute the variance of any regions referenced inside it, at the cost of potentially doing unnecessary extra work, ~~either when iteration continues over `DropckOutlivesResult::kinds`  (which should be redundant with it in most cases), or~~ if the local contains a region whose variance is actually not needed for computation or in regard to drop liveness (assuming that ever happens).

It also adds some debug statements that helped me debug the issue, and a ui test for the soundness issue.
JonathanBrouwer added a commit to JonathanBrouwer/rust that referenced this pull request Sep 16, 2026
…jackh726

Use the entire type of a dropped local to compute variance (edge direction) for Polonius alpha

Fixes: rust-lang#160670

The soundness issue is caused by (as suggested by the text extruder) the incorrect variance for a region, which is supposed to be bidirectional (invariant) but is registered as contravariant.

Starting with this example (from the issue):
```rust
use std::fmt::Debug;

struct D<T: HasArg>(T::Arg);

trait HasArg {
    type Arg: Debug;
}
impl<'a, T: Debug> HasArg for fn(&'a T) {
    type Arg = &'a T;
}
impl<T: HasArg> Drop for D<T> {
    fn drop(&mut self) {
        println!("{:?}", self.0);
    }
}
fn mk<'a, T: Debug>(r: &'a T) -> D<fn(&'a T)> {
    D(r)
}

fn main() {
    let b = Box::new(vec![vec![1]]);
    let d;
    d = mk(&*b);
    drop(b); // ERROR: move out of borrowed...
}
```

This generates a path through MIR on the way to a drop that looks like this:

```MIR
_1 = move _2

/// ...

drop(_1)
```
In this instance, the types of `_1` and `_2` are `D<fn(&'?1 Vec<...>)` and `D<fn(&'?2 Vec<...>)` respectively.

During liveness computation (in `liveness::trace`) region liveness is computed from drop liveness and use liveness. Additionally, for each live region (drop-live or use-live), region variance is computed for Polonius' loan propagation. Variance determines the direction of propagation across program flow.

For drop-live locals (variables), the types reported in ` DropckOutlivesResult::kinds` are used to register drop live regions and compute their variances. However, instead of using the full type `D<...>` for the left-hand side of this assignment statement, `kinds` starts with a `Binder {...}` and the function type inside of it. From that it finds region `'?1` and records it as contravariant (backwards propagated).

This PR addresses the issue by using the entire type of the drop-live local to compute the variance of any regions referenced inside it, at the cost of potentially doing unnecessary extra work, ~~either when iteration continues over `DropckOutlivesResult::kinds`  (which should be redundant with it in most cases), or~~ if the local contains a region whose variance is actually not needed for computation or in regard to drop liveness (assuming that ever happens).

It also adds some debug statements that helped me debug the issue, and a ui test for the soundness issue.
rust-bors Bot pushed a commit that referenced this pull request Sep 16, 2026
…uwer

Rollup of 8 pull requests

Successful merges:

 - #162796 (libtest: do not early exit from test runners)
 - #162844 (Add loan reachability traces to polonius MIR dumps)
 - #158186 (Guarantee 8 bytes of alignment of RawWakerVTable)
 - #160108 (Stabilize `windows_process_extensions_main_thread_handle`)
 - #161305 (Use the entire type of a dropped local to compute variance (edge direction) for Polonius alpha)
 - #161838 (tests: accept LLVM 24 optimization in this test)
 - #162825 (core: Add examples for `debug_closure_helpers`)
 - #162856 (Stabilize CommandExt::show_window)
Zalathar added a commit to Zalathar/rust that referenced this pull request Sep 17, 2026
…jackh726

Use the entire type of a dropped local to compute variance (edge direction) for Polonius alpha

Fixes: rust-lang#160670

The soundness issue is caused by (as suggested by the text extruder) the incorrect variance for a region, which is supposed to be bidirectional (invariant) but is registered as contravariant.

Starting with this example (from the issue):
```rust
use std::fmt::Debug;

struct D<T: HasArg>(T::Arg);

trait HasArg {
    type Arg: Debug;
}
impl<'a, T: Debug> HasArg for fn(&'a T) {
    type Arg = &'a T;
}
impl<T: HasArg> Drop for D<T> {
    fn drop(&mut self) {
        println!("{:?}", self.0);
    }
}
fn mk<'a, T: Debug>(r: &'a T) -> D<fn(&'a T)> {
    D(r)
}

fn main() {
    let b = Box::new(vec![vec![1]]);
    let d;
    d = mk(&*b);
    drop(b); // ERROR: move out of borrowed...
}
```

This generates a path through MIR on the way to a drop that looks like this:

```MIR
_1 = move _2

/// ...

drop(_1)
```
In this instance, the types of `_1` and `_2` are `D<fn(&'?1 Vec<...>)` and `D<fn(&'?2 Vec<...>)` respectively.

During liveness computation (in `liveness::trace`) region liveness is computed from drop liveness and use liveness. Additionally, for each live region (drop-live or use-live), region variance is computed for Polonius' loan propagation. Variance determines the direction of propagation across program flow.

For drop-live locals (variables), the types reported in ` DropckOutlivesResult::kinds` are used to register drop live regions and compute their variances. However, instead of using the full type `D<...>` for the left-hand side of this assignment statement, `kinds` starts with a `Binder {...}` and the function type inside of it. From that it finds region `'?1` and records it as contravariant (backwards propagated).

This PR addresses the issue by using the entire type of the drop-live local to compute the variance of any regions referenced inside it, at the cost of potentially doing unnecessary extra work, ~~either when iteration continues over `DropckOutlivesResult::kinds`  (which should be redundant with it in most cases), or~~ if the local contains a region whose variance is actually not needed for computation or in regard to drop liveness (assuming that ever happens).

It also adds some debug statements that helped me debug the issue, and a ui test for the soundness issue.
rust-bors Bot pushed a commit that referenced this pull request Sep 17, 2026
Rollup of 12 pull requests

Successful merges:

 - #161596 (coretests: Add more pattern tests.)
 - #162796 (libtest: do not early exit from test runners)
 - #162844 (Add loan reachability traces to polonius MIR dumps)
 - #158186 (Guarantee 8 bytes of alignment of RawWakerVTable)
 - #160108 (Stabilize `windows_process_extensions_main_thread_handle`)
 - #160212 (traits: Fix rigid alias liveness matching)
 - #160544 (Stabilize `feature(trim_prefix_suffix)` (`{str, [T], Path}::trim_prefix` and `{str, [T]}::trim_suffix`))
 - #161305 (Use the entire type of a dropped local to compute variance (edge direction) for Polonius alpha)
 - #161838 (tests: accept LLVM 24 optimization in this test)
 - #162805 (Add `must_use` lint to `ExitCode`)
 - #162825 (core: Add examples for `debug_closure_helpers`)
 - #162856 (Stabilize CommandExt::show_window)
Zalathar added a commit to Zalathar/rust that referenced this pull request Sep 17, 2026
…jackh726

Use the entire type of a dropped local to compute variance (edge direction) for Polonius alpha

Fixes: rust-lang#160670

The soundness issue is caused by (as suggested by the text extruder) the incorrect variance for a region, which is supposed to be bidirectional (invariant) but is registered as contravariant.

Starting with this example (from the issue):
```rust
use std::fmt::Debug;

struct D<T: HasArg>(T::Arg);

trait HasArg {
    type Arg: Debug;
}
impl<'a, T: Debug> HasArg for fn(&'a T) {
    type Arg = &'a T;
}
impl<T: HasArg> Drop for D<T> {
    fn drop(&mut self) {
        println!("{:?}", self.0);
    }
}
fn mk<'a, T: Debug>(r: &'a T) -> D<fn(&'a T)> {
    D(r)
}

fn main() {
    let b = Box::new(vec![vec![1]]);
    let d;
    d = mk(&*b);
    drop(b); // ERROR: move out of borrowed...
}
```

This generates a path through MIR on the way to a drop that looks like this:

```MIR
_1 = move _2

/// ...

drop(_1)
```
In this instance, the types of `_1` and `_2` are `D<fn(&'?1 Vec<...>)` and `D<fn(&'?2 Vec<...>)` respectively.

During liveness computation (in `liveness::trace`) region liveness is computed from drop liveness and use liveness. Additionally, for each live region (drop-live or use-live), region variance is computed for Polonius' loan propagation. Variance determines the direction of propagation across program flow.

For drop-live locals (variables), the types reported in ` DropckOutlivesResult::kinds` are used to register drop live regions and compute their variances. However, instead of using the full type `D<...>` for the left-hand side of this assignment statement, `kinds` starts with a `Binder {...}` and the function type inside of it. From that it finds region `'?1` and records it as contravariant (backwards propagated).

This PR addresses the issue by using the entire type of the drop-live local to compute the variance of any regions referenced inside it, at the cost of potentially doing unnecessary extra work, ~~either when iteration continues over `DropckOutlivesResult::kinds`  (which should be redundant with it in most cases), or~~ if the local contains a region whose variance is actually not needed for computation or in regard to drop liveness (assuming that ever happens).

It also adds some debug statements that helped me debug the issue, and a ui test for the soundness issue.
Zalathar added a commit to Zalathar/rust that referenced this pull request Sep 17, 2026
…jackh726

Use the entire type of a dropped local to compute variance (edge direction) for Polonius alpha

Fixes: rust-lang#160670

The soundness issue is caused by (as suggested by the text extruder) the incorrect variance for a region, which is supposed to be bidirectional (invariant) but is registered as contravariant.

Starting with this example (from the issue):
```rust
use std::fmt::Debug;

struct D<T: HasArg>(T::Arg);

trait HasArg {
    type Arg: Debug;
}
impl<'a, T: Debug> HasArg for fn(&'a T) {
    type Arg = &'a T;
}
impl<T: HasArg> Drop for D<T> {
    fn drop(&mut self) {
        println!("{:?}", self.0);
    }
}
fn mk<'a, T: Debug>(r: &'a T) -> D<fn(&'a T)> {
    D(r)
}

fn main() {
    let b = Box::new(vec![vec![1]]);
    let d;
    d = mk(&*b);
    drop(b); // ERROR: move out of borrowed...
}
```

This generates a path through MIR on the way to a drop that looks like this:

```MIR
_1 = move _2

/// ...

drop(_1)
```
In this instance, the types of `_1` and `_2` are `D<fn(&'?1 Vec<...>)` and `D<fn(&'?2 Vec<...>)` respectively.

During liveness computation (in `liveness::trace`) region liveness is computed from drop liveness and use liveness. Additionally, for each live region (drop-live or use-live), region variance is computed for Polonius' loan propagation. Variance determines the direction of propagation across program flow.

For drop-live locals (variables), the types reported in ` DropckOutlivesResult::kinds` are used to register drop live regions and compute their variances. However, instead of using the full type `D<...>` for the left-hand side of this assignment statement, `kinds` starts with a `Binder {...}` and the function type inside of it. From that it finds region `'?1` and records it as contravariant (backwards propagated).

This PR addresses the issue by using the entire type of the drop-live local to compute the variance of any regions referenced inside it, at the cost of potentially doing unnecessary extra work, ~~either when iteration continues over `DropckOutlivesResult::kinds`  (which should be redundant with it in most cases), or~~ if the local contains a region whose variance is actually not needed for computation or in regard to drop liveness (assuming that ever happens).

It also adds some debug statements that helped me debug the issue, and a ui test for the soundness issue.
rust-bors Bot pushed a commit that referenced this pull request Sep 17, 2026
Rollup of 16 pull requests

Successful merges:

 - #161596 (coretests: Add more pattern tests.)
 - #162796 (libtest: do not early exit from test runners)
 - #162844 (Add loan reachability traces to polonius MIR dumps)
 - #162876 (Move operations out of `rustc_middle::query::job`)
 - #160108 (Stabilize `windows_process_extensions_main_thread_handle`)
 - #160212 (traits: Fix rigid alias liveness matching)
 - #160544 (Stabilize `feature(trim_prefix_suffix)` (`{str, [T], Path}::trim_prefix` and `{str, [T]}::trim_suffix`))
 - #161246 (Normalize non-rigid aliases in ty_known_to_outlive)
 - #161305 (Use the entire type of a dropped local to compute variance (edge direction) for Polonius alpha)
 - #161838 (tests: accept LLVM 24 optimization in this test)
 - #162805 (Add `must_use` lint to `ExitCode`)
 - #162825 (core: Add examples for `debug_closure_helpers`)
 - #162841 (enable asm tests for xtensa targets)
 - #162842 (reintroduce check RibKind::ConstParamTy did in direct consts)
 - #162845 (mgca: fix issue with mismatched array valtree/valtree tys)
 - #162856 (Stabilize CommandExt::show_window)
JonathanBrouwer added a commit to JonathanBrouwer/rust that referenced this pull request Sep 17, 2026
…jackh726

Use the entire type of a dropped local to compute variance (edge direction) for Polonius alpha

Fixes: rust-lang#160670

The soundness issue is caused by (as suggested by the text extruder) the incorrect variance for a region, which is supposed to be bidirectional (invariant) but is registered as contravariant.

Starting with this example (from the issue):
```rust
use std::fmt::Debug;

struct D<T: HasArg>(T::Arg);

trait HasArg {
    type Arg: Debug;
}
impl<'a, T: Debug> HasArg for fn(&'a T) {
    type Arg = &'a T;
}
impl<T: HasArg> Drop for D<T> {
    fn drop(&mut self) {
        println!("{:?}", self.0);
    }
}
fn mk<'a, T: Debug>(r: &'a T) -> D<fn(&'a T)> {
    D(r)
}

fn main() {
    let b = Box::new(vec![vec![1]]);
    let d;
    d = mk(&*b);
    drop(b); // ERROR: move out of borrowed...
}
```

This generates a path through MIR on the way to a drop that looks like this:

```MIR
_1 = move _2

/// ...

drop(_1)
```
In this instance, the types of `_1` and `_2` are `D<fn(&'?1 Vec<...>)` and `D<fn(&'?2 Vec<...>)` respectively.

During liveness computation (in `liveness::trace`) region liveness is computed from drop liveness and use liveness. Additionally, for each live region (drop-live or use-live), region variance is computed for Polonius' loan propagation. Variance determines the direction of propagation across program flow.

For drop-live locals (variables), the types reported in ` DropckOutlivesResult::kinds` are used to register drop live regions and compute their variances. However, instead of using the full type `D<...>` for the left-hand side of this assignment statement, `kinds` starts with a `Binder {...}` and the function type inside of it. From that it finds region `'?1` and records it as contravariant (backwards propagated).

This PR addresses the issue by using the entire type of the drop-live local to compute the variance of any regions referenced inside it, at the cost of potentially doing unnecessary extra work, ~~either when iteration continues over `DropckOutlivesResult::kinds`  (which should be redundant with it in most cases), or~~ if the local contains a region whose variance is actually not needed for computation or in regard to drop liveness (assuming that ever happens).

It also adds some debug statements that helped me debug the issue, and a ui test for the soundness issue.
rust-bors Bot pushed a commit that referenced this pull request Sep 17, 2026
…uwer

Rollup of 24 pull requests

Successful merges:

 - #161596 (coretests: Add more pattern tests.)
 - #162177 (Properly implement the gpu-kernel ABI for amdgpu)
 - #162411 (Make Receiver `#[rustc_dyn_incompatible_trait]`)
 - #162760 (yeet alias new_from_def_id)
 - #162796 (libtest: do not early exit from test runners)
 - #162844 (Add loan reachability traces to polonius MIR dumps)
 - #162876 (Move operations out of `rustc_middle::query::job`)
 - #160108 (Stabilize `windows_process_extensions_main_thread_handle`)
 - #160212 (traits: Fix rigid alias liveness matching)
 - #160544 (Stabilize `feature(trim_prefix_suffix)` (`{str, [T], Path}::trim_prefix` and `{str, [T]}::trim_suffix`))
 - #161305 (Use the entire type of a dropped local to compute variance (edge direction) for Polonius alpha)
 - #161838 (tests: accept LLVM 24 optimization in this test)
 - #162312 (core: Rewrite docs for try_as_dyn)
 - #162785 (Avoid creating overlapping assignments in MatchBranchSimplification)
 - #162805 (Add `must_use` lint to `ExitCode`)
 - #162825 (core: Add examples for `debug_closure_helpers`)
 - #162841 (enable asm tests for xtensa targets)
 - #162842 (reintroduce check RibKind::ConstParamTy did in direct consts)
 - #162845 (mgca: fix issue with mismatched array valtree/valtree tys)
 - #162856 (Stabilize CommandExt::show_window)
 - #162865 (Complex conjugate, negation and default)
 - #162874 (Add support for `annotate_snippets::snippet::AnnotationKind::Visible`)
 - #162881 (Simplify the macro for forwarding Decoder methods )
 - #162888 (Fix a typo on the Armv7-R platform docs page)
rust-bors Bot pushed a commit that referenced this pull request Sep 17, 2026
…uwer

Rollup of 23 pull requests

Successful merges:

 - #161596 (coretests: Add more pattern tests.)
 - #162411 (Make Receiver `#[rustc_dyn_incompatible_trait]`)
 - #162760 (yeet alias new_from_def_id)
 - #162796 (libtest: do not early exit from test runners)
 - #162844 (Add loan reachability traces to polonius MIR dumps)
 - #162876 (Move operations out of `rustc_middle::query::job`)
 - #160108 (Stabilize `windows_process_extensions_main_thread_handle`)
 - #160212 (traits: Fix rigid alias liveness matching)
 - #160544 (Stabilize `feature(trim_prefix_suffix)` (`{str, [T], Path}::trim_prefix` and `{str, [T]}::trim_suffix`))
 - #161305 (Use the entire type of a dropped local to compute variance (edge direction) for Polonius alpha)
 - #161838 (tests: accept LLVM 24 optimization in this test)
 - #162312 (core: Rewrite docs for try_as_dyn)
 - #162785 (Avoid creating overlapping assignments in MatchBranchSimplification)
 - #162805 (Add `must_use` lint to `ExitCode`)
 - #162825 (core: Add examples for `debug_closure_helpers`)
 - #162841 (enable asm tests for xtensa targets)
 - #162842 (reintroduce check RibKind::ConstParamTy did in direct consts)
 - #162845 (mgca: fix issue with mismatched array valtree/valtree tys)
 - #162856 (Stabilize CommandExt::show_window)
 - #162865 (Complex conjugate, negation and default)
 - #162874 (Add support for `annotate_snippets::snippet::AnnotationKind::Visible`)
 - #162881 (Simplify the macro for forwarding Decoder methods )
 - #162888 (Fix a typo on the Armv7-R platform docs page)
@rust-bors
rust-bors Bot merged commit d025f9b into rust-lang:main Sep 17, 2026
13 checks passed
@rustbot rustbot added this to the 1.100.0 milestone Sep 17, 2026
rust-bors Bot pushed a commit that referenced this pull request Sep 17, 2026
Rollup merge of #161305 - amandasystems:issue-160670, r=lqd,jackh726

Use the entire type of a dropped local to compute variance (edge direction) for Polonius alpha

Fixes: #160670

The soundness issue is caused by (as suggested by the text extruder) the incorrect variance for a region, which is supposed to be bidirectional (invariant) but is registered as contravariant.

Starting with this example (from the issue):
```rust
use std::fmt::Debug;

struct D<T: HasArg>(T::Arg);

trait HasArg {
    type Arg: Debug;
}
impl<'a, T: Debug> HasArg for fn(&'a T) {
    type Arg = &'a T;
}
impl<T: HasArg> Drop for D<T> {
    fn drop(&mut self) {
        println!("{:?}", self.0);
    }
}
fn mk<'a, T: Debug>(r: &'a T) -> D<fn(&'a T)> {
    D(r)
}

fn main() {
    let b = Box::new(vec![vec![1]]);
    let d;
    d = mk(&*b);
    drop(b); // ERROR: move out of borrowed...
}
```

This generates a path through MIR on the way to a drop that looks like this:

```MIR
_1 = move _2

/// ...

drop(_1)
```
In this instance, the types of `_1` and `_2` are `D<fn(&'?1 Vec<...>)` and `D<fn(&'?2 Vec<...>)` respectively.

During liveness computation (in `liveness::trace`) region liveness is computed from drop liveness and use liveness. Additionally, for each live region (drop-live or use-live), region variance is computed for Polonius' loan propagation. Variance determines the direction of propagation across program flow.

For drop-live locals (variables), the types reported in ` DropckOutlivesResult::kinds` are used to register drop live regions and compute their variances. However, instead of using the full type `D<...>` for the left-hand side of this assignment statement, `kinds` starts with a `Binder {...}` and the function type inside of it. From that it finds region `'?1` and records it as contravariant (backwards propagated).

This PR addresses the issue by using the entire type of the drop-live local to compute the variance of any regions referenced inside it, at the cost of potentially doing unnecessary extra work, ~~either when iteration continues over `DropckOutlivesResult::kinds`  (which should be redundant with it in most cases), or~~ if the local contains a region whose variance is actually not needed for computation or in regard to drop liveness (assuming that ever happens).

It also adds some debug statements that helped me debug the issue, and a ui test for the soundness issue.
@rust-timer

Copy link
Copy Markdown
Collaborator

Note

This PR was benchmarked as part of triage of its containing rollup: triage URL.

Finished benchmarking commit (1c9b0c4): comparison URL.

Overall result: ❌✅ regressions and improvements - please read:

Our benchmarks found a performance regression caused by this PR.
This might be an actual regression, but it can also be just noise.

Next Steps:

  • If the regression was expected or you think it can be justified,
    please write a comment with sufficient written justification, and add
    @rustbot label: +perf-regression-triaged to it, to mark the regression as triaged.
  • If you think that you know of a way to resolve the regression, try to create
    a new PR with a fix for the regression.
  • If you do not understand the regression or you think that it is just noise,
    you can ask the @rust-lang/wg-compiler-performance working group for help (members of this group
    were already notified of this PR).

@rustbot label: +perf-regression
cc @rust-lang/wg-compiler-performance

Instruction count

Our most reliable metric. Used to determine the overall result above. However, even this metric can be noisy.

mean range count
Regressions ❌
(primary)
0.2% [0.1%, 0.4%] 33
Regressions ❌
(secondary)
0.2% [0.1%, 0.3%] 11
Improvements ✅
(primary)
- - 0
Improvements ✅
(secondary)
-0.1% [-0.1%, -0.1%] 1
All ❌✅ (primary) 0.2% [0.1%, 0.4%] 33

Max RSS (memory usage)

Results (primary -0.4%, secondary -2.2%)

A less reliable metric. May be of interest, but not used to determine the overall result above.

mean range count
Regressions ❌
(primary)
1.4% [1.4%, 1.4%] 1
Regressions ❌
(secondary)
- - 0
Improvements ✅
(primary)
-2.2% [-2.2%, -2.2%] 1
Improvements ✅
(secondary)
-2.2% [-2.2%, -2.2%] 1
All ❌✅ (primary) -0.4% [-2.2%, 1.4%] 2

Cycles

Results (primary 2.6%, secondary 3.8%)

A less reliable metric. May be of interest, but not used to determine the overall result above.

mean range count
Regressions ❌
(primary)
2.6% [2.5%, 2.7%] 2
Regressions ❌
(secondary)
3.8% [3.8%, 3.8%] 1
Improvements ✅
(primary)
- - 0
Improvements ✅
(secondary)
- - 0
All ❌✅ (primary) 2.6% [2.5%, 2.7%] 2

Binary size

This perf run didn't have relevant results for this metric.

Bootstrap: missing data
Artifact size: 406.81 MiB -> 408.83 MiB (0.50%)

@rustbot rustbot added the perf-regression Performance regression. label Sep 17, 2026
@lqd

lqd commented Sep 18, 2026

Copy link
Copy Markdown
Member

This is fine:

  • it's a soundness fix, and the hit is small
  • this is a change in liveness, and we're working towards deferring the eager work done there to loan propagation instead. That will claw back basically all the overhead present in the rustc-perf benchmarks. The open PRs will land soon, and we've landed a few already (already improving performance in liveness).

@rustbot label: +perf-regression-triaged

@rustbot rustbot added the perf-regression-triaged The performance regression has been triaged. label Sep 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

perf-regression Performance regression. perf-regression-triaged The performance regression has been triaged. S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Zpolonius=next soundness bug: UB caused by liveness detection

7 participants