Skip to content

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

Open
amandasystems wants to merge 1 commit into
rust-lang:mainfrom
amandasystems:issue-160670
Open

amandasystems wants to merge 1 commit into
rust-lang:mainfrom
amandasystems:issue-160670

Conversation

@amandasystems

@amandasystems amandasystems commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

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.

@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 should add some comment about why this test is interesting here, drop polonius alpha from the name and have revisions to show the failure should happen regardless of the borrowck algorithm we use.

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 there are no revisions in the test.

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 2 times, most recently from 3752ec6 to 7679b11 Compare September 2, 2026 14:30
@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


// 2. Otherwise, gather the edges due to explicit region liveness, when applicable.
if !live_regions.contains(region, next_point) {
debug!("Region {region:?} isn't live at successor {next_point:?}; traversal stops.");

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.

Suggested change
debug!("Region {region:?} isn't live at successor {next_point:?}; traversal stops.");
debug!(?region, ?next_point, "region isn't live at successor");

ConstraintDirection::Backward => {
// Contravariant cases: loans flow in the inverse direction, but we're only interested
// in forward successors and there are none here.
debug!("Constraint direction is backwards; {region:?} has no forward successors");

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.

Probably should just move above the match: debug!(?direction);

Comment on lines +305 to +307
debug!(
"Backwards successor: {region:?} not live at current point {current_point:?}; bailing out!"
);

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.

Suggested change
debug!(
"Backwards successor: {region:?} not live at current point {current_point:?}; bailing out!"
);
debug!(?region, ?current_point, "region isn't live at current point");

@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.

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.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Zpolonius=next soundness bug: UB caused by liveness detection

6 participants