Skip to content

Zpolonius=next soundness bug: UB caused by liveness detection #160670

Closed

Description

@carlini

I tried this code:

#![forbid(unsafe_code)]
use std::fmt::Debug;

trait HasArg { type Arg: Debug; }
impl<'a> HasArg for fn(&'a u8) { type Arg = &'a u8; }

struct D<T: HasArg>(T, Option<T::Arg>);
impl<T: HasArg> Drop for D<T> {
    fn drop(&mut self) { println!("{:?}", self.1); }
}

fn f(_: &u8) {}
fn mk<'a>(r: &'a u8) -> D<fn(&'a u8)> { D(f as fn(&'a u8), Some(r)) }

fn main() {
    let b = Box::new(42u8);
    let d;
    d = mk(&*b);
    drop(b);
}   // D::drop prints *(&*b) after the Box was freed

compiled with -Zpolonius=next.

I expected to see this happen: the program is rejected

Instead, this happened: -Zpolonius=next accepts it and gives a UB. Standard build rejects it.

Meta

rustc --version --verbose:

rustc 1.99.0-nightly (1ed2df61a 2026-08-04)

LLM-written explanation of what it thinks is happening

Summary: -Zpolonius=next propagates a loan from (R, P) to
(R, succ(P)) only along a liveness edge, and the direction of that edge
comes from the recorded variance of R. For a local that is only ever
drop-live, the variance is extracted from the dropck kinds instead of the
local's type — and for an ADT with a user Drop impl those are the generic
arguments verbatim. A lifetime that is contravariant in the generic argument
but reachable (invariantly) through an associated-type projection field gets a
Backward edge, i.e. no forward propagation at all, so the loan is live
nowhere on the local's drop-live range and a conflicting move is accepted.

What goes wrong

  • compiler/rustc_borrowck/src/polonius/liveness_constraints.rs:66-74 —
    covariant ⇒ Forward, contravariant ⇒ Backward, invariant ⇒
    Bidirectional.
  • compiler/rustc_borrowck/src/polonius/constraints.rs:271-287 —
    compute_forward_successor returns None for a Backward region: it has
    no forward successor.
  • Variances are recorded by VarianceExtractor on every value handed to
    make_all_regions_live (type_check/liveness/trace.rs:625-631).

let d; d = mk(..); has no initializer, hence no FakeRead(ForLet), hence
no Use appearance at all — only Defs and Drops — so compute_for_all_locals
skips the use-live path and only add_drop_live_facts_for
(trace.rs:547-600) runs. That function does not feed make_all_regions_live
the local's type D<fn(&'?4 u8)>; it feeds each dropck kind (trace.rs:590-591),
and for an ADT with a user Drop impl, destructor_constraints yields the
ADT's generic arguments verbatim: fn(&'?4 u8). There '?4 is
contravariant ⇒ Backward ⇒ no forward liveness edge ⇒ the loan of *b is
live nowhere on d's drop-live range ⇒ drop(b) is accepted.

But D really stores a &'?4 u8 through the projection field
<T as HasArg>::Arg (which makes T invariant in D), and its destructor
reads it. The variance of the kind is not the variance of the stored
data
.

Controls (all on the unfixed compiler):

variant result
as above, default borrowck rejected E0505
as above, -Zpolonius (legacy) rejected E0505
give d an initializer (let d = mk(&*b);) rejected E0505 under =next too (use-liveness records the local's type, invariant)
remove the Drop impl accepted everywhere, sound (no destructor reads the field)

Fix direction: record the regions of dropck kinds as invariant
(Bidirectional) in add_drop_live_facts_for, rather than reusing the
syntactic variance extractor on the kind. Drop executes arbitrary user code
on the stored data; directionality derived from the kind's surface syntax is
not sound for it.

Activity

  1. added
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Aug 7, 2026
  2. added
    P-highHigh priority
    I-unsoundIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness
    and removed
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Aug 7, 2026
  3. added
    T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.
    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.
    requires-nightlyThis issue requires a nightly compiler in some way. When possible, use a F-* label instead.
    A-destructorsArea: Destructors (`Drop`, …)
    on Aug 7, 2026
  4. maxdexh commented on Aug 7, 2026

    @maxdexh
    Member

    Simplified example that segfaults (playground)

    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);
        // D::drop prints *(&*b) after the Box was freed. Note that manually moving out of `d` correctly errors.
    }
  5. added 2 commits that reference this issue on Sep 16, 2026
    54abfe0
    82c24f8
  6. added 3 commits that reference this issue on Sep 17, 2026
    30ec521
    e79ce97
    25f675c
  7. added 2 commits that reference this issue on Sep 17, 2026
    aae995f
    d025f9b
  8. added a commit that references this issue on Sep 17, 2026
    1c9b0c4
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-borrow-checkerArea: The borrow checkerA-destructorsArea: Destructors (`Drop`, …)C-bugCategory: This is a bug.I-unsoundIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/SoundnessNLL-poloniusIssues related for using Polonius in the borrow checkerP-highHigh priorityT-compilerRelevant to the compiler team, which will review and decide on the PR/issue.T-typesRelevant to the types team, which will review and decide on the PR/issue.requires-nightlyThis issue requires a nightly compiler in some way. When possible, use a F-* label instead.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions